M .eslintrc.js => .eslintrc.js +67 -6
@@ 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',
M .github/workflows/test-migrations-one-step.yml => .github/workflows/test-migrations-one-step.yml +9 -1
@@ 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
M .github/workflows/test-migrations-two-step.yml => .github/workflows/test-migrations-two-step.yml +9 -1
@@ 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
M .rubocop_todo.yml => .rubocop_todo.yml +0 -90
@@ 22,12 22,6 @@ Layout/ArgumentAlignment:
- '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
# SupportedColonStyles: key, separator, table
@@ 40,12 34,6 @@ Layout/HashAlignment:
- '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:
Exclude:
@@ 53,14 41,6 @@ Layout/LeadingCommentSpace:
- '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
Layout/SpaceInLambdaLiteral:
@@ 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:
@@ 278,31 240,6 @@ Naming/VariableNumber:
- '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:
- 'app/lib/rss/builder.rb'
@@ 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'
M Dockerfile => Dockerfile +1 -1
@@ 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 && \
M Gemfile => Gemfile +57 -23
@@ 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 'private_address_check', '~> 0.5'
+
+group :test do
+ # RSpec runner for rails
gem 'rspec-rails', '~> 6.0'
+
+ # Used to split testing into chunks in CI
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
+ # RSpec progress bar formatter
+ gem 'fuubar', '~> 2.5'
-group :production, :test do
- gem 'private_address_check', '~> 0.5'
-end
+ # Extra RSpec extenion methods and helpers for sidekiq
+ gem 'rspec-sidekiq', '~> 3.1'
-group :test do
+ # Browser integration testing
gem 'capybara', '~> 3.39'
- gem 'climate_control'
+
+ # 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'
- gem 'rails-controller-testing', '~> 1.0'
- gem 'rspec_junit_formatter', '~> 0.6'
- gem 'rspec-sidekiq', '~> 3.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'
M Gemfile.lock => Gemfile.lock +24 -27
@@ 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
M app/controllers/api/v1/admin/canonical_email_blocks_controller.rb => app/controllers/api/v1/admin/canonical_email_blocks_controller.rb +1 -1
@@ 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
M app/controllers/api/v1/admin/domain_allows_controller.rb => app/controllers/api/v1/admin/domain_allows_controller.rb +1 -1
@@ 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)
M app/controllers/api/v1/emails/confirmations_controller.rb => app/controllers/api/v1/emails/confirmations_controller.rb +8 -3
@@ 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!
M app/controllers/api/v1/featured_tags_controller.rb => app/controllers/api/v1/featured_tags_controller.rb +2 -2
@@ 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
M app/controllers/api/v1/statuses/reblogs_controller.rb => app/controllers/api/v1/statuses/reblogs_controller.rb +5 -1
@@ 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
M app/controllers/auth/registrations_controller.rb => app/controllers/auth/registrations_controller.rb +1 -1
@@ 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
M app/controllers/auth/setup_controller.rb => app/controllers/auth/setup_controller.rb +1 -1
@@ 45,6 45,6 @@ class Auth::SetupController < ApplicationController
end
def set_pack
- use_pack 'auth'
+ use_pack 'sign_up'
end
end
M app/controllers/oauth/authorized_applications_controller.rb => app/controllers/oauth/authorized_applications_controller.rb +12 -0
@@ 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
M app/javascript/core/theme.yml => app/javascript/core/theme.yml +1 -0
@@ 16,4 16,5 @@ pack:
modal: public.js
public: public.js
settings: settings.js
+ sign_up:
share:
M app/javascript/flavours/glitch/actions/app.ts => app/javascript/flavours/glitch/actions/app.ts +3 -2
@@ 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<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
M app/javascript/flavours/glitch/actions/pin_statuses.js => app/javascript/flavours/glitch/actions/pin_statuses.js +2 -2
@@ 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());
M app/javascript/flavours/glitch/components/account.jsx => app/javascript/flavours/glitch/components/account.jsx +2 -2
@@ 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' },
M app/javascript/flavours/glitch/components/admin/Counter.jsx => app/javascript/flavours/glitch/components/admin/Counter.jsx +1 -1
@@ 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;
M app/javascript/flavours/glitch/components/admin/Dimension.jsx => app/javascript/flavours/glitch/components/admin/Dimension.jsx +1 -1
@@ 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 {
M app/javascript/flavours/glitch/components/animated_number.tsx => app/javascript/flavours/glitch/components/animated_number.tsx +11 -4
@@ 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<Props> = ({ value, obfuscate }) => {
const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1 | -1>(1);
@@ 64,7 67,11 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
transform: `translateY(${style.y * 100}%)`,
}}
>
- {obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
+ {obfuscate ? (
+ obfuscatedCount(data as number)
+ ) : (
+ <ShortNumber value={data as number} />
+ )}
</span>
))}
</span>
M app/javascript/flavours/glitch/components/autosuggest_input.jsx => app/javascript/flavours/glitch/components/autosuggest_input.jsx +1 -1
@@ 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 });
}
M app/javascript/flavours/glitch/components/autosuggest_textarea.jsx => app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +1 -1
@@ 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 });
}
M app/javascript/flavours/glitch/components/avatar.tsx => app/javascript/flavours/glitch/components/avatar.tsx +5 -3
@@ 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<Props> = ({
account,
M app/javascript/flavours/glitch/components/blurhash.tsx => app/javascript/flavours/glitch/components/blurhash.tsx +5 -4
@@ 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<HTMLCanvasElement> {
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<Props> = ({
hash,
width = 32,
@@ 21,6 21,7 @@ const Blurhash: React.FC<Props> = ({
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
M app/javascript/flavours/glitch/components/column.jsx => app/javascript/flavours/glitch/components/column.jsx +6 -4
@@ 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);
}
}
D app/javascript/flavours/glitch/components/display_name.jsx => app/javascript/flavours/glitch/components/display_name.jsx +0 -83
@@ 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 => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).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 = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
- suffix = <span className='display-name__account'>@{acct}</span>;
- } else {
- displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
- suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
- }
-
- return (
- <span className={classNames('display-name', { inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
- {displayName}
- {inline ? ' ' : null}
- {suffix}
- </span>
- );
- }
-
-}
A app/javascript/flavours/glitch/components/display_name.tsx => app/javascript/flavours/glitch/components/display_name.tsx +124 -0
@@ 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<Account>;
+ localDomain: string;
+ inline?: boolean;
+}
+export class DisplayName extends React.PureComponent<Props> {
+ handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
+ currentTarget,
+ }) => {
+ if (autoPlayGif) {
+ return;
+ }
+
+ const emojis =
+ currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
+
+ emojis.forEach((emoji) => {
+ const originalSrc = emoji.getAttribute('data-original');
+ if (originalSrc != null) emoji.src = originalSrc;
+ });
+ };
+
+ handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
+ currentTarget,
+ }) => {
+ if (autoPlayGif) {
+ return;
+ }
+
+ const emojis =
+ currentTarget.querySelectorAll<HTMLImageElement>('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) => (
+ <bdi key={a.get('id')}>
+ <strong
+ className='display-name__html'
+ dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
+ />
+ </bdi>
+ ))
+ .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 = (
+ <bdi>
+ <strong
+ className='display-name__html'
+ dangerouslySetInnerHTML={{
+ __html: account.get('display_name_html'),
+ }}
+ />
+ </bdi>
+ );
+ suffix = <span className='display-name__account'>@{acct}</span>;
+ } else {
+ displayName = (
+ <bdi>
+ <strong className='display-name__html'>
+ <Skeleton width='10ch' />
+ </strong>
+ </bdi>
+ );
+ suffix = (
+ <span className='display-name__account'>
+ <Skeleton width='7ch' />
+ </span>
+ );
+ }
+
+ return (
+ <span
+ className={classNames('display-name', { inline })}
+ onMouseEnter={this.handleMouseEnter}
+ onMouseLeave={this.handleMouseLeave}
+ >
+ {displayName}
+ {inline ? ' ' : null}
+ {suffix}
+ </span>
+ );
+ }
+}
M app/javascript/flavours/glitch/components/domain.tsx => app/javascript/flavours/glitch/components/domain.tsx +6 -3
@@ 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<Props> = ({ domain, onUnblockDomain, intl }) => {
const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain);
M => +6 -5
@@ 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);
}
M app/javascript/flavours/glitch/components/gifv.tsx => app/javascript/flavours/glitch/components/gifv.tsx +2 -2
@@ 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<Props> = ({
src,
M app/javascript/flavours/glitch/components/hashtag.jsx => app/javascript/flavours/glitch/components/hashtag.jsx +1 -1
@@ 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 {
M app/javascript/flavours/glitch/components/icon.tsx => app/javascript/flavours/glitch/components/icon.tsx +4 -3
@@ 1,13 1,14 @@
import React from 'react';
+
import classNames from 'classnames';
-type Props = {
+interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string;
className?: string;
fixedWidth?: boolean;
children?: never;
- [key: string]: any;
-};
+}
+
export const Icon: React.FC<Props> = ({
id,
className,
M app/javascript/flavours/glitch/components/icon_button.tsx => app/javascript/flavours/glitch/components/icon_button.tsx +7 -5
@@ 1,9 1,11 @@
import React from 'react';
+
import classNames from 'classnames';
-import { Icon } from './icon';
+
import { AnimatedNumber } from './animated_number';
+import { Icon } from './icon';
-type Props = {
+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<Props, States> {
static defaultProps = {
size: 18,
M app/javascript/flavours/glitch/components/icon_with_badge.tsx => app/javascript/flavours/glitch/components/icon_with_badge.tsx +3 -2
@@ 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<Props> = ({
id,
count,
M app/javascript/flavours/glitch/components/media_gallery.jsx => app/javascript/flavours/glitch/components/media_gallery.jsx +2 -2
@@ 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) => {
M app/javascript/flavours/glitch/components/modal_root.jsx => app/javascript/flavours/glitch/components/modal_root.jsx +1 -1
@@ 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;
M app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx => app/javascript/flavours/glitch/components/not_signed_in_indicator.tsx +2 -1
@@ 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 = () => (
<div className='empty-column-indicator'>
<FormattedMessage
id='not_signed_in_indicator.not_signed_in'
- defaultMessage='You need to sign in to access this resource.'
+ defaultMessage='You need to login to access this resource.'
/>
</div>
</div>
M app/javascript/flavours/glitch/components/radio_button.tsx => app/javascript/flavours/glitch/components/radio_button.tsx +3 -2
@@ 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<HTMLInputElement>) => void;
label: React.ReactNode;
-};
+}
export const RadioButton: React.FC<Props> = ({
name,
M app/javascript/flavours/glitch/components/relative_timestamp.tsx => app/javascript/flavours/glitch/components/relative_timestamp.tsx +7 -5
@@ 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<Props, States> {
state = {
now: this.props.intl.now(),
M app/javascript/flavours/glitch/components/scrollable_list.jsx => app/javascript/flavours/glitch/components/scrollable_list.jsx +6 -4
@@ 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);
}
}
M app/javascript/flavours/glitch/components/server_banner.jsx => app/javascript/flavours/glitch/components/server_banner.jsx +3 -3
@@ 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 {
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div>
- <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
+ <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<div className='server-banner__description'>
{isLoading ? (
R app/javascript/flavours/glitch/components/image.tsx => app/javascript/flavours/glitch/components/server_hero_image.tsx +6 -4
@@ 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<Props> = ({
+export const ServerHeroImage: React.FC<Props> = ({
src,
srcSet,
blurhash,
D app/javascript/flavours/glitch/components/skeleton.jsx => app/javascript/flavours/glitch/components/skeleton.jsx +0 -11
@@ 1,11 0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
-
-Skeleton.propTypes = {
- width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-};
-
-export default Skeleton;
A app/javascript/flavours/glitch/components/skeleton.tsx => app/javascript/flavours/glitch/components/skeleton.tsx +12 -0
@@ 0,0 1,12 @@
+import React from 'react';
+
+interface Props {
+ width?: number | string;
+ height?: number | string;
+}
+
+export const Skeleton: React.FC<Props> = ({ width, height }) => (
+ <span className='skeleton' style={{ width, height }}>
+ ‌
+ </span>
+);
M app/javascript/flavours/glitch/components/status.jsx => app/javascript/flavours/glitch/components/status.jsx +3 -2
@@ 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 => {
M => +1 -1
@@ 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 {
M app/javascript/flavours/glitch/components/status_list.jsx => app/javascript/flavours/glitch/components/status_list.jsx +3 -1
@@ 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) {
D app/javascript/flavours/glitch/components/timeline_hint.jsx => app/javascript/flavours/glitch/components/timeline_hint.jsx +0 -18
@@ 1,18 0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
-
-const TimelineHint = ({ resource, url }) => (
- <div className='timeline-hint'>
- <strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
- <br />
- <a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
- </div>
-);
-
-TimelineHint.propTypes = {
- resource: PropTypes.node.isRequired,
- url: PropTypes.string.isRequired,
-};
-
-export default TimelineHint;
A app/javascript/flavours/glitch/components/timeline_hint.tsx => app/javascript/flavours/glitch/components/timeline_hint.tsx +27 -0
@@ 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<Props> = ({ resource, url }) => (
+ <div className='timeline-hint'>
+ <strong>
+ <FormattedMessage
+ id='timeline_hint.remote_resource_not_displayed'
+ defaultMessage='{resource} from other servers are not displayed.'
+ values={{ resource }}
+ />
+ </strong>
+ <br />
+ <a href={url} target='_blank' rel='noopener noreferrer'>
+ <FormattedMessage
+ id='account.browse_more_on_origin_server'
+ defaultMessage='Browse more on the original profile'
+ />
+ </a>
+ </div>
+);
M app/javascript/flavours/glitch/containers/media_container.jsx => app/javascript/flavours/glitch/containers/media_container.jsx +8 -6
@@ 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 {...props} key={`media-${i}`} />,
component,
);
@@ 105,6 106,7 @@ export default class MediaContainer extends PureComponent {
<MediaModal
media={this.state.media}
index={this.state.index || 0}
+ lang={this.state.lang}
currentTime={this.state.options?.startTime}
autoPlay={this.state.options?.autoPlay}
volume={this.state.options?.defaultVolume}
M app/javascript/flavours/glitch/containers/status_container.js => app/javascript/flavours/glitch/containers/status_container.js +4 -4
@@ 211,12 211,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
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) {
M app/javascript/flavours/glitch/features/about/index.jsx => app/javascript/flavours/glitch/features/about/index.jsx +3 -3
@@ 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 {
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable about'>
<div className='about__header'>
- <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
+ <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
</div>
M app/javascript/flavours/glitch/features/account_gallery/index.jsx => app/javascript/flavours/glitch/features/account_gallery/index.jsx +4 -3
@@ 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 }));
}
};
M app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx => app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx +1 -1
@@ 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 {
M app/javascript/flavours/glitch/features/account_timeline/index.jsx => app/javascript/flavours/glitch/features/account_timeline/index.jsx +3 -3
@@ 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) {
M app/javascript/flavours/glitch/features/audio/index.jsx => app/javascript/flavours/glitch/features/audio/index.jsx +1 -1
@@ 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 });
}
M app/javascript/flavours/glitch/features/blocks/index.jsx => app/javascript/flavours/glitch/features/blocks/index.jsx +1 -1
@@ 34,7 34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchBlocks());
}
M app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx => app/javascript/flavours/glitch/features/bookmarked_statuses/index.jsx +1 -1
@@ 34,7 34,7 @@ class Bookmarks extends ImmutablePureComponent {
isLoading: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses());
}
M app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx => app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +1 -1
@@ 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';
M => +7 -6
@@ 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) => {
M app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx => app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +6 -6
@@ 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);
}
M app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx => app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +4 -3
@@ 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);
}
M app/javascript/flavours/glitch/features/directory/components/account_card.jsx => app/javascript/flavours/glitch/features/directory/components/account_card.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/domain_blocks/index.jsx => app/javascript/flavours/glitch/features/domain_blocks/index.jsx +1 -1
@@ 34,7 34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchDomainBlocks());
}
M app/javascript/flavours/glitch/features/explore/components/story.jsx => app/javascript/flavours/glitch/features/explore/components/story.jsx +1 -1
@@ 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 {
M app/javascript/flavours/glitch/features/favourited_statuses/index.jsx => app/javascript/flavours/glitch/features/favourited_statuses/index.jsx +1 -1
@@ 34,7 34,7 @@ class Favourites extends ImmutablePureComponent {
isLoading: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
}
M app/javascript/flavours/glitch/features/favourites/index.jsx => app/javascript/flavours/glitch/features/favourites/index.jsx +2 -2
@@ 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));
}
M app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx => app/javascript/flavours/glitch/features/follow_recommendations/components/account.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx => app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/follow_requests/index.jsx => app/javascript/flavours/glitch/features/follow_requests/index.jsx +1 -1
@@ 39,7 39,7 @@ class FollowRequests extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchFollowRequests());
}
M app/javascript/flavours/glitch/features/followers/index.jsx => app/javascript/flavours/glitch/features/followers/index.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/following/index.jsx => app/javascript/flavours/glitch/features/following/index.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/getting_started/index.jsx => app/javascript/flavours/glitch/features/getting_started/index.jsx +1 -1
@@ 96,7 96,7 @@ class GettingStarted extends ImmutablePureComponent {
openSettings: PropTypes.func.isRequired,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.fetchLists();
}
M app/javascript/flavours/glitch/features/interaction_modal/index.jsx => app/javascript/flavours/glitch/features/interaction_modal/index.jsx +1 -1
@@ 143,7 143,7 @@ class InteractionModal extends React.PureComponent {
<div className='interaction-modal__choices'>
<div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
- <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
+ <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
{signupButton}
</div>
M app/javascript/flavours/glitch/features/list_adder/components/account.jsx => app/javascript/flavours/glitch/features/list_adder/components/account.jsx +1 -1
@@ 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 = () => {
M app/javascript/flavours/glitch/features/list_editor/components/account.jsx => app/javascript/flavours/glitch/features/list_editor/components/account.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/list_timeline/index.jsx => app/javascript/flavours/glitch/features/list_timeline/index.jsx +1 -1
@@ 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;
M app/javascript/flavours/glitch/features/lists/index.jsx => app/javascript/flavours/glitch/features/lists/index.jsx +1 -1
@@ 42,7 42,7 @@ class Lists extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchLists());
}
M app/javascript/flavours/glitch/features/mutes/index.jsx => app/javascript/flavours/glitch/features/mutes/index.jsx +1 -1
@@ 35,7 35,7 @@ class Mutes extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchMutes());
}
M app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx => app/javascript/flavours/glitch/features/notifications/components/follow_request.jsx +1 -1
@@ 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';
M => +1 -1
@@ 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({
M app/javascript/flavours/glitch/features/pinned_statuses/index.jsx => app/javascript/flavours/glitch/features/pinned_statuses/index.jsx +1 -1
@@ 29,7 29,7 @@ class PinnedStatuses extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchPinnedStatuses());
}
M app/javascript/flavours/glitch/features/privacy_policy/index.jsx => app/javascript/flavours/glitch/features/privacy_policy/index.jsx +1 -1
@@ 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' },
M app/javascript/flavours/glitch/features/reblogs/index.jsx => app/javascript/flavours/glitch/features/reblogs/index.jsx +2 -2
@@ 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));
}
M app/javascript/flavours/glitch/features/report/components/status_check_box.jsx => app/javascript/flavours/glitch/features/report/components/status_check_box.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/status/components/card.jsx => app/javascript/flavours/glitch/features/status/components/card.jsx +1 -1
@@ 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 });
}
M app/javascript/flavours/glitch/features/status/components/detailed_status.jsx => app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js => app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +4 -4
@@ 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) {
M app/javascript/flavours/glitch/features/status/index.jsx => app/javascript/flavours/glitch/features/status/index.jsx +4 -4
@@ 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 => {
M app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx => app/javascript/flavours/glitch/features/ui/components/actions_modal.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx => app/javascript/flavours/glitch/features/ui/components/boost_modal.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/ui/components/bundle.jsx => app/javascript/flavours/glitch/features/ui/components/bundle.jsx +2 -2
@@ 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);
}
M app/javascript/flavours/glitch/features/ui/components/columns_area.jsx => app/javascript/flavours/glitch/features/ui/components/columns_area.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx => app/javascript/flavours/glitch/features/ui/components/embed_modal.jsx +1 -1
@@ 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'
/>
</div>
M app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx => app/javascript/flavours/glitch/features/ui/components/favourite_modal.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx => app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx +1 -1
@@ 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';
M => +3 -3
@@ 52,13 52,13 @@ class Header extends React.PureComponent {
if (registrationsOpen) {
signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'>
<a href='/auth/sign_up' className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
@@ 66,8 66,8 @@ class Header extends React.PureComponent {
content = (
<>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
M app/javascript/flavours/glitch/features/ui/components/media_modal.jsx => app/javascript/flavours/glitch/features/ui/components/media_modal.jsx +6 -10
@@ 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);
M app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx => app/javascript/flavours/glitch/features/ui/components/onboarding_modal.jsx +1 -1
@@ 184,7 184,7 @@ class OnboardingModal extends React.PureComponent {
currentIndex: 0,
};
- componentWillMount() {
+ UNSAFE_componentWillMount() {
const { myAccount, admin, domain, intl } = this.props;
this.pages = [
<PageOne key='1' acct={myAccount.get('acct')} domain={domain} />,
M app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx => app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx +4 -4
@@ 16,13 16,13 @@ const SignInBanner = () => {
if (registrationsOpen) {
signupButton = (
- <a href='/auth/sign_up' className='button button--block button-tertiary'>
+ <a href='/auth/sign_up' className='button button--block'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
- <button className='button button--block button-tertiary' onClick={openClosedRegistrationsModal}>
+ <button className='button button--block' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
@@ 30,9 30,9 @@ const SignInBanner = () => {
return (
<div className='sign-in-banner'>
- <p><FormattedMessage id='sign_in_banner.text' 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.' /></p>
- <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
+ <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
{signupButton}
+ <a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</div>
);
};
M app/javascript/flavours/glitch/features/ui/components/upload_area.jsx => app/javascript/flavours/glitch/features/ui/components/upload_area.jsx +1 -1
@@ 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';
M app/javascript/flavours/glitch/features/ui/containers/status_list_container.js => app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +1 -0
@@ 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']),
M app/javascript/flavours/glitch/features/ui/index.jsx => app/javascript/flavours/glitch/features/ui/index.jsx +3 -3
@@ 64,7 64,7 @@ import Header from './components/header';
// Dummy import, to make sure that <Status /> 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);
M app/javascript/flavours/glitch/features/video/index.jsx => app/javascript/flavours/glitch/features/video/index.jsx +2 -2
@@ 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,
M app/javascript/flavours/glitch/is_mobile.ts => app/javascript/flavours/glitch/is_mobile.ts +1 -0
@@ 1,4 1,5 @@
import { supportsPassiveEvents } from 'detect-passive-events';
+
import { forceSingleColumn } from 'flavours/glitch/initial_state';
const LAYOUT_BREAKPOINT = 630;
M app/javascript/flavours/glitch/main.jsx => app/javascript/flavours/glitch/main.jsx +3 -2
@@ 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(<Mastodon {...props} />, mountNode);
+ const root = createRoot(mountNode);
+ root.render(<Mastodon {...props} />);
store.dispatch(setupBrowserNotifications());
if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
M app/javascript/flavours/glitch/packs/admin.jsx => app/javascript/flavours/glitch/packs/admin.jsx +6 -4
@@ 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 (
<AdminComponent locale={locale}>
<Component {...componentProps} />
- </AdminComponent>
- ), element);
+ </AdminComponent>,
+ );
});
}).catch(error => {
console.error(error);
M app/javascript/flavours/glitch/packs/common.js => app/javascript/flavours/glitch/packs/common.js +2 -2
@@ 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);
M app/javascript/flavours/glitch/packs/public.jsx => app/javascript/flavours/glitch/packs/public.jsx +3 -2
@@ 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(<MediaContainer locale={locale} components={reactComponents} />, content);
+ const root = createRoot(content);
+ root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
M app/javascript/flavours/glitch/packs/share.jsx => app/javascript/flavours/glitch/packs/share.jsx +4 -3
@@ 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(<ComposeContainer {...props} />, mountNode);
+ const root = createRoot(mountNode);
+ root.render(<ComposeContainer {...props} />);
}
}
A app/javascript/flavours/glitch/packs/sign_up.js => app/javascript/flavours/glitch/packs/sign_up.js +15 -0
@@ 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);
+});
M app/javascript/flavours/glitch/polyfills/base_polyfills.ts => app/javascript/flavours/glitch/polyfills/base_polyfills.ts +7 -2
@@ 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) {
M app/javascript/flavours/glitch/reducers/index.ts => app/javascript/flavours/glitch/reducers/index.ts +35 -34
@@ 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 contexts from './contexts';
+import conversations from './conversations';
import custom_emojis from './custom_emojis';
-import lists from './lists';
-import listEditor from './list_editor';
-import listAdder from './list_adder';
+import domain_lists from './domain_lists';
+import dropdown_menu from './dropdown_menu';
import filters from './filters';
-import conversations from './conversations';
-import suggestions from './suggestions';
-import pinnedAccountsEditor from './pinned_accounts_editor';
-import polls from './polls';
-import trends from './trends';
-import announcements from './announcements';
+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 account_notes from './account_notes';
+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 accounts_map from './accounts_map';
-import history from './history';
+import pinnedAccountsEditor from './pinned_accounts_editor';
+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 followed_tags from './followed_tags';
+import timelines from './timelines';
+import trends from './trends';
+import user_lists from './user_lists';
const reducers = {
announcements,
M app/javascript/flavours/glitch/reducers/markers.js => app/javascript/flavours/glitch/reducers/markers.js +2 -2
@@ 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:
M app/javascript/flavours/glitch/store/index.ts => app/javascript/flavours/glitch/store/index.ts +21 -3
@@ 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'],
M app/javascript/flavours/glitch/store/middlewares/errors.ts => app/javascript/flavours/glitch/store/middlewares/errors.ts +6 -4
@@ 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<Record<string, never>, 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));
}
}
M app/javascript/flavours/glitch/store/middlewares/loading_bar.ts => app/javascript/flavours/glitch/store/middlewares/loading_bar.ts +13 -10
@@ 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());
+ }
}
}
M app/javascript/flavours/glitch/store/middlewares/sounds.ts => app/javascript/flavours/glitch/store/middlewares/sounds.ts +13 -10
@@ 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);
+ };
};
M app/javascript/flavours/glitch/styles/components/media.scss => app/javascript/flavours/glitch/styles/components/media.scss +2 -12
@@ 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%);
}
}
M app/javascript/flavours/glitch/theme.yml => app/javascript/flavours/glitch/theme.yml +1 -0
@@ 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
M app/javascript/flavours/glitch/types/resources.ts => app/javascript/flavours/glitch/types/resources.ts +4 -4
@@ 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
M app/javascript/flavours/glitch/utils/dom_helpers.js => app/javascript/flavours/glitch/utils/dom_helpers.js +0 -7
@@ 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;
M app/javascript/flavours/glitch/utils/resize_image.js => app/javascript/flavours/glitch/utils/resize_image.js +3 -1
@@ 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;
M app/javascript/flavours/glitch/uuid.ts => app/javascript/flavours/glitch/uuid.ts +4 -3
@@ 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);
}
M app/javascript/flavours/vanilla/theme.yml => app/javascript/flavours/vanilla/theme.yml +1 -0
@@ 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
M app/javascript/mastodon/actions/app.ts => app/javascript/mastodon/actions/app.ts +3 -2
@@ 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<ChangeLayoutPayload>('APP_LAYOUT_CHANGE');
M app/javascript/mastodon/actions/pin_statuses.js => app/javascript/mastodon/actions/pin_statuses.js +2 -2
@@ 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());
M app/javascript/mastodon/components/__tests__/display_name-test.jsx => app/javascript/mastodon/components/__tests__/display_name-test.jsx +1 -1
@@ 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('<DisplayName />', () => {
it('renders display name + account name', () => {
M app/javascript/mastodon/components/account.jsx => app/javascript/mastodon/components/account.jsx +3 -16
@@ 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 (
- <div className={classNames('account', { 'account--minimal': minimal })}>
- <div className='account__wrapper'>
- <div className='account__display-name'>
- <div className='account__avatar-wrapper'><Skeleton width={size} height={size} /></div>
-
- <div>
- <DisplayName />
- <Skeleton width='7ch' />
- </div>
- </div>
- </div>
- </div>
- );
+ return <EmptyAccount size={size} minimal={minimal} />;
}
if (hidden) {
M app/javascript/mastodon/components/admin/Counter.jsx => app/javascript/mastodon/components/admin/Counter.jsx +1 -1
@@ 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;
M app/javascript/mastodon/components/admin/Dimension.jsx => app/javascript/mastodon/components/admin/Dimension.jsx +1 -1
@@ 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 {
M app/javascript/mastodon/components/animated_number.tsx => app/javascript/mastodon/components/animated_number.tsx +11 -4
@@ 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<Props> = ({ value, obfuscate }) => {
const [previousValue, setPreviousValue] = useState(value);
const [direction, setDirection] = useState<1 | -1>(1);
@@ 64,7 67,11 @@ export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
transform: `translateY(${style.y * 100}%)`,
}}
>
- {obfuscate ? obfuscatedCount(data) : <ShortNumber value={data} />}
+ {obfuscate ? (
+ obfuscatedCount(data as number)
+ ) : (
+ <ShortNumber value={data as number} />
+ )}
</span>
))}
</span>
M app/javascript/mastodon/components/autosuggest_input.jsx => app/javascript/mastodon/components/autosuggest_input.jsx +1 -1
@@ 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 });
}
M app/javascript/mastodon/components/autosuggest_textarea.jsx => app/javascript/mastodon/components/autosuggest_textarea.jsx +1 -1
@@ 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 });
}
M app/javascript/mastodon/components/avatar.tsx => app/javascript/mastodon/components/avatar.tsx +5 -3
@@ 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<Props> = ({
account,
M app/javascript/mastodon/components/avatar_overlay.tsx => app/javascript/mastodon/components/avatar_overlay.tsx +4 -3
@@ 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<Props> = ({
account,
M app/javascript/mastodon/components/blurhash.tsx => app/javascript/mastodon/components/blurhash.tsx +5 -4
@@ 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<HTMLCanvasElement> {
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<Props> = ({
hash,
width = 32,
@@ 21,6 21,7 @@ const Blurhash: React.FC<Props> = ({
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
M app/javascript/mastodon/components/column.jsx => app/javascript/mastodon/components/column.jsx +6 -4
@@ 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);
}
}
D app/javascript/mastodon/components/display_name.jsx => app/javascript/mastodon/components/display_name.jsx +0 -79
@@ 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 => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).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 = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
- suffix = <span className='display-name__account'>@{acct}</span>;
- } else {
- displayName = <bdi><strong className='display-name__html'><Skeleton width='10ch' /></strong></bdi>;
- suffix = <span className='display-name__account'><Skeleton width='7ch' /></span>;
- }
-
- return (
- <span className='display-name' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
- {displayName} {suffix}
- </span>
- );
- }
-
-}
A app/javascript/mastodon/components/display_name.tsx => app/javascript/mastodon/components/display_name.tsx +121 -0
@@ 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<Account>;
+ localDomain?: string;
+}
+
+export class DisplayName extends React.PureComponent<Props> {
+ handleMouseEnter: React.ReactEventHandler<HTMLSpanElement> = ({
+ currentTarget,
+ }) => {
+ if (autoPlayGif) {
+ return;
+ }
+
+ const emojis =
+ currentTarget.querySelectorAll<HTMLImageElement>('img.custom-emoji');
+
+ emojis.forEach((emoji) => {
+ const originalSrc = emoji.getAttribute('data-original');
+ if (originalSrc != null) emoji.src = originalSrc;
+ });
+ };
+
+ handleMouseLeave: React.ReactEventHandler<HTMLSpanElement> = ({
+ currentTarget,
+ }) => {
+ if (autoPlayGif) {
+ return;
+ }
+
+ const emojis =
+ currentTarget.querySelectorAll<HTMLImageElement>('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) => (
+ <bdi key={a.get('id')}>
+ <strong
+ className='display-name__html'
+ dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
+ />
+ </bdi>
+ ))
+ .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 = (
+ <bdi>
+ <strong
+ className='display-name__html'
+ dangerouslySetInnerHTML={{
+ __html: account.get('display_name_html'),
+ }}
+ />
+ </bdi>
+ );
+ suffix = <span className='display-name__account'>@{acct}</span>;
+ } else {
+ displayName = (
+ <bdi>
+ <strong className='display-name__html'>
+ <Skeleton width='10ch' />
+ </strong>
+ </bdi>
+ );
+ suffix = (
+ <span className='display-name__account'>
+ <Skeleton width='7ch' />
+ </span>
+ );
+ }
+
+ return (
+ <span
+ className='display-name'
+ onMouseEnter={this.handleMouseEnter}
+ onMouseLeave={this.handleMouseLeave}
+ >
+ {displayName} {suffix}
+ </span>
+ );
+ }
+}
M app/javascript/mastodon/components/domain.tsx => app/javascript/mastodon/components/domain.tsx +6 -3
@@ 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<Props> = ({ domain, onUnblockDomain, intl }) => {
const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain);
M => +6 -5
@@ 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);
}
A app/javascript/mastodon/components/empty_account.tsx => app/javascript/mastodon/components/empty_account.tsx +33 -0
@@ 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<Props> = ({
+ size = 46,
+ minimal = false,
+}) => {
+ return (
+ <div className={classNames('account', { 'account--minimal': minimal })}>
+ <div className='account__wrapper'>
+ <div className='account__display-name'>
+ <div className='account__avatar-wrapper'>
+ <Skeleton width={size} height={size} />
+ </div>
+
+ <div>
+ <DisplayName />
+ <Skeleton width='7ch' />
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
M app/javascript/mastodon/components/gifv.tsx => app/javascript/mastodon/components/gifv.tsx +2 -2
@@ 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<Props> = ({
src,
M app/javascript/mastodon/components/hashtag.jsx => app/javascript/mastodon/components/hashtag.jsx +1 -1
@@ 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 {
M app/javascript/mastodon/components/icon.tsx => app/javascript/mastodon/components/icon.tsx +4 -3
@@ 1,13 1,14 @@
import React from 'react';
+
import classNames from 'classnames';
-type Props = {
+interface Props extends React.HTMLAttributes<HTMLImageElement> {
id: string;
className?: string;
fixedWidth?: boolean;
children?: never;
- [key: string]: any;
-};
+}
+
export const Icon: React.FC<Props> = ({
id,
className,
M app/javascript/mastodon/components/icon_button.tsx => app/javascript/mastodon/components/icon_button.tsx +7 -5
@@ 1,9 1,11 @@
import React from 'react';
+
import classNames from 'classnames';
-import { Icon } from './icon';
+
import { AnimatedNumber } from './animated_number';
+import { Icon } from './icon';
-type Props = {
+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<Props, States> {
static defaultProps = {
size: 18,
M app/javascript/mastodon/components/icon_with_badge.tsx => app/javascript/mastodon/components/icon_with_badge.tsx +3 -2
@@ 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<Props> = ({
id,
count,
R app/javascript/mastodon/components/logo.jsx => app/javascript/mastodon/components/logo.tsx +3 -4
@@ 1,15 1,14 @@
import React from 'react';
+
import logo from 'mastodon/../images/logo.svg';
-export const WordmarkLogo = () => (
+export const WordmarkLogo: React.FC = () => (
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' />
</svg>
);
-export const SymbolLogo = () => (
+export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' />
);
-
-export default WordmarkLogo;
M app/javascript/mastodon/components/media_gallery.jsx => app/javascript/mastodon/components/media_gallery.jsx +2 -2
@@ 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 => {
M app/javascript/mastodon/components/modal_root.jsx => app/javascript/mastodon/components/modal_root.jsx +1 -1
@@ 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;
M app/javascript/mastodon/components/not_signed_in_indicator.tsx => app/javascript/mastodon/components/not_signed_in_indicator.tsx +2 -1
@@ 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 = () => (
<div className='empty-column-indicator'>
<FormattedMessage
id='not_signed_in_indicator.not_signed_in'
- defaultMessage='You need to sign in to access this resource.'
+ defaultMessage='You need to login to access this resource.'
/>
</div>
</div>
M app/javascript/mastodon/components/radio_button.tsx => app/javascript/mastodon/components/radio_button.tsx +3 -2
@@ 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<HTMLInputElement>) => void;
label: React.ReactNode;
-};
+}
export const RadioButton: React.FC<Props> = ({
name,
M app/javascript/mastodon/components/relative_timestamp.tsx => app/javascript/mastodon/components/relative_timestamp.tsx +7 -5
@@ 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<Props, States> {
state = {
now: this.props.intl.now(),
M app/javascript/mastodon/components/scrollable_list.jsx => app/javascript/mastodon/components/scrollable_list.jsx +6 -4
@@ 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);
}
}
M app/javascript/mastodon/components/server_banner.jsx => app/javascript/mastodon/components/server_banner.jsx +3 -3
@@ 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 {
<FormattedMessage id='server_banner.introduction' defaultMessage='{domain} is part of the decentralized social network powered by {mastodon}.' values={{ domain: <strong>{domain}</strong>, mastodon: <a href='https://joinmastodon.org' target='_blank'>Mastodon</a> }} />
</div>
- <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
+ <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} className='server-banner__hero' />
<div className='server-banner__description'>
{isLoading ? (
R app/javascript/mastodon/components/image.tsx => app/javascript/mastodon/components/server_hero_image.tsx +6 -4
@@ 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<Props> = ({
+export const ServerHeroImage: React.FC<Props> = ({
src,
srcSet,
blurhash,
D app/javascript/mastodon/components/skeleton.jsx => app/javascript/mastodon/components/skeleton.jsx +0 -11
@@ 1,11 0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
-
-Skeleton.propTypes = {
- width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-};
-
-export default Skeleton;
A app/javascript/mastodon/components/skeleton.tsx => app/javascript/mastodon/components/skeleton.tsx +12 -0
@@ 0,0 1,12 @@
+import React from 'react';
+
+interface Props {
+ width?: number | string;
+ height?: number | string;
+}
+
+export const Skeleton: React.FC<Props> = ({ width, height }) => (
+ <span className='skeleton' style={{ width, height }}>
+ ‌
+ </span>
+);
M app/javascript/mastodon/components/status.jsx => app/javascript/mastodon/components/status.jsx +7 -5
@@ 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);
}
}
};
M app/javascript/mastodon/components/status_list.jsx => app/javascript/mastodon/components/status_list.jsx +3 -1
@@ 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) {
D app/javascript/mastodon/components/timeline_hint.jsx => app/javascript/mastodon/components/timeline_hint.jsx +0 -18
@@ 1,18 0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
-
-const TimelineHint = ({ resource, url }) => (
- <div className='timeline-hint'>
- <strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
- <br />
- <a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
- </div>
-);
-
-TimelineHint.propTypes = {
- resource: PropTypes.node.isRequired,
- url: PropTypes.string.isRequired,
-};
-
-export default TimelineHint;
A app/javascript/mastodon/components/timeline_hint.tsx => app/javascript/mastodon/components/timeline_hint.tsx +27 -0
@@ 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<Props> = ({ resource, url }) => (
+ <div className='timeline-hint'>
+ <strong>
+ <FormattedMessage
+ id='timeline_hint.remote_resource_not_displayed'
+ defaultMessage='{resource} from other servers are not displayed.'
+ values={{ resource }}
+ />
+ </strong>
+ <br />
+ <a href={url} target='_blank' rel='noopener noreferrer'>
+ <FormattedMessage
+ id='account.browse_more_on_origin_server'
+ defaultMessage='Browse more on the original profile'
+ />
+ </a>
+ </div>
+);
M app/javascript/mastodon/components/verified_badge.tsx => app/javascript/mastodon/components/verified_badge.tsx +3 -2
@@ 1,9 1,10 @@
import React from 'react';
+
import { Icon } from './icon';
-type Props = {
+interface Props {
link: string;
-};
+}
export const VerifiedBadge: React.FC<Props> = ({ link }) => (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
M app/javascript/mastodon/containers/media_container.jsx => app/javascript/mastodon/containers/media_container.jsx +8 -6
@@ 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 {...props} key={`media-${i}`} />,
component,
);
@@ 105,6 106,7 @@ export default class MediaContainer extends PureComponent {
<MediaModal
media={this.state.media}
index={this.state.index || 0}
+ lang={this.state.lang}
currentTime={this.state.options?.startTime}
autoPlay={this.state.options?.autoPlay}
volume={this.state.options?.defaultVolume}
M app/javascript/mastodon/containers/status_container.jsx => app/javascript/mastodon/containers/status_container.jsx +4 -4
@@ 182,12 182,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
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) {
M app/javascript/mastodon/features/about/index.jsx => app/javascript/mastodon/features/about/index.jsx +3 -3
@@ 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 {
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
<div className='scrollable about'>
<div className='about__header'>
- <Image blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
+ <ServerHeroImage blurhash={server.getIn(['thumbnail', 'blurhash'])} src={server.getIn(['thumbnail', 'url'])} srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${value} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' />
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank'>Mastodon</a> }} /></p>
</div>
M app/javascript/mastodon/features/account/components/account_note.jsx => app/javascript/mastodon/features/account/components/account_note.jsx +3 -3
@@ 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 = {};
M app/javascript/mastodon/features/account_gallery/index.jsx => app/javascript/mastodon/features/account_gallery/index.jsx +4 -3
@@ 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 }));
}
};
M app/javascript/mastodon/features/account_timeline/components/moved_note.jsx => app/javascript/mastodon/features/account_timeline/components/moved_note.jsx +1 -1
@@ 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 {
M app/javascript/mastodon/features/account_timeline/index.jsx => app/javascript/mastodon/features/account_timeline/index.jsx +2 -3
@@ 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';
M app/javascript/mastodon/features/audio/index.jsx => app/javascript/mastodon/features/audio/index.jsx +1 -1
@@ 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 });
}
M app/javascript/mastodon/features/blocks/index.jsx => app/javascript/mastodon/features/blocks/index.jsx +1 -1
@@ 34,7 34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchBlocks());
}
M app/javascript/mastodon/features/bookmarked_statuses/index.jsx => app/javascript/mastodon/features/bookmarked_statuses/index.jsx +1 -1
@@ 34,7 34,7 @@ class Bookmarks extends ImmutablePureComponent {
isLoading: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchBookmarkedStatuses());
}
M app/javascript/mastodon/features/compose/components/autosuggest_account.jsx => app/javascript/mastodon/features/compose/components/autosuggest_account.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx => app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.jsx +6 -6
@@ 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);
}
M app/javascript/mastodon/features/compose/components/language_dropdown.jsx => app/javascript/mastodon/features/compose/components/language_dropdown.jsx +4 -3
@@ 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);
}
M app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx => app/javascript/mastodon/features/compose/components/privacy_dropdown.jsx +5 -4
@@ 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 = [
M app/javascript/mastodon/features/compose/components/reply_indicator.jsx => app/javascript/mastodon/features/compose/components/reply_indicator.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/directory/components/account_card.jsx => app/javascript/mastodon/features/directory/components/account_card.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/domain_blocks/index.jsx => app/javascript/mastodon/features/domain_blocks/index.jsx +1 -1
@@ 34,7 34,7 @@ class Blocks extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchDomainBlocks());
}
M app/javascript/mastodon/features/explore/components/story.jsx => app/javascript/mastodon/features/explore/components/story.jsx +1 -1
@@ 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 {
M app/javascript/mastodon/features/favourited_statuses/index.jsx => app/javascript/mastodon/features/favourited_statuses/index.jsx +1 -1
@@ 34,7 34,7 @@ class Favourites extends ImmutablePureComponent {
isLoading: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchFavouritedStatuses());
}
M app/javascript/mastodon/features/favourites/index.jsx => app/javascript/mastodon/features/favourites/index.jsx +2 -2
@@ 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));
}
M app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx => app/javascript/mastodon/features/follow_requests/components/account_authorize.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/follow_requests/index.jsx => app/javascript/mastodon/features/follow_requests/index.jsx +1 -1
@@ 39,7 39,7 @@ class FollowRequests extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchFollowRequests());
}
M app/javascript/mastodon/features/followers/index.jsx => app/javascript/mastodon/features/followers/index.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/following/index.jsx => app/javascript/mastodon/features/following/index.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/interaction_modal/index.jsx => app/javascript/mastodon/features/interaction_modal/index.jsx +1 -1
@@ 143,7 143,7 @@ class InteractionModal extends React.PureComponent {
<div className='interaction-modal__choices'>
<div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
- <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
+ <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
{signupButton}
</div>
M app/javascript/mastodon/features/list_adder/components/account.jsx => app/javascript/mastodon/features/list_adder/components/account.jsx +1 -1
@@ 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 = () => {
M app/javascript/mastodon/features/list_editor/components/account.jsx => app/javascript/mastodon/features/list_editor/components/account.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/list_timeline/index.jsx => app/javascript/mastodon/features/list_timeline/index.jsx +1 -1
@@ 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;
M app/javascript/mastodon/features/lists/index.jsx => app/javascript/mastodon/features/lists/index.jsx +1 -1
@@ 42,7 42,7 @@ class Lists extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchLists());
}
M app/javascript/mastodon/features/mutes/index.jsx => app/javascript/mastodon/features/mutes/index.jsx +1 -1
@@ 35,7 35,7 @@ class Mutes extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchMutes());
}
M app/javascript/mastodon/features/notifications/components/follow_request.jsx => app/javascript/mastodon/features/notifications/components/follow_request.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/notifications/index.jsx => app/javascript/mastodon/features/notifications/index.jsx +1 -1
@@ 93,7 93,7 @@ class Notifications extends React.PureComponent {
trackScroll: true,
};
- componentWillMount() {
+ UNSAFE_componentWillMount() {
this.props.dispatch(mountNotifications());
}
M app/javascript/mastodon/features/onboarding/follows.jsx => app/javascript/mastodon/features/onboarding/follows.jsx +5 -4
@@ 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 (
<Column>
- <ColumnBackButton onClick={onBack} />
+ <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
<div className='scrollable privacy-policy'>
<div className='column-title'>
@@ 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);
M app/javascript/mastodon/features/onboarding/index.jsx => app/javascript/mastodon/features/onboarding/index.jsx +5 -4
@@ 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 <Follows onBack={this.handleBackClick} />;
+ return <Follows onBack={this.handleBackClick} multiColumn={multiColumn} />;
case 'share':
- return <Share onBack={this.handleBackClick} />;
+ return <Share onBack={this.handleBackClick} multiColumn={multiColumn} />;
}
return (
@@ 114,7 115,7 @@ class Onboarding extends ImmutablePureComponent {
<div className='onboarding__steps'>
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
- <Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage='You curate your own feed. Lets fill it with interesting people.' />} />
+ <Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own feed. Let's fill it with interesting people." />} />
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
M app/javascript/mastodon/features/onboarding/share.jsx => app/javascript/mastodon/features/onboarding/share.jsx +3 -2
@@ 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 (
<Column>
- <ColumnBackButton onClick={onBack} />
+ <ColumnBackButton multiColumn={multiColumn} onClick={onBack} />
<div className='scrollable privacy-policy'>
<div className='column-title'>
M => +1 -1
@@ 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({
M app/javascript/mastodon/features/pinned_statuses/index.jsx => app/javascript/mastodon/features/pinned_statuses/index.jsx +1 -1
@@ 29,7 29,7 @@ class PinnedStatuses extends ImmutablePureComponent {
multiColumn: PropTypes.bool,
};
- componentWillMount () {
+ UNSAFE_componentWillMount () {
this.props.dispatch(fetchPinnedStatuses());
}
M app/javascript/mastodon/features/privacy_policy/index.jsx => app/javascript/mastodon/features/privacy_policy/index.jsx +1 -1
@@ 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' },
M app/javascript/mastodon/features/reblogs/index.jsx => app/javascript/mastodon/features/reblogs/index.jsx +2 -2
@@ 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));
}
M app/javascript/mastodon/features/report/components/status_check_box.jsx => app/javascript/mastodon/features/report/components/status_check_box.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/status/components/card.jsx => app/javascript/mastodon/features/status/components/card.jsx +1 -1
@@ 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 });
}
M app/javascript/mastodon/features/status/components/detailed_status.jsx => app/javascript/mastodon/features/status/components/detailed_status.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/status/containers/detailed_status_container.js => app/javascript/mastodon/features/status/containers/detailed_status_container.js +4 -4
@@ 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) {
M app/javascript/mastodon/features/status/index.jsx => app/javascript/mastodon/features/status/index.jsx +6 -6
@@ 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 => {
M app/javascript/mastodon/features/ui/components/boost_modal.jsx => app/javascript/mastodon/features/ui/components/boost_modal.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/ui/components/bundle.jsx => app/javascript/mastodon/features/ui/components/bundle.jsx +2 -2
@@ 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);
}
M app/javascript/mastodon/features/ui/components/columns_area.jsx => app/javascript/mastodon/features/ui/components/columns_area.jsx +2 -2
@@ 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);
}
M app/javascript/mastodon/features/ui/components/embed_modal.jsx => app/javascript/mastodon/features/ui/components/embed_modal.jsx +1 -1
@@ 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'
/>
</div>
M app/javascript/mastodon/features/ui/components/focal_point_modal.jsx => app/javascript/mastodon/features/ui/components/focal_point_modal.jsx +1 -2
@@ 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';
M => +3 -3
@@ 51,13 51,13 @@ class Header extends React.PureComponent {
if (registrationsOpen) {
signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'>
<a href='/auth/sign_up' className='button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
<button className='button' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
@@ 65,8 65,8 @@ class Header extends React.PureComponent {
content = (
<>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton}
<a href='/auth/sign_in' className='button button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</>
);
}
M app/javascript/mastodon/features/ui/components/media_modal.jsx => app/javascript/mastodon/features/ui/components/media_modal.jsx +6 -10
@@ 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);
M app/javascript/mastodon/features/ui/components/navigation_panel.jsx => app/javascript/mastodon/features/ui/components/navigation_panel.jsx +2 -2
@@ 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 (
<div className='navigation-panel'>
<div className='navigation-panel__logo'>
- <Link to='/' className='column-link column-link--logo'><Logo /></Link>
+ <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
<hr />
</div>
M app/javascript/mastodon/features/ui/components/sign_in_banner.jsx => app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +4 -4
@@ 16,13 16,13 @@ const SignInBanner = () => {
if (registrationsOpen) {
signupButton = (
- <a href='/auth/sign_up' className='button button--block button-tertiary'>
+ <a href='/auth/sign_up' className='button button--block'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
- <button className='button button--block button-tertiary' onClick={openClosedRegistrationsModal}>
+ <button className='button button--block' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
@@ 30,9 30,9 @@ const SignInBanner = () => {
return (
<div className='sign-in-banner'>
- <p><FormattedMessage id='sign_in_banner.text' 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.' /></p>
- <a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
+ <p><FormattedMessage id='sign_in_banner.text' defaultMessage='Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
{signupButton}
+ <a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
</div>
);
};
M app/javascript/mastodon/features/ui/components/upload_area.jsx => app/javascript/mastodon/features/ui/components/upload_area.jsx +1 -1
@@ 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';
M app/javascript/mastodon/features/ui/containers/status_list_container.js => app/javascript/mastodon/features/ui/containers/status_list_container.js +1 -0
@@ 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']),
M app/javascript/mastodon/features/ui/index.jsx => app/javascript/mastodon/features/ui/index.jsx +1 -1
@@ 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);
M app/javascript/mastodon/features/video/index.jsx => app/javascript/mastodon/features/video/index.jsx +2 -2
@@ 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,
M app/javascript/mastodon/is_mobile.ts => app/javascript/mastodon/is_mobile.ts +1 -0
@@ 1,4 1,5 @@
import { supportsPassiveEvents } from 'detect-passive-events';
+
import { forceSingleColumn } from './initial_state';
const LAYOUT_BREAKPOINT = 630;
M app/javascript/mastodon/locales/defaultMessages.json => app/javascript/mastodon/locales/defaultMessages.json +6 -6
@@ 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"
}
],
M app/javascript/mastodon/locales/en.json => app/javascript/mastodon/locales/en.json +3 -3
@@ 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",
M app/javascript/mastodon/locales/locale-data/co.js => app/javascript/mastodon/locales/locale-data/co.js +3 -1
@@ 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;
M app/javascript/mastodon/locales/locale-data/oc.js => app/javascript/mastodon/locales/locale-data/oc.js +3 -1
@@ 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;
M app/javascript/mastodon/locales/locale-data/sa.js => app/javascript/mastodon/locales/locale-data/sa.js +4 -3
@@ 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;
M app/javascript/mastodon/main.jsx => app/javascript/mastodon/main.jsx +3 -2
@@ 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(<Mastodon {...props} />, mountNode);
+ const root = createRoot(mountNode);
+ root.render(<Mastodon {...props} />);
store.dispatch(setupBrowserNotifications());
if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
M app/javascript/mastodon/polyfills/base_polyfills.ts => app/javascript/mastodon/polyfills/base_polyfills.ts +7 -2
@@ 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) {
M app/javascript/mastodon/reducers/index.ts => app/javascript/mastodon/reducers/index.ts +33 -32
@@ 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 contexts from './contexts';
+import conversations from './conversations';
import custom_emojis from './custom_emojis';
-import lists from './lists';
-import listEditor from './list_editor';
-import listAdder from './list_adder';
+import domain_lists from './domain_lists';
+import dropdown_menu from './dropdown_menu';
import filters from './filters';
-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 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 accounts_map from './accounts_map';
-import history from './history';
+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 followed_tags from './followed_tags';
+import timelines from './timelines';
+import trends from './trends';
+import user_lists from './user_lists';
const reducers = {
announcements,
M app/javascript/mastodon/reducers/markers.js => app/javascript/mastodon/reducers/markers.js +2 -2
@@ 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:
M app/javascript/mastodon/reducers/missed_updates.ts => app/javascript/mastodon/reducers/missed_updates.ts +5 -3
@@ 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';
+import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
-type MissedUpdatesState = {
+interface MissedUpdatesState {
focused: boolean;
unread: number;
-};
+}
const initialState = Record<MissedUpdatesState>({
focused: true,
unread: 0,
M app/javascript/mastodon/store/index.ts => app/javascript/mastodon/store/index.ts +21 -3
@@ 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'],
M app/javascript/mastodon/store/middlewares/errors.ts => app/javascript/mastodon/store/middlewares/errors.ts +5 -4
@@ 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<Record<string, never>, 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));
}
}
M app/javascript/mastodon/store/middlewares/loading_bar.ts => app/javascript/mastodon/store/middlewares/loading_bar.ts +13 -10
@@ 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());
+ }
}
}
M app/javascript/mastodon/store/middlewares/sounds.ts => app/javascript/mastodon/store/middlewares/sounds.ts +13 -10
@@ 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);
+ };
};
M app/javascript/mastodon/utils/__tests__/html-test.js => app/javascript/mastodon/utils/__tests__/html-test.js +1 -1
@@ 1,7 1,7 @@
import * as html from '../html';
describe('html', () => {
- describe('unsecapeHTML', () => {
+ describe('unescapeHTML', () => {
it('returns unescaped HTML', () => {
const output = html.unescapeHTML('<p>lorem</p><p>ipsum</p><br><br>');
expect(output).toEqual('lorem\n\nipsum\n<br>');
M app/javascript/mastodon/uuid.ts => app/javascript/mastodon/uuid.ts +4 -3
@@ 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);
}
M app/javascript/packs/admin.jsx => app/javascript/packs/admin.jsx +6 -4
@@ 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 (
<AdminComponent locale={locale}>
<Component {...componentProps} />
- </AdminComponent>
- ), element);
+ </AdminComponent>,
+ );
});
}).catch(error => {
console.error(error);
M app/javascript/packs/public.jsx => app/javascript/packs/public.jsx +3 -2
@@ 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(<MediaContainer locale={locale} components={reactComponents} />, content);
+ const root = createRoot(content);
+ root.render(<MediaContainer locale={locale} components={reactComponents} />);
document.body.appendChild(content);
scrollToDetailedStatus();
})
M app/javascript/packs/share.jsx => app/javascript/packs/share.jsx +3 -2
@@ 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(<ComposeContainer {...props} />, mountNode);
+ const root = createRoot(mountNode);
+ root.render(<ComposeContainer {...props} />);
}
}
A app/javascript/packs/sign_up.js => app/javascript/packs/sign_up.js +15 -0
@@ 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);
+});
M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +3 -15
@@ 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%);
}
}
M app/javascript/styles/mastodon/forms.scss => app/javascript/styles/mastodon/forms.scss +8 -0
@@ 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;
M app/javascript/types/image.d.ts => app/javascript/types/image.d.ts +0 -5
@@ 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;
M app/javascript/types/resources.ts => app/javascript/types/resources.ts +4 -4
@@ 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
M app/lib/account_reach_finder.rb => app/lib/account_reach_finder.rb +8 -1
@@ 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
M app/lib/activitypub/activity/flag.rb => app/lib/activitypub/activity/flag.rb +5 -1
@@ 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
M app/lib/activitypub/tag_manager.rb => app/lib/activitypub/tag_manager.rb +4 -0
@@ 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
M app/lib/admin/metrics/dimension.rb => app/lib/admin/metrics/dimension.rb +2 -2
@@ 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
M app/lib/admin/metrics/measure.rb => app/lib/admin/metrics/measure.rb +2 -2
@@ 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
M app/lib/application_extension.rb => app/lib/application_extension.rb +0 -4
@@ 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
M => +1 -1
@@ 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
M app/lib/feed_manager.rb => app/lib/feed_manager.rb +4 -4
@@ 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)
M => +1 -1
@@ 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
M app/lib/vacuum/access_tokens_vacuum.rb => app/lib/vacuum/access_tokens_vacuum.rb +4 -2
@@ 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
M app/models/account.rb => app/models/account.rb +2 -2
@@ 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)
M app/models/account_statuses_cleanup_policy.rb => app/models/account_statuses_cleanup_policy.rb +2 -2
@@ 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
M app/models/account_suggestions/setting_source.rb => app/models/account_suggestions/setting_source.rb +2 -2
@@ 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
M app/models/account_suggestions/source.rb => app/models/account_suggestions/source.rb +1 -1
@@ 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
M app/models/follow_recommendation_filter.rb => app/models/follow_recommendation_filter.rb +1 -1
@@ 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
M app/models/form/account_batch.rb => app/models/form/account_batch.rb +11 -0
@@ 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)
M app/models/form/admin_settings.rb => app/models/form/admin_settings.rb +1 -0
@@ 41,6 41,7 @@ class Form::AdminSettings
content_cache_retention_period
backups_retention_period
status_page_url
+ captcha_enabled
).freeze
INTEGER_KEYS = %i(
M app/models/notification.rb => app/models/notification.rb +1 -1
@@ 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)
M app/models/report.rb => app/models/report.rb +4 -5
@@ 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
M app/models/user_role.rb => app/models/user_role.rb +1 -1
@@ 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)
M app/models/webhook.rb => app/models/webhook.rb +1 -1
@@ 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
M app/services/backup_service.rb => app/services/backup_service.rb +2 -2
@@ 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)
M app/services/process_mentions_service.rb => app/services/process_mentions_service.rb +1 -1
@@ 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))
M app/validators/existing_username_validator.rb => app/validators/existing_username_validator.rb +2 -2
@@ 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)
M app/validators/vote_validator.rb => app/validators/vote_validator.rb +5 -1
@@ 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?
M app/views/admin/reports/_media_attachments.html.haml => app/views/admin/reports/_media_attachments.html.haml +3 -3
@@ 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 }
M app/views/admin/settings/registrations/show.html.haml => app/views/admin/settings/registrations/show.html.haml +1 -1
@@ 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 }
M app/views/auth/confirmations/captcha.html.haml => app/views/auth/confirmations/captcha.html.haml +1 -0
@@ 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')
M app/views/oauth/authorized_applications/index.html.haml => app/views/oauth/authorized_applications/index.html.haml +2 -2
@@ 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')
M app/workers/post_process_media_worker.rb => app/workers/post_process_media_worker.rb +1 -1
@@ 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
M config/initializers/ffmpeg.rb => config/initializers/ffmpeg.rb +1 -1
@@ 1,3 1,3 @@
if ENV['FFMPEG_BINARY'].present?
- FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY']
+ FFMPEG.ffmpeg_binary = ENV['FFMPEG_BINARY']
end
M config/initializers/omniauth.rb => config/initializers/omniauth.rb +1 -1
@@ 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 }
M config/initializers/paperclip.rb => config/initializers/paperclip.rb +4 -4
@@ 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'],
M config/initializers/rack_attack.rb => config/initializers/rack_attack.rb +1 -1
@@ 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]]
M config/initializers/webauthn.rb => config/initializers/webauthn.rb +1 -1
@@ 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"
M config/locales/devise.en.yml => config/locales/devise.en.yml +3 -3
@@ 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
M config/locales/en.yml => config/locales/en.yml +10 -3
@@ 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, <strong>this can make the registration process significantly less accessible to some (especially disabled) people</strong>. 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 <a href="/about/more">contact the server administrator</a> 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 <strong>%{domain}</strong> 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 <strong>%{domain}</strong> 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
M config/routes/api.rb => config/routes/api.rb +1 -0
@@ 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
M config/settings.yml => config/settings.yml +1 -1
@@ 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
M config/webpack/generateLocalePacks.js => config/webpack/generateLocalePacks.js +1 -1
@@ 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) {
M db/migrate/20200407202420_migrate_unavailable_inboxes.rb => db/migrate/20200407202420_migrate_unavailable_inboxes.rb +2 -2
@@ 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
M jest.config.js => jest.config.js +0 -1
@@ 10,7 10,6 @@ const config = {
'<rootDir>/tmp/',
'<rootDir>/app/javascript/themes/',
],
- setupFiles: ['raf/polyfill'],
setupFilesAfterEnv: ['<rootDir>/app/javascript/mastodon/test_setup.js'],
collectCoverageFrom: [
'app/javascript/mastodon/**/*.{js,jsx,ts,tsx}',
M lib/mastodon/media_cli.rb => lib/mastodon/media_cli.rb +1 -1
@@ 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
M lib/tasks/tests.rake => lib/tasks/tests.rake +1 -1
@@ 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
M package.json => package.json +20 -20
@@ 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"
}
M spec/controllers/activitypub/followers_synchronizations_controller_spec.rb => spec/controllers/activitypub/followers_synchronizations_controller_spec.rb +0 -2
@@ 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
M spec/controllers/activitypub/outboxes_controller_spec.rb => spec/controllers/activitypub/outboxes_controller_spec.rb +0 -2
@@ 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
M spec/controllers/admin/announcements_controller_spec.rb => spec/controllers/admin/announcements_controller_spec.rb +55 -0
@@ 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
M spec/controllers/admin/confirmations_controller_spec.rb => spec/controllers/admin/confirmations_controller_spec.rb +1 -1
@@ 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) }
M spec/controllers/admin/disputes/appeals_controller_spec.rb => spec/controllers/admin/disputes/appeals_controller_spec.rb +5 -5
@@ 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')) }
M spec/controllers/admin/reports/actions_controller_spec.rb => spec/controllers/admin/reports/actions_controller_spec.rb +2 -2
@@ 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'
M spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb => spec/controllers/api/v1/admin/canonical_email_blocks_controller_spec.rb +339 -4
@@ 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
M spec/controllers/api/v1/admin/domain_allows_controller_spec.rb => spec/controllers/api/v1/admin/domain_allows_controller_spec.rb +8 -0
@@ 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
M spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb => spec/controllers/api/v1/admin/email_domain_blocks_controller_spec.rb +264 -3
@@ 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
M spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb => spec/controllers/api/v1/admin/ip_blocks_controller_spec.rb +290 -4
@@ 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
+
+ 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
- get :index, params: { account_id: account.id, limit: 2 }
+ 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
M spec/controllers/api/v1/emails/confirmations_controller_spec.rb => spec/controllers/api/v1/emails/confirmations_controller_spec.rb +68 -0
@@ 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
D spec/controllers/api/v1/featured_tags_controller_spec.rb => spec/controllers/api/v1/featured_tags_controller_spec.rb +0 -23
@@ 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
M spec/controllers/auth/registrations_controller_spec.rb => spec/controllers/auth/registrations_controller_spec.rb +3 -3
@@ 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'
M spec/controllers/concerns/signature_verification_spec.rb => spec/controllers/concerns/signature_verification_spec.rb +1 -1
@@ 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
M spec/controllers/statuses_controller_spec.rb => spec/controllers/statuses_controller_spec.rb +128 -13
@@ 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
M spec/fabricators/canonical_email_block_fabricator.rb => spec/fabricators/canonical_email_block_fabricator.rb +1 -1
@@ 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
M spec/fabricators/featured_tag_fabricator.rb => spec/fabricators/featured_tag_fabricator.rb +1 -1
@@ 3,5 3,5 @@
Fabricator(:featured_tag) do
account
tag
- name 'Tag'
+ name { sequence(:name) { |i| "Tag#{i}" } }
end
M spec/fabricators/notification_fabricator.rb => spec/fabricators/notification_fabricator.rb +1 -1
@@ 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
A spec/features/captcha_spec.rb => spec/features/captcha_spec.rb +35 -0
@@ 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
A spec/lib/account_reach_finder_spec.rb => spec/lib/account_reach_finder_spec.rb +53 -0
@@ 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
M spec/lib/activitypub/activity/flag_spec.rb => spec/lib/activitypub/activity/flag_spec.rb +31 -0
@@ 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) }
A spec/lib/mastodon/ip_blocks_cli_spec.rb => spec/lib/mastodon/ip_blocks_cli_spec.rb +292 -0
@@ 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
A spec/lib/mastodon/migration_warning_spec.rb => spec/lib/mastodon/migration_warning_spec.rb +34 -0
@@ 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
M spec/lib/vacuum/access_tokens_vacuum_spec.rb => spec/lib/vacuum/access_tokens_vacuum_spec.rb +10 -0
@@ 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
A spec/locales/i18n_spec.rb => spec/locales/i18n_spec.rb +35 -0
@@ 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
M spec/mailers/notification_mailer_spec.rb => spec/mailers/notification_mailer_spec.rb +1 -1
@@ 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
M spec/mailers/user_mailer_spec.rb => spec/mailers/user_mailer_spec.rb +1 -1
@@ 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
M spec/models/account_migration_spec.rb => spec/models/account_migration_spec.rb +1 -1
@@ 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
M spec/models/account_spec.rb => spec/models/account_spec.rb +1 -1
@@ 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
A spec/models/form/account_batch_spec.rb => spec/models/form/account_batch_spec.rb +63 -0
@@ 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
M spec/models/report_spec.rb => spec/models/report_spec.rb +9 -2
@@ 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
M spec/models/user_settings/setting_spec.rb => spec/models/user_settings/setting_spec.rb +1 -1
@@ 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
M spec/policies/report_note_policy_spec.rb => spec/policies/report_note_policy_spec.rb +9 -11
@@ 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
M spec/policies/status_policy_spec.rb => spec/policies/status_policy_spec.rb +102 -90
@@ 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
M spec/presenters/status_relationships_presenter_spec.rb => spec/presenters/status_relationships_presenter_spec.rb +1 -1
@@ 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
A spec/requests/api/v1/featured_tags_spec.rb => spec/requests/api/v1/featured_tags_spec.rb +201 -0
@@ 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
M spec/services/activitypub/process_account_service_spec.rb => spec/services/activitypub/process_account_service_spec.rb +3 -5
@@ 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 = {
M spec/services/backup_service_spec.rb => spec/services/backup_service_spec.rb +21 -0
@@ 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
M spec/services/report_service_spec.rb => spec/services/report_service_spec.rb +9 -3
@@ 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
M spec/services/unsuspend_account_service_spec.rb => spec/services/unsuspend_account_service_spec.rb +3 -3
@@ 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 }
M tsconfig.json => tsconfig.json +2 -0
@@ 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"],
M yarn.lock => yarn.lock +371 -247
@@ 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==
- 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==
+"@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" "^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"