diff --git a/.eslintrc.js b/.eslintrc.js
index 91dcd8e60c..d5f0ae1ac5 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -325,8 +325,8 @@ module.exports = {
extends: [
'eslint:recommended',
- 'plugin:@typescript-eslint/recommended',
- 'plugin:@typescript-eslint/recommended-requiring-type-checking',
+ 'plugin:@typescript-eslint/strict-type-checked',
+ 'plugin:@typescript-eslint/stylistic-type-checked',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
@@ -338,7 +338,7 @@ module.exports = {
],
parserOptions: {
- project: './tsconfig.json',
+ project: true,
tsconfigRootDir: __dirname,
},
@@ -348,6 +348,7 @@ module.exports = {
'@typescript-eslint/consistent-type-definitions': ['warn', 'interface'],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
+ "@typescript-eslint/prefer-nullish-coalescing": ['error', {ignorePrimitives: {boolean: true}}],
'jsdoc/require-jsdoc': 'off',
diff --git a/.github/renovate.json5 b/.github/renovate.json5
index 1ae40d4161..0dddc3e290 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -15,7 +15,6 @@
// Ignore major version bumps for these node packages
matchManagers: ['npm'],
matchPackageNames: [
- '@rails/ujs', // Needs to match the major Rails version
'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes
@@ -51,12 +50,6 @@
'sidekiq', // Requires manual upgrade
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
'redis', // Requires manual upgrade and sync with Sidekiq version
- 'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
-
- // Needs major Rails version bump
- 'rack',
- 'rails',
- 'rails-i18n',
],
matchUpdateTypes: ['major'],
enabled: false,
diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
index f9dd36e36f..c01914a97f 100644
--- a/.github/workflows/build-image.yml
+++ b/.github/workflows/build-image.yml
@@ -52,7 +52,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release
flavor: |
- latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') && 'auto' || 'false' }}
+ latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
tags: |
type=edge,branch=main
type=pep440,pattern={{raw}}
diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml
new file mode 100644
index 0000000000..6c4869f12d
--- /dev/null
+++ b/.github/workflows/bundler-audit.yml
@@ -0,0 +1,40 @@
+name: Bundler Audit
+on:
+ push:
+ branches-ignore:
+ - 'dependabot/**'
+ paths:
+ - 'Gemfile*'
+ - '.ruby-version'
+ - '.bundler-audit.yml'
+ - '.github/workflows/bundler-audit.yml'
+
+ pull_request:
+ paths:
+ - 'Gemfile*'
+ - '.ruby-version'
+ - '.bundler-audit.yml'
+ - '.github/workflows/bundler-audit.yml'
+
+ schedule:
+ - cron: '0 5 * * 1'
+
+jobs:
+ security:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v3
+
+ - name: Install native Ruby dependencies
+ run: sudo apt-get install -y libicu-dev libidn11-dev
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: .ruby-version
+ bundler-cache: true
+
+ - name: Run bundler-audit
+ run: bundle exec bundler-audit
diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml
index 0395c8639f..c898b26325 100644
--- a/.github/workflows/lint-ruby.yml
+++ b/.github/workflows/lint-ruby.yml
@@ -8,7 +8,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- - '.bundler-audit.yml'
+ - 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@@ -18,7 +18,7 @@ on:
- 'Gemfile*'
- '.rubocop*.yml'
- '.ruby-version'
- - '.bundler-audit.yml'
+ - 'config/brakeman.ignore'
- '**/*.rb'
- '**/*.rake'
- '.github/workflows/lint-ruby.yml'
@@ -46,5 +46,6 @@ jobs:
- name: Run rubocop
run: bundle exec rubocop
- - name: Run bundler-audit
- run: bundle exec bundler-audit
+ - name: Run brakeman
+ if: always() # Run both checks, even if the first failed
+ run: bundle exec brakeman
diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml
index 131a62a576..295039c414 100644
--- a/.github/workflows/rebase-needed.yml
+++ b/.github/workflows/rebase-needed.yml
@@ -1,17 +1,8 @@
name: PR Needs Rebase
on:
- push:
- branches-ignore:
- - 'dependabot/**'
- - 'renovate/**'
- - 'l10n_main'
- pull_request_target:
- branches-ignore:
- - 'dependabot/**'
- - 'renovate/**'
- - 'l10n_main'
- types: [synchronize]
+ schedule:
+ - cron: '0 * * * *'
permissions:
pull-requests: write
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index c601683907..31b79f7db2 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -1,73 +1,23 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
-# on 2023-03-15 00:55:01 -0400 using Haml-Lint version 0.45.0.
+# on 2023-07-11 23:58:05 +0200 using Haml-Lint version 0.48.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
- # Offense count: 63
+ # Offense count: 94
RuboCop:
- exclude:
- - 'app/views/accounts/_og.html.haml'
- - 'app/views/admin/account_warnings/_account_warning.html.haml'
- - 'app/views/admin/accounts/index.html.haml'
- - 'app/views/admin/accounts/show.html.haml'
- - 'app/views/admin/announcements/edit.html.haml'
- - 'app/views/admin/announcements/new.html.haml'
- - 'app/views/admin/disputes/appeals/_appeal.html.haml'
- - 'app/views/admin/domain_blocks/edit.html.haml'
- - 'app/views/admin/domain_blocks/new.html.haml'
- - 'app/views/admin/ip_blocks/new.html.haml'
- - 'app/views/admin/reports/actions/preview.html.haml'
- - 'app/views/admin/reports/index.html.haml'
- - 'app/views/admin/reports/show.html.haml'
- - 'app/views/admin/roles/_form.html.haml'
- - 'app/views/admin/settings/about/show.html.haml'
- - 'app/views/admin/settings/appearance/show.html.haml'
- - 'app/views/admin/settings/registrations/show.html.haml'
- - 'app/views/admin/statuses/show.html.haml'
- - 'app/views/auth/registrations/new.html.haml'
- - 'app/views/disputes/strikes/show.html.haml'
- - 'app/views/filters/_filter_fields.html.haml'
- - 'app/views/invites/_form.html.haml'
- - 'app/views/layouts/application.html.haml'
- - 'app/views/layouts/error.html.haml'
- - 'app/views/notification_mailer/_status.html.haml'
- - 'app/views/settings/applications/_fields.html.haml'
- - 'app/views/settings/imports/show.html.haml'
- - 'app/views/settings/preferences/appearance/show.html.haml'
- - 'app/views/settings/preferences/other/show.html.haml'
- - 'app/views/statuses/_detailed_status.html.haml'
- - 'app/views/statuses/_poll.html.haml'
- - 'app/views/statuses/show.html.haml'
- - 'app/views/statuses_cleanup/show.html.haml'
- - 'app/views/user_mailer/warning.html.haml'
+ enabled: false
- # Offense count: 913
+ # Offense count: 960
LineLength:
enabled: false
# Offense count: 22
UnnecessaryStringOutput:
- exclude:
- - 'app/views/accounts/show.html.haml'
- - 'app/views/admin/custom_emojis/_custom_emoji.html.haml'
- - 'app/views/admin/relays/_relay.html.haml'
- - 'app/views/admin/rules/_rule.html.haml'
- - 'app/views/admin/statuses/index.html.haml'
- - 'app/views/auth/registrations/_sessions.html.haml'
- - 'app/views/disputes/strikes/show.html.haml'
- - 'app/views/notification_mailer/_status.html.haml'
- - 'app/views/settings/two_factor_authentication_methods/index.html.haml'
- - 'app/views/statuses/_detailed_status.html.haml'
- - 'app/views/statuses/_poll.html.haml'
- - 'app/views/statuses/_simple_status.html.haml'
- - 'app/views/user_mailer/suspicious_sign_in.html.haml'
- - 'app/views/user_mailer/webauthn_credential_added.html.haml'
- - 'app/views/user_mailer/webauthn_credential_deleted.html.haml'
- - 'app/views/user_mailer/welcome.html.haml'
+ enabled: false
# Offense count: 3
ViewLength:
diff --git a/.rubocop.yml b/.rubocop.yml
index 74f79611fb..051ad9fa97 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -24,7 +24,6 @@ AllCops:
Exclude:
- 'db/schema.rb'
- 'bin/*'
- - 'Rakefile'
- 'node_modules/**/*'
- 'Vagrantfile'
- 'vendor/**/*'
@@ -202,6 +201,11 @@ Style/RedundantBegin:
Style/RescueStandardError:
EnforcedStyle: implicit
+# Reason: Simplify some spec layouts
+# https://docs.rubocop.org/rubocop/cops_style.html#stylesemicolon
+Style/Semicolon:
+ AllowAsExpressionSeparator: true
+
# Reason: Originally disabled for CodeClimate, and no config consensus has been found
# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray
Style/SymbolArray:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 0bf6624788..7104c7a1b2 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
-# using RuboCop version 1.52.1.
+# using RuboCop version 1.54.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -28,7 +28,6 @@ Layout/ArgumentAlignment:
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/HashAlignment:
Exclude:
- - 'config/boot.rb'
- 'config/environments/production.rb'
- 'config/initializers/rack_attack.rb'
- 'config/routes.rb'
@@ -48,15 +47,6 @@ Layout/SpaceInLambdaLiteral:
- 'config/environments/production.rb'
- 'config/initializers/content_security_policy.rb'
-# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowedMethods, AllowedPatterns.
-Lint/AmbiguousBlockAssociation:
- Exclude:
- - 'spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb'
- - 'spec/controllers/settings/two_factor_authentication/otp_authentication_controller_spec.rb'
- - 'spec/services/activitypub/process_status_update_service_spec.rb'
- - 'spec/services/post_status_service_spec.rb'
-
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
Exclude:
@@ -106,11 +96,6 @@ Lint/OrAssignmentToConstant:
Exclude:
- 'lib/sanitize_ext/sanitize_config.rb'
-# This cop supports safe autocorrection (--autocorrect).
-Lint/SendWithMixinArgument:
- Exclude:
- - 'config/application.rb'
-
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@@ -165,10 +150,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity:
Max: 27
-Naming/AccessorMethodName:
- Exclude:
- - 'app/controllers/auth/sessions_controller.rb'
-
# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms.
# CheckDefinitionPathHierarchyRoots: lib, spec, test, src
# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
@@ -176,19 +157,6 @@ Naming/FileName:
Exclude:
- 'config/locales/sr-Latn.rb'
-# This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: EnforcedStyleForLeadingUnderscores.
-# SupportedStylesForLeadingUnderscores: disallowed, required, optional
-Naming/MemoizedInstanceVariableName:
- Exclude:
- - 'app/controllers/api/v1/bookmarks_controller.rb'
- - 'app/controllers/api/v1/favourites_controller.rb'
- - 'app/controllers/concerns/rate_limit_headers.rb'
- - 'app/lib/activitypub/activity.rb'
- - 'app/services/resolve_url_service.rb'
- - 'app/services/search_service.rb'
- - 'config/initializers/rack_attack.rb'
-
# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
@@ -283,7 +251,6 @@ RSpec/HookArgument:
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/services/import_service_spec.rb'
- - 'spec/spec_helper.rb'
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
@@ -398,45 +365,6 @@ RSpec/PendingWithoutReason:
Exclude:
- 'spec/models/account_spec.rb'
-RSpec/StubbedMock:
- Exclude:
- - 'spec/controllers/api/base_controller_spec.rb'
- - 'spec/controllers/api/v1/media_controller_spec.rb'
- - 'spec/controllers/auth/registrations_controller_spec.rb'
- - 'spec/helpers/application_helper_spec.rb'
- - 'spec/lib/status_filter_spec.rb'
- - 'spec/lib/status_finder_spec.rb'
- - 'spec/lib/webfinger_resource_spec.rb'
- - 'spec/services/activitypub/process_collection_service_spec.rb'
-
-RSpec/SubjectDeclaration:
- Exclude:
- - 'spec/controllers/admin/domain_blocks_controller_spec.rb'
- - 'spec/models/account_migration_spec.rb'
- - 'spec/models/account_spec.rb'
- - 'spec/models/relationship_filter_spec.rb'
- - 'spec/models/user_role_spec.rb'
- - 'spec/policies/account_moderation_note_policy_spec.rb'
- - 'spec/policies/account_policy_spec.rb'
- - 'spec/policies/backup_policy_spec.rb'
- - 'spec/policies/custom_emoji_policy_spec.rb'
- - 'spec/policies/domain_block_policy_spec.rb'
- - 'spec/policies/email_domain_block_policy_spec.rb'
- - 'spec/policies/instance_policy_spec.rb'
- - 'spec/policies/invite_policy_spec.rb'
- - 'spec/policies/relay_policy_spec.rb'
- - 'spec/policies/report_note_policy_spec.rb'
- - 'spec/policies/report_policy_spec.rb'
- - 'spec/policies/settings_policy_spec.rb'
- - 'spec/policies/tag_policy_spec.rb'
- - 'spec/policies/user_policy_spec.rb'
- - 'spec/services/activitypub/process_account_service_spec.rb'
-
-RSpec/SubjectStub:
- Exclude:
- - 'spec/services/unallow_domain_service_spec.rb'
- - 'spec/validators/blacklisted_email_validator_spec.rb'
-
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/ApplicationController:
Exclude:
@@ -782,406 +710,6 @@ Style/FormatStringToken:
- 'config/initializers/devise.rb'
- 'lib/paperclip/color_extractor.rb'
-# This cop supports unsafe autocorrection (--autocorrect-all).
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: always, always_true, never
-Style/FrozenStringLiteralComment:
- Exclude:
- - 'app/views/accounts/show.rss.ruby'
- - 'app/views/tags/show.rss.ruby'
- - 'app/views/well_known/host_meta/show.xml.ruby'
- - 'config/application.rb'
- - 'config/boot.rb'
- - 'config/environment.rb'
- - 'config/environments/development.rb'
- - 'config/environments/production.rb'
- - 'config/environments/test.rb'
- - 'config/initializers/0_post_deployment_migrations.rb'
- - 'config/initializers/active_model_serializers.rb'
- - 'config/initializers/application_controller_renderer.rb'
- - 'config/initializers/assets.rb'
- - 'config/initializers/backtrace_silencers.rb'
- - 'config/initializers/cache_logging.rb'
- - 'config/initializers/chewy.rb'
- - 'config/initializers/content_security_policy.rb'
- - 'config/initializers/cookies_serializer.rb'
- - 'config/initializers/cors.rb'
- - 'config/initializers/devise.rb'
- - 'config/initializers/doorkeeper.rb'
- - 'config/initializers/fast_blank.rb'
- - 'config/initializers/ffmpeg.rb'
- - 'config/initializers/filter_parameter_logging.rb'
- - 'config/initializers/http_client_proxy.rb'
- - 'config/initializers/httplog.rb'
- - 'config/initializers/inflections.rb'
- - 'config/initializers/mail_delivery_job.rb'
- - 'config/initializers/makara.rb'
- - 'config/initializers/mime_types.rb'
- - 'config/initializers/oj.rb'
- - 'config/initializers/omniauth.rb'
- - 'config/initializers/open_uri_redirection.rb'
- - 'config/initializers/permissions_policy.rb'
- - 'config/initializers/pghero.rb'
- - 'config/initializers/preload_link_headers.rb'
- - 'config/initializers/premailer_rails.rb'
- - 'config/initializers/rack_attack_logging.rb'
- - 'config/initializers/redis.rb'
- - 'config/initializers/session_store.rb'
- - 'config/initializers/simple_form.rb'
- - 'config/initializers/stoplight.rb'
- - 'config/initializers/trusted_proxies.rb'
- - 'config/initializers/twitter_regex.rb'
- - 'config/initializers/webauthn.rb'
- - 'config/initializers/wrap_parameters.rb'
- - 'config/locales/sr-Latn.rb'
- - 'config/locales/sr.rb'
- - 'config/puma.rb'
- - 'db/migrate/20160220174730_create_accounts.rb'
- - 'db/migrate/20160220211917_create_statuses.rb'
- - 'db/migrate/20160221003140_create_users.rb'
- - 'db/migrate/20160221003621_create_follows.rb'
- - 'db/migrate/20160222122600_create_stream_entries.rb'
- - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb'
- - 'db/migrate/20160223162837_add_metadata_to_statuses.rb'
- - 'db/migrate/20160223164502_make_uris_nullable_in_statuses.rb'
- - 'db/migrate/20160223165723_add_url_to_statuses.rb'
- - 'db/migrate/20160223165855_add_url_to_accounts.rb'
- - 'db/migrate/20160223171800_create_favourites.rb'
- - 'db/migrate/20160224223247_create_mentions.rb'
- - 'db/migrate/20160227230233_add_attachment_avatar_to_accounts.rb'
- - 'db/migrate/20160305115639_add_devise_to_users.rb'
- - 'db/migrate/20160306172223_create_doorkeeper_tables.rb'
- - 'db/migrate/20160312193225_add_attachment_header_to_accounts.rb'
- - 'db/migrate/20160314164231_add_owner_to_application.rb'
- - 'db/migrate/20160316103650_add_missing_indices.rb'
- - 'db/migrate/20160322193748_add_avatar_remote_url_to_accounts.rb'
- - 'db/migrate/20160325130944_add_admin_to_users.rb'
- - 'db/migrate/20160826155805_add_superapp_to_oauth_applications.rb'
- - 'db/migrate/20160905150353_create_media_attachments.rb'
- - 'db/migrate/20160919221059_add_subscription_expires_at_to_accounts.rb'
- - 'db/migrate/20160920003904_remove_verify_token_from_accounts.rb'
- - 'db/migrate/20160926213048_remove_owner_from_application.rb'
- - 'db/migrate/20161003142332_add_confirmable_to_users.rb'
- - 'db/migrate/20161003145426_create_blocks.rb'
- - 'db/migrate/20161006213403_rails_settings_migration.rb'
- - 'db/migrate/20161009120834_create_domain_blocks.rb'
- - 'db/migrate/20161027172456_add_silenced_to_accounts.rb'
- - 'db/migrate/20161104173623_create_tags.rb'
- - 'db/migrate/20161105130633_create_statuses_tags_join_table.rb'
- - 'db/migrate/20161116162355_add_locale_to_users.rb'
- - 'db/migrate/20161119211120_create_notifications.rb'
- - 'db/migrate/20161122163057_remove_unneeded_indexes.rb'
- - 'db/migrate/20161123093447_add_sensitive_to_statuses.rb'
- - 'db/migrate/20161128103007_create_subscriptions.rb'
- - 'db/migrate/20161130142058_add_last_successful_delivery_at_to_subscriptions.rb'
- - 'db/migrate/20161130185319_add_visibility_to_statuses.rb'
- - 'db/migrate/20161202132159_add_in_reply_to_account_id_to_statuses.rb'
- - 'db/migrate/20161203164520_add_from_account_id_to_notifications.rb'
- - 'db/migrate/20161205214545_add_suspended_to_accounts.rb'
- - 'db/migrate/20161221152630_add_hidden_to_stream_entries.rb'
- - 'db/migrate/20161222201034_add_locked_to_accounts.rb'
- - 'db/migrate/20161222204147_create_follow_requests.rb'
- - 'db/migrate/20170105224407_add_shortcode_to_media_attachments.rb'
- - 'db/migrate/20170109120109_create_web_settings.rb'
- - 'db/migrate/20170112154826_migrate_settings.rb'
- - 'db/migrate/20170114194937_add_application_to_statuses.rb'
- - 'db/migrate/20170114203041_add_website_to_oauth_application.rb'
- - 'db/migrate/20170119214911_create_preview_cards.rb'
- - 'db/migrate/20170123162658_add_severity_to_domain_blocks.rb'
- - 'db/migrate/20170123203248_add_reject_media_to_domain_blocks.rb'
- - 'db/migrate/20170125145934_add_spoiler_text_to_statuses.rb'
- - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
- - 'db/migrate/20170205175257_remove_devices.rb'
- - 'db/migrate/20170209184350_add_reply_to_statuses.rb'
- - 'db/migrate/20170214110202_create_reports.rb'
- - 'db/migrate/20170217012631_add_reblog_of_id_foreign_key_to_statuses.rb'
- - 'db/migrate/20170301222600_create_mutes.rb'
- - 'db/migrate/20170303212857_add_last_emailed_at_to_users.rb'
- - 'db/migrate/20170304202101_add_type_to_media_attachments.rb'
- - 'db/migrate/20170317193015_add_search_index_to_accounts.rb'
- - 'db/migrate/20170318214217_add_header_remote_url_to_accounts.rb'
- - 'db/migrate/20170322021028_add_lowercase_index_to_accounts.rb'
- - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
- - 'db/migrate/20170322162804_add_search_index_to_tags.rb'
- - 'db/migrate/20170330021336_add_counter_caches.rb'
- - 'db/migrate/20170330163835_create_imports.rb'
- - 'db/migrate/20170330164118_add_attachment_data_to_imports.rb'
- - 'db/migrate/20170403172249_add_action_taken_by_account_id_to_reports.rb'
- - 'db/migrate/20170405112956_add_index_on_mentions_status_id.rb'
- - 'db/migrate/20170406215816_add_notifications_and_favourites_indices.rb'
- - 'db/migrate/20170409170753_add_last_webfingered_at_to_accounts.rb'
- - 'db/migrate/20170414080609_add_devise_two_factor_backupable_to_users.rb'
- - 'db/migrate/20170414132105_add_language_to_statuses.rb'
- - 'db/migrate/20170418160728_add_indexes_to_reports_for_accounts.rb'
- - 'db/migrate/20170423005413_add_allowed_languages_to_user.rb'
- - 'db/migrate/20170424003227_create_account_domain_blocks.rb'
- - 'db/migrate/20170424112722_add_status_id_index_to_statuses_tags.rb'
- - 'db/migrate/20170425131920_add_media_attachment_meta.rb'
- - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb'
- - 'db/migrate/20170427011934_re_add_owner_to_application.rb'
- - 'db/migrate/20170506235850_create_conversations.rb'
- - 'db/migrate/20170507000211_add_conversation_id_to_statuses.rb'
- - 'db/migrate/20170507141759_optimize_index_subscriptions.rb'
- - 'db/migrate/20170508230434_create_conversation_mutes.rb'
- - 'db/migrate/20170516072309_add_index_accounts_on_uri.rb'
- - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
- - 'db/migrate/20170601210557_add_index_on_media_attachments_account_id.rb'
- - 'db/migrate/20170604144747_add_foreign_keys_for_accounts.rb'
- - 'db/migrate/20170606113804_change_tag_search_index_to_btree.rb'
- - 'db/migrate/20170609145826_remove_default_language_from_statuses.rb'
- - 'db/migrate/20170610000000_add_statuses_index_on_account_id_id.rb'
- - 'db/migrate/20170623152212_create_session_activations.rb'
- - 'db/migrate/20170624134742_add_description_to_session_activations.rb'
- - 'db/migrate/20170625140443_add_access_token_id_to_session_activations.rb'
- - 'db/migrate/20170711225116_fix_null_booleans.rb'
- - 'db/migrate/20170713112503_make_tag_search_case_insensitive.rb'
- - 'db/migrate/20170713175513_create_web_push_subscriptions.rb'
- - 'db/migrate/20170713190709_add_web_push_subscription_to_session_activations.rb'
- - 'db/migrate/20170714184731_add_domain_to_subscriptions.rb'
- - 'db/migrate/20170716191202_add_hide_notifications_to_mute.rb'
- - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb'
- - 'db/migrate/20170720000000_add_index_favourites_on_account_id_and_id.rb'
- - 'db/migrate/20170823162448_create_status_pins.rb'
- - 'db/migrate/20170824103029_add_timestamps_to_status_pins.rb'
- - 'db/migrate/20170829215220_remove_status_pins_account_index.rb'
- - 'db/migrate/20170901141119_truncate_preview_cards.rb'
- - 'db/migrate/20170901142658_create_join_table_preview_cards_statuses.rb'
- - 'db/migrate/20170905044538_add_index_id_account_id_activity_type_on_notifications.rb'
- - 'db/migrate/20170905165803_add_local_to_statuses.rb'
- - 'db/migrate/20170913000752_create_site_uploads.rb'
- - 'db/migrate/20170917153509_create_custom_emojis.rb'
- - 'db/migrate/20170918125918_ids_to_bigints.rb'
- - 'db/migrate/20170920024819_status_ids_to_timestamp_ids.rb'
- - 'db/migrate/20170920032311_fix_reblogs_in_feeds.rb'
- - 'db/migrate/20170924022025_ids_to_bigints2.rb'
- - 'db/migrate/20170927215609_add_description_to_media_attachments.rb'
- - 'db/migrate/20170928082043_create_email_domain_blocks.rb'
- - 'db/migrate/20171005102658_create_account_moderation_notes.rb'
- - 'db/migrate/20171005171936_add_disabled_to_custom_emojis.rb'
- - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb'
- - 'db/migrate/20171010023049_add_foreign_key_to_account_moderation_notes.rb'
- - 'db/migrate/20171010025614_change_accounts_nonnullable_in_account_moderation_notes.rb'
- - 'db/migrate/20171020084748_add_visible_in_picker_to_custom_emoji.rb'
- - 'db/migrate/20171028221157_add_reblogs_to_follows.rb'
- - 'db/migrate/20171107143332_add_memorial_to_accounts.rb'
- - 'db/migrate/20171107143624_add_disabled_to_users.rb'
- - 'db/migrate/20171109012327_add_moderator_to_accounts.rb'
- - 'db/migrate/20171114080328_add_index_domain_to_email_domain_blocks.rb'
- - 'db/migrate/20171114231651_create_lists.rb'
- - 'db/migrate/20171116161857_create_list_accounts.rb'
- - 'db/migrate/20171118012443_add_moved_to_account_id_to_accounts.rb'
- - 'db/migrate/20171119172437_create_admin_action_logs.rb'
- - 'db/migrate/20171122120436_add_index_account_and_reblog_of_id_to_statuses.rb'
- - 'db/migrate/20171125024930_create_invites.rb'
- - 'db/migrate/20171125031751_add_invite_id_to_users.rb'
- - 'db/migrate/20171125185353_add_index_reblog_of_id_and_account_to_statuses.rb'
- - 'db/migrate/20171125190735_remove_old_reblog_index_on_statuses.rb'
- - 'db/migrate/20171129172043_add_index_on_stream_entries.rb'
- - 'db/migrate/20171130000000_add_embed_url_to_preview_cards.rb'
- - 'db/migrate/20171201000000_change_account_id_nonnullable_in_lists.rb'
- - 'db/migrate/20171212195226_remove_duplicate_indexes_in_lists.rb'
- - 'db/migrate/20171226094803_more_faster_index_on_notifications.rb'
- - 'db/migrate/20180106000232_add_index_on_statuses_for_api_v1_accounts_account_id_statuses.rb'
- - 'db/migrate/20180109143959_add_remember_token_to_users.rb'
- - 'db/migrate/20180204034416_create_identities.rb'
- - 'db/migrate/20180206000000_change_user_id_nonnullable.rb'
- - 'db/migrate/20180211015820_create_backups.rb'
- - 'db/migrate/20180304013859_add_featured_collection_url_to_accounts.rb'
- - 'db/migrate/20180310000000_change_columns_in_notifications_nonnullable.rb'
- - 'db/migrate/20180402031200_add_assigned_account_id_to_reports.rb'
- - 'db/migrate/20180402040909_create_report_notes.rb'
- - 'db/migrate/20180410204633_add_fields_to_accounts.rb'
- - 'db/migrate/20180416210259_add_uri_to_relationships.rb'
- - 'db/migrate/20180506221944_add_actor_type_to_accounts.rb'
- - 'db/migrate/20180510214435_add_access_token_id_to_web_push_subscriptions.rb'
- - 'db/migrate/20180510230049_migrate_web_push_subscriptions.rb'
- - 'db/migrate/20180528141303_fix_accounts_unique_index.rb'
- - 'db/migrate/20180608213548_reject_following_blocked_users.rb'
- - 'db/migrate/20180609104432_migrate_web_push_subscriptions2.rb'
- - 'db/migrate/20180615122121_add_autofollow_to_invites.rb'
- - 'db/migrate/20180616192031_add_chosen_languages_to_users.rb'
- - 'db/migrate/20180617162849_remove_unused_indexes.rb'
- - 'db/migrate/20180628181026_create_custom_filters.rb'
- - 'db/migrate/20180707154237_add_whole_word_to_custom_filter.rb'
- - 'db/migrate/20180711152640_create_relays.rb'
- - 'db/migrate/20180808175627_create_account_pins.rb'
- - 'db/migrate/20180812123222_change_relays_enabled.rb'
- - 'db/migrate/20180812162710_create_status_stats.rb'
- - 'db/migrate/20180812173710_copy_status_stats.rb'
- - 'db/migrate/20180814171349_add_confidential_to_doorkeeper_application.rb'
- - 'db/migrate/20180831171112_create_bookmarks.rb'
- - 'db/migrate/20180929222014_create_account_conversations.rb'
- - 'db/migrate/20181007025445_create_pghero_space_stats.rb'
- - 'db/migrate/20181010141500_add_silent_to_mentions.rb'
- - 'db/migrate/20181017170937_add_reject_reports_to_domain_blocks.rb'
- - 'db/migrate/20181018205649_add_unread_to_account_conversations.rb'
- - 'db/migrate/20181024224956_migrate_account_conversations.rb'
- - 'db/migrate/20181026034033_remove_faux_remote_account_duplicates.rb'
- - 'db/migrate/20181116165755_create_account_stats.rb'
- - 'db/migrate/20181116173541_copy_account_stats.rb'
- - 'db/migrate/20181127130500_identity_id_to_bigint.rb'
- - 'db/migrate/20181127165847_add_show_replies_to_lists.rb'
- - 'db/migrate/20181203003808_create_accounts_tags_join_table.rb'
- - 'db/migrate/20181203021853_add_discoverable_to_accounts.rb'
- - 'db/migrate/20181204193439_add_last_status_at_to_account_stats.rb'
- - 'db/migrate/20181204215309_create_account_tag_stats.rb'
- - 'db/migrate/20181207011115_downcase_custom_emoji_domains.rb'
- - 'db/migrate/20181213184704_create_account_warnings.rb'
- - 'db/migrate/20181213185533_create_account_warning_presets.rb'
- - 'db/migrate/20181219235220_add_created_by_application_id_to_users.rb'
- - 'db/migrate/20181226021420_add_also_known_as_to_accounts.rb'
- - 'db/migrate/20190103124649_create_scheduled_statuses.rb'
- - 'db/migrate/20190103124754_add_scheduled_status_id_to_media_attachments.rb'
- - 'db/migrate/20190117114553_create_tombstones.rb'
- - 'db/migrate/20190201012802_add_overwrite_to_imports.rb'
- - 'db/migrate/20190203180359_create_featured_tags.rb'
- - 'db/migrate/20190225031541_create_polls.rb'
- - 'db/migrate/20190225031625_create_poll_votes.rb'
- - 'db/migrate/20190226003449_add_poll_id_to_statuses.rb'
- - 'db/migrate/20190304152020_add_uri_to_poll_votes.rb'
- - 'db/migrate/20190306145741_add_lock_version_to_polls.rb'
- - 'db/migrate/20190307234537_add_approved_to_users.rb'
- - 'db/migrate/20190314181829_migrate_open_registrations_setting.rb'
- - 'db/migrate/20190316190352_create_account_identity_proofs.rb'
- - 'db/migrate/20190317135723_add_uri_to_reports.rb'
- - 'db/migrate/20190403141604_add_comment_to_invites.rb'
- - 'db/migrate/20190409054914_create_user_invite_requests.rb'
- - 'db/migrate/20190420025523_add_blurhash_to_media_attachments.rb'
- - 'db/migrate/20190509164208_add_by_moderator_to_tombstone.rb'
- - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- - 'db/migrate/20190529143559_preserve_old_layout_for_existing_users.rb'
- - 'db/migrate/20190627222225_create_custom_emoji_categories.rb'
- - 'db/migrate/20190627222826_add_category_id_to_custom_emojis.rb'
- - 'db/migrate/20190701022101_add_trust_level_to_accounts.rb'
- - 'db/migrate/20190705002136_create_domain_allows.rb'
- - 'db/migrate/20190715164535_add_instance_actor.rb'
- - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
- - 'db/migrate/20190729185330_add_score_to_tags.rb'
- - 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
- - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb'
- - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb'
- - 'db/migrate/20190819134503_add_deleted_at_to_statuses.rb'
- - 'db/migrate/20190820003045_update_statuses_index.rb'
- - 'db/migrate/20190823221802_add_local_index_to_statuses.rb'
- - 'db/migrate/20190901035623_add_max_score_to_tags.rb'
- - 'db/migrate/20190904222339_create_markers.rb'
- - 'db/migrate/20190914202517_create_account_migrations.rb'
- - 'db/migrate/20190915194355_create_account_aliases.rb'
- - 'db/migrate/20190927232842_add_voters_count_to_polls.rb'
- - 'db/migrate/20191001213028_add_lock_version_to_account_stats.rb'
- - 'db/migrate/20191007013357_update_pt_locales.rb'
- - 'db/migrate/20191031163205_change_list_account_follow_nullable.rb'
- - 'db/migrate/20191212003415_increase_backup_size.rb'
- - 'db/migrate/20191212163405_add_hide_collections_to_accounts.rb'
- - 'db/migrate/20191218153258_create_announcements.rb'
- - 'db/migrate/20200113125135_create_announcement_mutes.rb'
- - 'db/migrate/20200114113335_create_announcement_reactions.rb'
- - 'db/migrate/20200119112504_add_public_index_to_statuses.rb'
- - 'db/migrate/20200126203551_add_published_at_to_announcements.rb'
- - 'db/migrate/20200306035625_add_processing_to_media_attachments.rb'
- - 'db/migrate/20200309150742_add_forwarded_to_reports.rb'
- - 'db/migrate/20200312144258_add_title_to_account_warning_presets.rb'
- - 'db/migrate/20200312162302_add_status_ids_to_announcements.rb'
- - 'db/migrate/20200312185443_add_parent_id_to_email_domain_blocks.rb'
- - 'db/migrate/20200317021758_add_expires_at_to_mutes.rb'
- - 'db/migrate/20200407201300_create_unavailable_domains.rb'
- - 'db/migrate/20200407202420_migrate_unavailable_inboxes.rb'
- - 'db/migrate/20200417125749_add_storage_schema_version.rb'
- - 'db/migrate/20200508212852_reset_unique_jobs_locks.rb'
- - 'db/migrate/20200510110808_reset_web_app_secret.rb'
- - 'db/migrate/20200510181721_remove_duplicated_indexes_pghero.rb'
- - 'db/migrate/20200516180352_create_devices.rb'
- - 'db/migrate/20200516183822_create_one_time_keys.rb'
- - 'db/migrate/20200518083523_create_encrypted_messages.rb'
- - 'db/migrate/20200521180606_encrypted_message_ids_to_timestamp_ids.rb'
- - 'db/migrate/20200529214050_add_devices_url_to_accounts.rb'
- - 'db/migrate/20200601222558_create_system_keys.rb'
- - 'db/migrate/20200605155027_add_blurhash_to_preview_cards.rb'
- - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb'
- - 'db/migrate/20200614002136_add_sensitized_to_accounts.rb'
- - 'db/migrate/20200620164023_add_fixed_lowercase_index_to_accounts.rb'
- - 'db/migrate/20200622213645_media_attachment_ids_to_timestamp_ids.rb'
- - 'db/migrate/20200627125810_add_thumbnail_columns_to_media_attachments.rb'
- - 'db/migrate/20200628133322_create_account_notes.rb'
- - 'db/migrate/20200630190240_create_webauthn_credentials.rb'
- - 'db/migrate/20200630190544_add_webauthn_id_to_users.rb'
- - 'db/migrate/20200908193330_create_account_deletion_requests.rb'
- - 'db/migrate/20200917192924_add_notify_to_follows.rb'
- - 'db/migrate/20200917193034_add_type_to_notifications.rb'
- - 'db/migrate/20200917222316_add_index_notifications_on_type.rb'
- - 'db/migrate/20201008202037_create_ip_blocks.rb'
- - 'db/migrate/20201008220312_add_sign_up_ip_to_users.rb'
- - 'db/migrate/20201017233919_add_suspension_origin_to_accounts.rb'
- - 'db/migrate/20201206004238_create_instances.rb'
- - 'db/migrate/20201218054746_add_obfuscate_to_domain_blocks.rb'
- - 'db/migrate/20210221045109_create_rules.rb'
- - 'db/migrate/20210306164523_account_ids_to_timestamp_ids.rb'
- - 'db/migrate/20210322164601_create_account_summaries.rb'
- - 'db/migrate/20210323114347_create_follow_recommendations.rb'
- - 'db/migrate/20210324171613_create_follow_recommendation_suppressions.rb'
- - 'db/migrate/20210416200740_create_canonical_email_blocks.rb'
- - 'db/migrate/20210421121431_add_case_insensitive_btree_index_to_tags.rb'
- - 'db/migrate/20210425135952_add_index_on_media_attachments_account_id_status_id.rb'
- - 'db/migrate/20210505174616_update_follow_recommendations_to_version_2.rb'
- - 'db/migrate/20210609202149_create_login_activities.rb'
- - 'db/migrate/20210616214526_create_user_ips.rb'
- - 'db/migrate/20210621221010_add_skip_sign_in_token_to_users.rb'
- - 'db/migrate/20210630000137_fix_canonical_email_blocks_foreign_key.rb'
- - 'db/migrate/20210722120340_create_account_statuses_cleanup_policies.rb'
- - 'db/migrate/20210904215403_add_edited_at_to_statuses.rb'
- - 'db/migrate/20210908220918_create_status_edits.rb'
- - 'db/migrate/20211031031021_create_preview_card_providers.rb'
- - 'db/migrate/20211112011713_add_language_to_preview_cards.rb'
- - 'db/migrate/20211115032527_add_trendable_to_preview_cards.rb'
- - 'db/migrate/20211123212714_add_link_type_to_preview_cards.rb'
- - 'db/migrate/20211213040746_update_account_summaries_to_version_2.rb'
- - 'db/migrate/20211231080958_add_category_to_reports.rb'
- - 'db/migrate/20220105163928_remove_mentions_status_id_index.rb'
- - 'db/migrate/20220115125126_add_report_id_to_account_warnings.rb'
- - 'db/migrate/20220115125341_fix_account_warning_actions.rb'
- - 'db/migrate/20220116202951_add_deleted_at_index_on_statuses.rb'
- - 'db/migrate/20220124141035_create_appeals.rb'
- - 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
- - 'db/migrate/20220202200926_add_trendable_to_statuses.rb'
- - 'db/migrate/20220210153119_add_overruled_at_to_account_warnings.rb'
- - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb'
- - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb'
- - 'db/migrate/20220302232632_add_ordered_media_attachment_ids_to_statuses.rb'
- - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
- - 'db/migrate/20220304195405_migrate_hide_network_preference.rb'
- - 'db/migrate/20220307094650_fix_featured_tags_constraints.rb'
- - 'db/migrate/20220309213005_fix_reblog_deleted_at.rb'
- - 'db/migrate/20220316233212_update_kurdish_locales.rb'
- - 'db/migrate/20220428112511_add_index_statuses_on_account_id.rb'
- - 'db/migrate/20220428112727_add_index_statuses_pins_on_status_id.rb'
- - 'db/migrate/20220428114454_add_index_reports_on_assigned_account_id.rb'
- - 'db/migrate/20220428114902_add_index_reports_on_action_taken_by_account_id.rb'
- - 'db/migrate/20220606044941_create_webhooks.rb'
- - 'db/migrate/20220611210335_create_user_roles.rb'
- - 'db/migrate/20220611212541_add_role_id_to_users.rb'
- - 'db/migrate/20220710102457_add_display_name_to_tags.rb'
- - 'db/migrate/20220714171049_create_tag_follows.rb'
- - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb'
- - 'db/migrate/20220824233535_create_status_trends.rb'
- - 'db/migrate/20220827195229_change_canonical_email_blocks_nullable.rb'
- - 'db/migrate/20220829192633_add_languages_to_follows.rb'
- - 'db/migrate/20220829192658_add_languages_to_follow_requests.rb'
- - 'db/migrate/20221006061337_create_preview_card_trends.rb'
- - 'db/migrate/20221012181003_add_blurhash_to_site_uploads.rb'
- - 'db/migrate/20221021055441_add_index_featured_tags_on_account_id_and_tag_id.rb'
- - 'db/migrate/20221025171544_add_index_ip_blocks_on_ip.rb'
- - 'db/migrate/20221104133904_add_name_to_featured_tags.rb'
- - 'db/post_migrate/20190519130537_remove_boosts_widening_audience.rb'
- - 'db/post_migrate/20210308133107_remove_subscription_expires_at_from_accounts.rb'
- - 'db/post_migrate/20220118183123_remove_rememberable_from_users.rb'
- - 'db/seeds/01_web_app.rb'
- - 'db/seeds/02_instance_actor.rb'
- - 'db/seeds/03_roles.rb'
- - 'db/seeds/04_admin.rb'
- - 'lib/rails/engine_extensions.rb'
- - 'lib/tasks/branding.rake'
- - 'spec/fabricators_spec.rb'
-
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
@@ -1343,13 +871,6 @@ Style/SafeNavigation:
- 'app/models/concerns/account_finder_concern.rb'
- 'app/models/status.rb'
-# This cop supports safe autocorrection (--autocorrect).
-# Configuration parameters: AllowAsExpressionSeparator.
-Style/Semicolon:
- Exclude:
- - 'spec/services/activitypub/process_status_update_service_spec.rb'
- - 'spec/validators/blacklisted_email_validator_spec.rb'
-
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: only_raise, only_fail, semantic
@@ -1363,21 +884,6 @@ Style/SingleArgumentDig:
Exclude:
- 'lib/webpacker/manifest_extensions.rb'
-# This cop supports unsafe autocorrection (--autocorrect-all).
-Style/SlicingWithRange:
- Exclude:
- - 'app/lib/emoji_formatter.rb'
- - 'app/lib/text_formatter.rb'
- - 'app/models/account_alias.rb'
- - 'app/models/domain_block.rb'
- - 'app/models/email_domain_block.rb'
- - 'app/models/preview_card_provider.rb'
- - 'app/validators/status_length_validator.rb'
- - 'db/migrate/20190726175042_add_case_insensitive_index_to_tags.rb'
- - 'lib/active_record/batches.rb'
- - 'lib/mastodon/premailer_webpack_strategy.rb'
- - 'lib/tasks/repo.rake'
-
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_parentheses, require_no_parentheses
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6f1b7bcb3..5383d426b6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -143,7 +143,7 @@ All notable changes to this project will be documented in this file.
- Add instance activity API endpoint toggle back to the admin interface ([dariusk](https://github.com/mastodon/mastodon/pull/22833))
- Add setting for status page URL ([Gargron](https://github.com/mastodon/mastodon/pull/23390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23499))
- REST API changes:
- - Add `configuration.urls.status` attribute to the object returned by `GET /api/v1/instance`
+ - Add `configuration.urls.status` attribute to the object returned by `GET /api/v2/instance`
- Add `account.approved` webhook ([Saiv46](https://github.com/mastodon/mastodon/pull/22938))
- Add 12 hours option to polls ([Pleclown](https://github.com/mastodon/mastodon/pull/21131))
- Add dropdown menu item to open admin interface for remote domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21895))
diff --git a/Gemfile b/Gemfile
index 3feb3f9548..0746970664 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,14 +4,13 @@ source 'https://rubygems.org'
ruby '>= 3.0.0'
gem 'puma', '~> 6.3'
-gem 'rails', '~> 6.1.7'
+gem 'rails', '~> 7.0'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5'
-gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
@@ -67,7 +66,7 @@ gem 'pundit', '~> 2.3'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 2.0', require: 'rack/cors'
-gem 'rails-i18n', '~> 6.0'
+gem 'rails-i18n', '~> 7.0'
gem 'rails-settings-cached', '~> 0.6', git: 'https://github.com/mastodon/rails-settings-cached.git', branch: 'v0.6.6-aliases-true'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
@@ -159,7 +158,7 @@ group :development do
gem 'letter_opener_web', '~> 2.0'
# Security analysis CLI tools
- gem 'brakeman', '~> 5.4', require: false
+ gem 'brakeman', '~> 6.0', require: false
gem 'bundler-audit', '~> 0.9', require: false
# Linter CLI for HAML files
diff --git a/Gemfile.lock b/Gemfile.lock
index 985e36c20c..87d8ef7d25 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -18,40 +18,47 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.1.7.4)
- actionpack (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ actioncable (7.0.6)
+ actionpack (= 7.0.6)
+ activesupport (= 7.0.6)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.7.4)
- actionpack (= 6.1.7.4)
- activejob (= 6.1.7.4)
- activerecord (= 6.1.7.4)
- activestorage (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ actionmailbox (7.0.6)
+ actionpack (= 7.0.6)
+ activejob (= 7.0.6)
+ activerecord (= 7.0.6)
+ activestorage (= 7.0.6)
+ activesupport (= 7.0.6)
mail (>= 2.7.1)
- actionmailer (6.1.7.4)
- actionpack (= 6.1.7.4)
- actionview (= 6.1.7.4)
- activejob (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ net-imap
+ net-pop
+ net-smtp
+ actionmailer (7.0.6)
+ actionpack (= 7.0.6)
+ actionview (= 7.0.6)
+ activejob (= 7.0.6)
+ activesupport (= 7.0.6)
mail (~> 2.5, >= 2.5.4)
+ net-imap
+ net-pop
+ net-smtp
rails-dom-testing (~> 2.0)
- actionpack (6.1.7.4)
- actionview (= 6.1.7.4)
- activesupport (= 6.1.7.4)
- rack (~> 2.0, >= 2.0.9)
+ actionpack (7.0.6)
+ actionview (= 7.0.6)
+ activesupport (= 7.0.6)
+ rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.7.4)
- actionpack (= 6.1.7.4)
- activerecord (= 6.1.7.4)
- activestorage (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ actiontext (7.0.6)
+ actionpack (= 7.0.6)
+ activerecord (= 7.0.6)
+ activestorage (= 7.0.6)
+ activesupport (= 7.0.6)
+ globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (6.1.7.4)
- activesupport (= 6.1.7.4)
+ actionview (7.0.6)
+ activesupport (= 7.0.6)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -61,27 +68,26 @@ GEM
activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
- activejob (6.1.7.4)
- activesupport (= 6.1.7.4)
+ activejob (7.0.6)
+ activesupport (= 7.0.6)
globalid (>= 0.3.6)
- activemodel (6.1.7.4)
- activesupport (= 6.1.7.4)
- activerecord (6.1.7.4)
- activemodel (= 6.1.7.4)
- activesupport (= 6.1.7.4)
- activestorage (6.1.7.4)
- actionpack (= 6.1.7.4)
- activejob (= 6.1.7.4)
- activerecord (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ activemodel (7.0.6)
+ activesupport (= 7.0.6)
+ activerecord (7.0.6)
+ activemodel (= 7.0.6)
+ activesupport (= 7.0.6)
+ activestorage (7.0.6)
+ actionpack (= 7.0.6)
+ activejob (= 7.0.6)
+ activerecord (= 7.0.6)
+ activesupport (= 7.0.6)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.7.4)
+ activesupport (7.0.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
- zeitwerk (~> 2.3)
addressable (2.8.4)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
@@ -130,7 +136,7 @@ GEM
blurhash (0.1.7)
bootsnap (1.16.0)
msgpack (~> 1.2)
- brakeman (5.4.1)
+ brakeman (6.0.0)
browser (5.3.1)
brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5)
@@ -146,7 +152,7 @@ GEM
sshkit (>= 1.9.0)
capistrano-bundler (2.1.0)
capistrano (~> 3.1)
- capistrano-rails (1.6.2)
+ capistrano-rails (1.6.3)
capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3)
capistrano-rbenv (2.2.0)
@@ -167,7 +173,7 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
- chewy (7.3.2)
+ chewy (7.3.3)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
@@ -291,11 +297,11 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
- haml_lint (0.45.0)
+ haml_lint (0.48.0)
haml (>= 4.0, < 6.2)
parallel (~> 1.10)
rainbow
- rubocop (>= 0.50.0)
+ rubocop (>= 1.0)
sysexits (~> 1.1)
hashdiff (1.0.1)
hashie (5.0.0)
@@ -373,6 +379,7 @@ GEM
marcel (~> 1.0.1)
mime-types
terrapin (~> 0.6.0)
+ language_server-protocol (3.17.0.3)
launchy (2.5.2)
addressable (~> 2.8)
letter_opener (1.8.1)
@@ -399,8 +406,6 @@ GEM
net-imap
net-pop
net-smtp
- makara (0.5.1)
- activerecord (>= 5.2.0)
marcel (1.0.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
@@ -510,21 +515,20 @@ GEM
rack
rack-test (2.1.0)
rack (>= 1.3)
- rails (6.1.7.4)
- actioncable (= 6.1.7.4)
- actionmailbox (= 6.1.7.4)
- actionmailer (= 6.1.7.4)
- actionpack (= 6.1.7.4)
- actiontext (= 6.1.7.4)
- actionview (= 6.1.7.4)
- activejob (= 6.1.7.4)
- activemodel (= 6.1.7.4)
- activerecord (= 6.1.7.4)
- activestorage (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ rails (7.0.6)
+ actioncable (= 7.0.6)
+ actionmailbox (= 7.0.6)
+ actionmailer (= 7.0.6)
+ actionpack (= 7.0.6)
+ actiontext (= 7.0.6)
+ actionview (= 7.0.6)
+ activejob (= 7.0.6)
+ activemodel (= 7.0.6)
+ activerecord (= 7.0.6)
+ activestorage (= 7.0.6)
+ activesupport (= 7.0.6)
bundler (>= 1.15.0)
- railties (= 6.1.7.4)
- sprockets-rails (>= 2.0.0)
+ railties (= 7.0.6)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -535,15 +539,16 @@ GEM
rails-html-sanitizer (1.6.0)
loofah (~> 2.21)
nokogiri (~> 1.14)
- rails-i18n (6.0.0)
+ rails-i18n (7.0.7)
i18n (>= 0.7, < 2)
- railties (>= 6.0.0, < 7)
- railties (6.1.7.4)
- actionpack (= 6.1.7.4)
- activesupport (= 6.1.7.4)
+ railties (>= 6.0.0, < 8)
+ railties (7.0.6)
+ actionpack (= 7.0.6)
+ activesupport (= 7.0.6)
method_source
rake (>= 12.2)
thor (~> 1.0)
+ zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.0.6)
rdf (3.2.11)
@@ -591,8 +596,9 @@ GEM
sidekiq (>= 2.4.0)
rspec-support (3.12.0)
rspec_chunked (0.6)
- rubocop (1.52.1)
+ rubocop (1.54.1)
json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
@@ -610,7 +616,7 @@ GEM
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
- rubocop-rails (2.19.1)
+ rubocop-rails (2.20.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
@@ -670,7 +676,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
- sshkit (1.21.4)
+ sshkit (1.21.5)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.25)
@@ -690,7 +696,7 @@ GEM
climate_control (>= 0.0.3, < 1.0)
thor (1.2.2)
tilt (2.2.0)
- timeout (0.3.2)
+ timeout (0.4.0)
tpm-key_attestation (0.12.0)
bindata (~> 2.4)
openssl (> 2.0)
@@ -767,7 +773,7 @@ DEPENDENCIES
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.16.0)
- brakeman (~> 5.4)
+ brakeman (~> 6.0)
browser
bundler-audit (~> 0.9)
capistrano (~> 3.17)
@@ -815,7 +821,6 @@ DEPENDENCIES
letter_opener_web (~> 2.0)
link_header (~> 0.0)
lograge (~> 0.12)
- makara (~> 0.5)
mario-redis-lock (~> 1.2)
memory_profiler
mime-types (~> 3.4.1)
@@ -842,9 +847,9 @@ DEPENDENCIES
rack-attack (~> 6.6)
rack-cors (~> 2.0)
rack-test (~> 2.1)
- rails (~> 6.1.7)
+ rails (~> 7.0)
rails-controller-testing (~> 1.0)
- rails-i18n (~> 6.0)
+ rails-i18n (~> 7.0)
rails-settings-cached (~> 0.6)!
rdf-normalize (~> 0.5)
redcarpet (~> 3.6)
diff --git a/Rakefile b/Rakefile
index ba6b733dd2..e51cf0e17e 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require File.expand_path('../config/application', __FILE__)
+require File.expand_path('config/application', __dir__)
Rails.application.load_tasks
diff --git a/app/controllers/api/v1/bookmarks_controller.rb b/app/controllers/api/v1/bookmarks_controller.rb
index 0cc2318409..498eb16f44 100644
--- a/app/controllers/api/v1/bookmarks_controller.rb
+++ b/app/controllers/api/v1/bookmarks_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::BookmarksController < Api::BaseController
end
def results
- @_results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
+ @results ||= account_bookmarks.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb
index bd7f3d775e..faf1bda96a 100644
--- a/app/controllers/api/v1/favourites_controller.rb
+++ b/app/controllers/api/v1/favourites_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::FavouritesController < Api::BaseController
end
def results
- @_results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
+ @results ||= account_favourites.joins(:status).eager_load(:status).to_a_paginated_by_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
diff --git a/app/controllers/api/v1/markers_controller.rb b/app/controllers/api/v1/markers_controller.rb
index 867e6facf4..f8dfba8a94 100644
--- a/app/controllers/api/v1/markers_controller.rb
+++ b/app/controllers/api/v1/markers_controller.rb
@@ -7,7 +7,10 @@ class Api::V1::MarkersController < Api::BaseController
before_action :require_user!
def index
- @markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
+ with_read_replica do
+ @markers = current_user.markers.where(timeline: Array(params[:timeline])).index_by(&:timeline)
+ end
+
render json: serialize_map(@markers)
end
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index 8414f6b25c..406ab97538 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -9,8 +9,12 @@ class Api::V1::NotificationsController < Api::BaseController
DEFAULT_NOTIFICATIONS_LIMIT = 40
def index
- @notifications = load_notifications
- render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
+ with_read_replica do
+ @notifications = load_notifications
+ @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
+ end
+
+ render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
end
def show
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 8ff6c8fe5c..300c9faa3f 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -23,6 +23,6 @@ class Api::V1::ReportsController < Api::BaseController
end
def report_params
- params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
+ params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: [])
end
end
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index ae6dbcb8b3..83b8cb4c66 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
- @statuses = load_statuses
+ with_read_replica do
+ @statuses = load_statuses
+ @relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ end
render json: @statuses,
each_serializer: REST::StatusSerializer,
- relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
+ relationships: @relationships,
status: account_home_feed.regenerating? ? 206 : 200
end
diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb
index 58f6345e68..63c3f2d90a 100644
--- a/app/controllers/api/web/embeds_controller.rb
+++ b/app/controllers/api/web/embeds_controller.rb
@@ -1,25 +1,36 @@
# frozen_string_literal: true
class Api::Web::EmbedsController < Api::Web::BaseController
- before_action :require_user!
+ include Authorization
- def create
- status = StatusFinder.new(params[:url]).status
+ before_action :set_status
- return not_found if status.hidden?
+ def show
+ return not_found if @status.hidden?
- render json: status, serializer: OEmbedSerializer, width: 400
- rescue ActiveRecord::RecordNotFound
- oembed = FetchOEmbedService.new.call(params[:url])
+ if @status.local?
+ render json: @status, serializer: OEmbedSerializer, width: 400
+ else
+ return not_found unless user_signed_in?
- return not_found if oembed.nil?
+ url = ActivityPub::TagManager.instance.url_for(@status)
+ oembed = FetchOEmbedService.new.call(url)
+ return not_found if oembed.nil?
- begin
- oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
- rescue ArgumentError
- return not_found
+ begin
+ oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
+ rescue ArgumentError
+ return not_found
+ end
+
+ render json: oembed
end
+ end
- render json: oembed
+ def set_status
+ @status = Status.find(params[:id])
+ authorize @status, :show?
+ rescue Mastodon::NotPermittedError
+ not_found
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index f966b18ab5..66886b4519 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
include SessionTrackingConcern
include CacheConcern
include DomainControlHelper
+ include DatabaseHelper
helper_method :current_account
helper_method :current_session
diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb
index 4f59fd501b..1380e6f283 100644
--- a/app/controllers/auth/sessions_controller.rb
+++ b/app/controllers/auth/sessions_controller.rb
@@ -124,7 +124,7 @@ class Auth::SessionsController < Devise::SessionsController
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
end
- def set_attempt_session(user)
+ def register_attempt_in_session(user)
session[:attempt_user_id] = user.id
session[:attempt_user_updated_at] = user.updated_at.to_s
end
diff --git a/app/controllers/concerns/rate_limit_headers.rb b/app/controllers/concerns/rate_limit_headers.rb
index 30702f00e7..5b83d8575b 100644
--- a/app/controllers/concerns/rate_limit_headers.rb
+++ b/app/controllers/concerns/rate_limit_headers.rb
@@ -61,7 +61,7 @@ module RateLimitHeaders
end
def request_time
- @_request_time ||= Time.now.utc
+ @request_time ||= Time.now.utc
end
def reset_period_offset
diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb
index 94f3ce00f1..9eb45b90d6 100644
--- a/app/controllers/concerns/two_factor_authentication_concern.rb
+++ b/app/controllers/concerns/two_factor_authentication_concern.rb
@@ -75,7 +75,7 @@ module TwoFactorAuthenticationConcern
end
def prompt_for_two_factor(user)
- set_attempt_session(user)
+ register_attempt_in_session(user)
@body_classes = 'lighter'
@webauthn_enabled = user.webauthn_enabled?
diff --git a/app/helpers/database_helper.rb b/app/helpers/database_helper.rb
new file mode 100644
index 0000000000..965eeaf41d
--- /dev/null
+++ b/app/helpers/database_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module DatabaseHelper
+ def with_read_replica(&block)
+ ApplicationRecord.connected_to(role: :read, prevent_writes: true, &block)
+ end
+
+ def with_primary(&block)
+ ApplicationRecord.connected_to(role: :primary, &block)
+ end
+end
diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb
index ffcf375ea7..6fce7eb1f5 100644
--- a/app/helpers/domain_control_helper.rb
+++ b/app/helpers/domain_control_helper.rb
@@ -2,7 +2,7 @@
module DomainControlHelper
def domain_not_allowed?(uri_or_domain)
- return if uri_or_domain.blank?
+ return false if uri_or_domain.blank?
domain = if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).host
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index 0220b0af58..051a9675b3 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
-export function dismissAlert(alert) {
- return {
- type: ALERT_DISMISS,
- alert,
- };
-}
+export const dismissAlert = alert => ({
+ type: ALERT_DISMISS,
+ alert,
+});
-export function clearAlert() {
- return {
- type: ALERT_CLEAR,
- };
-}
+export const clearAlert = () => ({
+ type: ALERT_CLEAR,
+});
-export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
- return {
- type: ALERT_SHOW,
- title,
- message,
- message_values,
- };
-}
+export const showAlert = alert => ({
+ type: ALERT_SHOW,
+ alert,
+});
-export function showAlertForError(error, skipNotFound = false) {
+export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) {
const { data, status, statusText, headers } = error.response;
+ // Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
- // Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}
+ // Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) {
- const reset_date = new Date(headers['x-ratelimit-reset']);
- return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
+ return showAlert({
+ title: messages.rateLimitedTitle,
+ message: messages.rateLimitedMessage,
+ values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
+ });
}
- let message = statusText;
- let title = `${status}`;
-
- if (data.error) {
- message = data.error;
- }
-
- return showAlert(title, message);
- } else {
- console.error(error);
- return showAlert();
+ return showAlert({
+ title: `${status}`,
+ message: data.error || statusText,
+ });
}
+
+ console.error(error);
+
+ return showAlert({
+ title: messages.unexpectedTitle,
+ message: messages.unexpectedMessage,
+ });
}
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 0dbddba063..dadcdee5dd 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -86,6 +86,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
+ open: { id: 'compose.published.open', defaultMessage: 'Open' },
+ published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ -246,6 +248,13 @@ export function submitCompose(routerHistory) {
insertIfOnline('public');
insertIfOnline(`account:${response.data.account.id}`);
}
+
+ dispatch(showAlert({
+ message: messages.published,
+ action: messages.open,
+ dismissAfter: 10000,
+ onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
+ }));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@@ -275,13 +284,14 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
- const media = getState().getIn(['compose', 'media_attachments']);
- const pending = getState().getIn(['compose', 'pending_media_attachments']);
+ const media = getState().getIn(['compose', 'media_attachments']);
+ const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
+
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) {
- dispatch(showAlert(undefined, messages.uploadErrorLimit));
+ dispatch(showAlert({ message: messages.uploadErrorLimit }));
return;
}
diff --git a/app/javascript/mastodon/blurhash.ts b/app/javascript/mastodon/blurhash.ts
index dadf2b7f2c..cafe7b12dc 100644
--- a/app/javascript/mastodon/blurhash.ts
+++ b/app/javascript/mastodon/blurhash.ts
@@ -86,10 +86,9 @@ const DIGIT_CHARACTERS = [
export const decode83 = (str: string) => {
let value = 0;
- let c, digit;
+ let digit;
- for (let i = 0; i < str.length; i++) {
- c = str[i];
+ for (const c of str) {
digit = DIGIT_CHARACTERS.indexOf(c);
value = value * 83 + digit;
}
diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx
index 600168f16a..220b63c566 100644
--- a/app/javascript/mastodon/components/account.jsx
+++ b/app/javascript/mastodon/components/account.jsx
@@ -8,15 +8,15 @@ import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
-import { counterRenderer } from 'mastodon/components/common_counter';
import { EmptyAccount } from 'mastodon/components/empty_account';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
import { me } from '../initial_state';
import { Avatar } from './avatar';
import Button from './button';
+import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import { IconButton } from './icon_button';
import { RelativeTimestamp } from './relative_timestamp';
@@ -162,7 +162,7 @@ class Account extends ImmutablePureComponent {
{!minimal && (
- {verification} {muteTimeRemaining}
+ {verification} {muteTimeRemaining}
)}
diff --git a/app/javascript/mastodon/components/animated_number.tsx b/app/javascript/mastodon/components/animated_number.tsx
index ad985a29ea..05a7e01898 100644
--- a/app/javascript/mastodon/components/animated_number.tsx
+++ b/app/javascript/mastodon/components/animated_number.tsx
@@ -4,7 +4,7 @@ import { TransitionMotion, spring } from 'react-motion';
import { reduceMotion } from '../initial_state';
-import ShortNumber from './short_number';
+import { ShortNumber } from './short_number';
const obfuscatedCount = (count: number) => {
if (count < 0) {
@@ -32,7 +32,7 @@ export const AnimatedNumber: React.FC = ({ value, obfuscate }) => {
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
const willLeave = useCallback(
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
- [direction]
+ [direction],
);
if (reduceMotion) {
diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.tsx b/app/javascript/mastodon/components/autosuggest_hashtag.tsx
index c6798054db..e83d493c2c 100644
--- a/app/javascript/mastodon/components/autosuggest_hashtag.tsx
+++ b/app/javascript/mastodon/components/autosuggest_hashtag.tsx
@@ -1,16 +1,16 @@
import { FormattedMessage } from 'react-intl';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
interface Props {
tag: {
name: string;
url?: string;
- history?: Array<{
+ history?: {
uses: number;
accounts: string;
day: string;
- }>;
+ }[];
following?: boolean;
type: 'hashtag';
};
diff --git a/app/javascript/mastodon/components/avatar.tsx b/app/javascript/mastodon/components/avatar.tsx
index 8e5e165fe7..5f9bb390e8 100644
--- a/app/javascript/mastodon/components/avatar.tsx
+++ b/app/javascript/mastodon/components/avatar.tsx
@@ -5,7 +5,7 @@ import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
interface Props {
- account: Account;
+ account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size: number;
style?: React.CSSProperties;
inline?: boolean;
diff --git a/app/javascript/mastodon/components/avatar_overlay.tsx b/app/javascript/mastodon/components/avatar_overlay.tsx
index 602f9b4fa6..61de9d0beb 100644
--- a/app/javascript/mastodon/components/avatar_overlay.tsx
+++ b/app/javascript/mastodon/components/avatar_overlay.tsx
@@ -3,8 +3,8 @@ import type { Account } from '../../types/resources';
import { autoPlayGif } from '../initial_state';
interface Props {
- account: Account;
- friend: Account;
+ account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
+ friend: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
size?: number;
baseSize?: number;
overlaySize?: number;
diff --git a/app/javascript/mastodon/components/common_counter.jsx b/app/javascript/mastodon/components/common_counter.jsx
deleted file mode 100644
index 23e1f22638..0000000000
--- a/app/javascript/mastodon/components/common_counter.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-// @ts-check
-import { FormattedMessage } from 'react-intl';
-
-/**
- * Returns custom renderer for one of the common counter types
- * @param {"statuses" | "following" | "followers"} counterType
- * Type of the counter
- * @param {boolean} isBold Whether display number must be displayed in bold
- * @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
- * Renderer function
- * @throws If counterType is not covered by this function
- */
-export function counterRenderer(counterType, isBold = true) {
- /**
- * @type {(displayNumber: JSX.Element) => JSX.Element}
- */
- const renderCounter = isBold
- ? (displayNumber) => {displayNumber}
- : (displayNumber) => displayNumber;
-
- switch (counterType) {
- case 'statuses': {
- return (displayNumber, pluralReady) => (
-
- );
- }
- case 'following': {
- return (displayNumber, pluralReady) => (
-
- );
- }
- case 'followers': {
- return (displayNumber, pluralReady) => (
-
- );
- }
- default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
- }
-}
diff --git a/app/javascript/mastodon/components/counters.tsx b/app/javascript/mastodon/components/counters.tsx
new file mode 100644
index 0000000000..35b0ad8d60
--- /dev/null
+++ b/app/javascript/mastodon/components/counters.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+export const StatusesCounter = (
+ displayNumber: React.ReactNode,
+ pluralReady: number,
+) => (
+ {displayNumber},
+ }}
+ />
+);
+
+export const FollowingCounter = (
+ displayNumber: React.ReactNode,
+ pluralReady: number,
+) => (
+ {displayNumber},
+ }}
+ />
+);
+
+export const FollowersCounter = (
+ displayNumber: React.ReactNode,
+ pluralReady: number,
+) => (
+ {displayNumber},
+ }}
+ />
+);
diff --git a/app/javascript/mastodon/components/dismissable_banner.jsx b/app/javascript/mastodon/components/dismissable_banner.jsx
deleted file mode 100644
index 5aecc88b16..0000000000
--- a/app/javascript/mastodon/components/dismissable_banner.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { injectIntl, defineMessages } from 'react-intl';
-
-import { bannerSettings } from 'mastodon/settings';
-
-import { IconButton } from './icon_button';
-
-const messages = defineMessages({
- dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
-});
-
-class DismissableBanner extends PureComponent {
-
- static propTypes = {
- id: PropTypes.string.isRequired,
- children: PropTypes.node,
- intl: PropTypes.object.isRequired,
- };
-
- state = {
- visible: !bannerSettings.get(this.props.id),
- };
-
- handleDismiss = () => {
- const { id } = this.props;
- this.setState({ visible: false }, () => bannerSettings.set(id, true));
- };
-
- render () {
- const { visible } = this.state;
-
- if (!visible) {
- return null;
- }
-
- const { children, intl } = this.props;
-
- return (
-
-
- {children}
-
-
-
-
-
-
- );
- }
-
-}
-
-export default injectIntl(DismissableBanner);
diff --git a/app/javascript/mastodon/components/dismissable_banner.tsx b/app/javascript/mastodon/components/dismissable_banner.tsx
new file mode 100644
index 0000000000..d5cdb07503
--- /dev/null
+++ b/app/javascript/mastodon/components/dismissable_banner.tsx
@@ -0,0 +1,47 @@
+import type { PropsWithChildren } from 'react';
+import { useCallback, useState } from 'react';
+
+import { defineMessages, useIntl } from 'react-intl';
+
+import { bannerSettings } from 'mastodon/settings';
+
+import { IconButton } from './icon_button';
+
+const messages = defineMessages({
+ dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
+});
+
+interface Props {
+ id: string;
+}
+
+export const DismissableBanner: React.FC> = ({
+ id,
+ children,
+}) => {
+ const [visible, setVisible] = useState(!bannerSettings.get(id));
+ const intl = useIntl();
+
+ const handleDismiss = useCallback(() => {
+ setVisible(false);
+ bannerSettings.set(id, true);
+ }, [id]);
+
+ if (!visible) {
+ return null;
+ }
+
+ return (
+
+ );
+};
diff --git a/app/javascript/mastodon/components/display_name.tsx b/app/javascript/mastodon/components/display_name.tsx
index c537cd24ce..82a42bb022 100644
--- a/app/javascript/mastodon/components/display_name.tsx
+++ b/app/javascript/mastodon/components/display_name.tsx
@@ -78,7 +78,7 @@ export class DisplayName extends React.PureComponent {
} else if (account) {
let acct = account.get('acct');
- if (acct.indexOf('@') === -1 && localDomain) {
+ if (!acct.includes('@') && localDomain) {
acct = `${acct}@${localDomain}`;
}
diff --git a/app/javascript/mastodon/components/gifv.tsx b/app/javascript/mastodon/components/gifv.tsx
index ac148c5a21..c2be591128 100644
--- a/app/javascript/mastodon/components/gifv.tsx
+++ b/app/javascript/mastodon/components/gifv.tsx
@@ -32,7 +32,7 @@ export const GIFV: React.FC = ({
onClick();
}
},
- [onClick]
+ [onClick],
);
return (
diff --git a/app/javascript/mastodon/components/hashtag.jsx b/app/javascript/mastodon/components/hashtag.jsx
index 4a7b9ef719..14bb4ddc64 100644
--- a/app/javascript/mastodon/components/hashtag.jsx
+++ b/app/javascript/mastodon/components/hashtag.jsx
@@ -11,7 +11,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
class SilentErrorBoundary extends Component {
diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx
index a2eb270231..d4d472334a 100644
--- a/app/javascript/mastodon/components/media_gallery.jsx
+++ b/app/javascript/mastodon/components/media_gallery.jsx
@@ -339,7 +339,10 @@ class MediaGallery extends PureComponent {
if (uncached) {
spoilerButton = (
);
} else if (visible) {
@@ -347,7 +350,10 @@ class MediaGallery extends PureComponent {
} else {
spoilerButton = (
);
}
diff --git a/app/javascript/mastodon/components/relative_timestamp.tsx b/app/javascript/mastodon/components/relative_timestamp.tsx
index e4a8437d0e..ac3ab0fb4d 100644
--- a/app/javascript/mastodon/components/relative_timestamp.tsx
+++ b/app/javascript/mastodon/components/relative_timestamp.tsx
@@ -108,7 +108,7 @@ export const timeAgoString = (
now: number,
year: number,
timeGiven: boolean,
- short?: boolean
+ short?: boolean,
) => {
const delta = now - date.getTime();
@@ -118,28 +118,28 @@ export const timeAgoString = (
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
relativeTime = intl.formatMessage(
- short ? messages.just_now : messages.just_now_full
+ short ? messages.just_now : messages.just_now_full,
);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
relativeTime = intl.formatMessage(
short ? messages.seconds : messages.seconds_full,
- { number: Math.floor(delta / SECOND) }
+ { number: Math.floor(delta / SECOND) },
);
} else if (delta < HOUR) {
relativeTime = intl.formatMessage(
short ? messages.minutes : messages.minutes_full,
- { number: Math.floor(delta / MINUTE) }
+ { number: Math.floor(delta / MINUTE) },
);
} else if (delta < DAY) {
relativeTime = intl.formatMessage(
short ? messages.hours : messages.hours_full,
- { number: Math.floor(delta / HOUR) }
+ { number: Math.floor(delta / HOUR) },
);
} else {
relativeTime = intl.formatMessage(
short ? messages.days : messages.days_full,
- { number: Math.floor(delta / DAY) }
+ { number: Math.floor(delta / DAY) },
);
}
} else if (date.getFullYear() === year) {
@@ -158,7 +158,7 @@ const timeRemainingString = (
intl: IntlShape,
date: Date,
now: number,
- timeGiven = true
+ timeGiven = true,
) => {
const delta = date.getTime() - now;
diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx
new file mode 100644
index 0000000000..c82711790b
--- /dev/null
+++ b/app/javascript/mastodon/components/router.tsx
@@ -0,0 +1,23 @@
+import type { PropsWithChildren } from 'react';
+import React from 'react';
+
+import type { History } from 'history';
+import { createBrowserHistory } from 'history';
+import { Router as OriginalRouter } from 'react-router';
+
+import { layoutFromWindow } from 'mastodon/is_mobile';
+
+const browserHistory = createBrowserHistory();
+const originalPush = browserHistory.push.bind(browserHistory);
+
+browserHistory.push = (path: string, state: History.LocationState) => {
+ if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
+ originalPush(`/deck${path}`, state);
+ } else {
+ originalPush(path, state);
+ }
+};
+
+export const Router: React.FC = ({ children }) => {
+ return {children};
+};
diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx
index 9982378601..63eec53492 100644
--- a/app/javascript/mastodon/components/server_banner.jsx
+++ b/app/javascript/mastodon/components/server_banner.jsx
@@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { fetchServer } from 'mastodon/actions/server';
import { ServerHeroImage } from 'mastodon/components/server_hero_image';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import Account from 'mastodon/containers/account_container';
import { domain } from 'mastodon/initial_state';
diff --git a/app/javascript/mastodon/components/short_number.jsx b/app/javascript/mastodon/components/short_number.jsx
deleted file mode 100644
index 27c1d25a8b..0000000000
--- a/app/javascript/mastodon/components/short_number.jsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import PropTypes from 'prop-types';
-import { memo } from 'react';
-
-import { FormattedMessage, FormattedNumber } from 'react-intl';
-
-import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
-
-// @ts-check
-
-/**
- * @callback ShortNumberRenderer
- * @param {JSX.Element} displayNumber Number to display
- * @param {number} pluralReady Number used for pluralization
- * @returns {JSX.Element} Final render of number
- */
-
-/**
- * @typedef {object} ShortNumberProps
- * @property {number} value Number to display in short variant
- * @property {ShortNumberRenderer} [renderer]
- * Custom renderer for numbers, provided as a prop. If another renderer
- * passed as a child of this component, this prop won't be used.
- * @property {ShortNumberRenderer} [children]
- * Custom renderer for numbers, provided as a child. If another renderer
- * passed as a prop of this component, this one will be used instead.
- */
-
-/**
- * Component that renders short big number to a shorter version
- * @param {ShortNumberProps} param0 Props for the component
- * @returns {JSX.Element} Rendered number
- */
-function ShortNumber({ value, isHide, renderer, children }) {
- const shortNumber = toShortNumber(value);
- const [, division] = shortNumber;
-
- if (children != null && renderer != null) {
- console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
- }
-
- const customRenderer = children != null ? children : renderer;
-
- const displayNumber = !isHide ? : -;
-
- return customRenderer != null
- ? customRenderer(displayNumber, pluralReady(value, division))
- : displayNumber;
-}
-
-ShortNumber.propTypes = {
- value: PropTypes.number.isRequired,
- isHide: PropTypes.bool,
- renderer: PropTypes.func,
- children: PropTypes.func,
-};
-
-/**
- * @typedef {object} ShortNumberCounterProps
- * @property {import('../utils/number').ShortNumber} value Short number
- */
-
-/**
- * Renders short number into corresponding localizable react fragment
- * @param {ShortNumberCounterProps} param0 Props for the component
- * @returns {JSX.Element} FormattedMessage ready to be embedded in code
- */
-function ShortNumberCounter({ value }) {
- const [rawNumber, unit, maxFractionDigits = 0] = value;
-
- const count = (
-
- );
-
- let values = { count, rawNumber };
-
- switch (unit) {
- case DECIMAL_UNITS.THOUSAND: {
- return (
-
- );
- }
- case DECIMAL_UNITS.MILLION: {
- return (
-
- );
- }
- case DECIMAL_UNITS.BILLION: {
- return (
-
- );
- }
- // Not sure if we should go farther - @Sasha-Sorokin
- default: return count;
- }
-}
-
-ShortNumberCounter.propTypes = {
- value: PropTypes.arrayOf(PropTypes.number),
-};
-
-export default memo(ShortNumber);
diff --git a/app/javascript/mastodon/components/short_number.tsx b/app/javascript/mastodon/components/short_number.tsx
new file mode 100644
index 0000000000..474288d6bd
--- /dev/null
+++ b/app/javascript/mastodon/components/short_number.tsx
@@ -0,0 +1,92 @@
+import { memo } from 'react';
+
+import { FormattedMessage, FormattedNumber } from 'react-intl';
+
+import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
+
+type ShortNumberRenderer = (
+ displayNumber: JSX.Element,
+ pluralReady: number,
+) => JSX.Element;
+
+interface ShortNumberProps {
+ value: number;
+ isHide: boolean;
+ renderer?: ShortNumberRenderer;
+ children?: ShortNumberRenderer;
+}
+
+export const ShortNumberRenderer: React.FC = ({
+ value,
+ isHide,
+ renderer,
+ children,
+}) => {
+ const shortNumber = toShortNumber(value);
+ const [, division] = shortNumber;
+
+ if (children && renderer) {
+ console.warn(
+ 'Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.',
+ );
+ }
+
+ const customRenderer = children ?? renderer ?? null;
+
+ const displayNumber = !isHide ? : -;
+
+ return (
+ customRenderer?.(displayNumber, pluralReady(value, division)) ??
+ displayNumber
+ );
+};
+export const ShortNumber = memo(ShortNumberRenderer);
+
+interface ShortNumberCounterProps {
+ value: number[];
+}
+const ShortNumberCounter: React.FC = ({ value }) => {
+ const [rawNumber, unit, maxFractionDigits = 0] = value;
+
+ const count = (
+
+ );
+
+ const values = { count, rawNumber };
+
+ switch (unit) {
+ case DECIMAL_UNITS.THOUSAND: {
+ return (
+
+ );
+ }
+ case DECIMAL_UNITS.MILLION: {
+ return (
+
+ );
+ }
+ case DECIMAL_UNITS.BILLION: {
+ return (
+
+ );
+ }
+ // Not sure if we should go farther - @Sasha-Sorokin
+ default:
+ return count;
+ }
+};
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 89d19c3da0..ced931f3f8 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -264,7 +264,6 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity;
- const anonymousAccess = !signedIn;
const publicStatus = ['public', 'unlisted', 'public_unlisted', 'login'].includes(status.get('visibility_ex'));
const anonymousStatus = ['public', 'unlisted', 'public_unlisted'].includes(status.get('visibility_ex'));
const pinnableStatus = ['public', 'unlisted', 'public_unlisted', 'login', 'private'].includes(status.get('visibility_ex'));
@@ -287,81 +286,83 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
}
- if (anonymousStatus) {
+ if (anonymousStatus && (signedIn || !isRemote)) {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
- menu.push(null);
-
- menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancelReblog : messages.reblog), action: this.handleReblogForceModalClick });
-
- if (publicStatus) {
- menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
- }
-
- menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
-
- if (writtenByMe && pinnableStatus) {
- menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
- }
-
- menu.push(null);
-
- if (writtenByMe || withDismiss) {
- menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
- menu.push(null);
- }
-
- if (writtenByMe) {
- menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
- menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
- menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
- } else {
- menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
- menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
+ if (signedIn) {
menu.push(null);
- if (relationship && relationship.get('muting')) {
- menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
+ menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancelReblog : messages.reblog), action: this.handleReblogForceModalClick });
+
+ if (publicStatus) {
+ menu.push({ text: intl.formatMessage(messages.reference), action: this.handleReference });
+ }
+
+ menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
+
+ if (writtenByMe && pinnableStatus) {
+ menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+ }
+
+ menu.push(null);
+
+ if (writtenByMe || withDismiss) {
+ menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+ menu.push(null);
+ }
+
+ if (writtenByMe) {
+ menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+ menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
- menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
- }
-
- if (relationship && relationship.get('blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
- } else {
- menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
- }
-
- if (!this.props.onFilter) {
- menu.push(null);
- menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
- menu.push(null);
- }
-
- menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
-
- if (account.get('acct') !== account.get('username')) {
- const domain = account.get('acct').split('@')[1];
-
+ menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
+ menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
- if (relationship && relationship.get('domain_blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ if (relationship && relationship.get('muting')) {
+ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
- menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
}
- }
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
- menu.push(null);
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
- menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
- menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ if (relationship && relationship.get('blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
}
- if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+
+ if (!this.props.onFilter) {
+ menu.push(null);
+ menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
+ menu.push(null);
+ }
+
+ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
+
+ if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
- menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+
+ menu.push(null);
+
+ if (relationship && relationship.get('domain_blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ }
+ }
+
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+ menu.push(null);
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+ menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+ menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ }
+ if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+ const domain = account.get('acct').split('@')[1];
+ menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+ }
}
}
}
@@ -410,7 +411,6 @@ class StatusActionBar extends ImmutablePureComponent {
-
+
-
+
diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx
index 31bbb3708a..c0577b830b 100644
--- a/app/javascript/mastodon/containers/status_container.jsx
+++ b/app/javascript/mastodon/containers/status_container.jsx
@@ -154,7 +154,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
dispatch(openModal({
modalType: 'EMBED',
modalProps: {
- url: status.get('url'),
+ id: status.get('id'),
onError: error => dispatch(showAlertForError(error)),
},
}));
diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx
index 7ee801d800..86e1945a5d 100644
--- a/app/javascript/mastodon/features/account/components/header.jsx
+++ b/app/javascript/mastodon/features/account/components/header.jsx
@@ -11,10 +11,10 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'mastodon/components/avatar';
import Button from 'mastodon/components/button';
-import { counterRenderer } from 'mastodon/components/common_counter';
+import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
@@ -266,14 +266,14 @@ class Header extends ImmutablePureComponent {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
- actionBtn = ;
+ actionBtn = ;
} else if (!account.getIn(['relationship', 'blocking'])) {
- actionBtn = ;
+ actionBtn = ;
} else if (account.getIn(['relationship', 'blocking'])) {
- actionBtn = ;
+ actionBtn = ;
}
} else {
- actionBtn = ;
+ actionBtn = ;
}
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
@@ -292,7 +292,6 @@ class Header extends ImmutablePureComponent {
if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
- menu.push(null);
}
if ('share' in navigator) {
@@ -455,7 +454,7 @@ class Header extends ImmutablePureComponent {
@@ -463,7 +462,7 @@ class Header extends ImmutablePureComponent {
@@ -471,7 +470,7 @@ class Header extends ImmutablePureComponent {
diff --git a/app/javascript/mastodon/features/community_timeline/index.jsx b/app/javascript/mastodon/features/community_timeline/index.jsx
index 7e3b9babe9..2d94cabed2 100644
--- a/app/javascript/mastodon/features/community_timeline/index.jsx
+++ b/app/javascript/mastodon/features/community_timeline/index.jsx
@@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
-import DismissableBanner from 'mastodon/components/dismissable_banner';
+import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { domain } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
diff --git a/app/javascript/mastodon/features/directory/components/account_card.jsx b/app/javascript/mastodon/features/directory/components/account_card.jsx
index cf1c63f9e4..0306b63d32 100644
--- a/app/javascript/mastodon/features/directory/components/account_card.jsx
+++ b/app/javascript/mastodon/features/directory/components/account_card.jsx
@@ -19,7 +19,7 @@ import { openModal } from 'mastodon/actions/modal';
import { Avatar } from 'mastodon/components/avatar';
import Button from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors';
@@ -160,16 +160,16 @@ class AccountCard extends ImmutablePureComponent {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
- actionBtn = ;
+ actionBtn = ;
} else if (account.getIn(['relationship', 'muting'])) {
- actionBtn = ;
+ actionBtn = ;
} else if (!account.getIn(['relationship', 'blocking'])) {
- actionBtn = ;
+ actionBtn = ;
} else if (account.getIn(['relationship', 'blocking'])) {
- actionBtn = ;
+ actionBtn = ;
}
} else {
- actionBtn = ;
+ actionBtn = ;
}
return (
diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts b/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
index 2408942ed6..9f0feba06c 100644
--- a/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
+++ b/app/javascript/mastodon/features/emoji/emoji_compressed.d.ts
@@ -25,12 +25,13 @@ export type SearchData = [
BaseEmoji['native'],
Emoji['short_names'],
Search,
- Emoji['unified']
+ Emoji['unified'],
];
-export interface ShortCodesToEmojiData {
- [key: ShortCodesToEmojiDataKey]: [FilenameData, SearchData];
-}
+export type ShortCodesToEmojiData = Record<
+ ShortCodesToEmojiDataKey,
+ [FilenameData, SearchData]
+>;
export type EmojisWithoutShortCodes = FilenameData[];
export type EmojiCompressed = [
@@ -38,7 +39,7 @@ export type EmojiCompressed = [
Skins,
Category[],
Data['aliases'],
- EmojisWithoutShortCodes
+ EmojisWithoutShortCodes,
];
/*
diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
index 62cb84baf8..142605b4bc 100644
--- a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
+++ b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts
@@ -9,7 +9,7 @@ import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = {
- [key in keyof ShortCodesToEmojiData]: {
+ [key in NonNullable]: {
native: BaseEmoji['native'];
search: Search;
short_names: Emoji['short_names'];
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx
index 0a9fbb1905..73ec99c14b 100644
--- a/app/javascript/mastodon/features/explore/components/story.jsx
+++ b/app/javascript/mastodon/features/explore/components/story.jsx
@@ -5,7 +5,7 @@ import classNames from 'classnames';
import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag';
-import ShortNumber from 'mastodon/components/short_number';
+import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
export default class Story extends PureComponent {
diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx
index 185db0732a..1a66adc87c 100644
--- a/app/javascript/mastodon/features/explore/index.jsx
+++ b/app/javascript/mastodon/features/explore/index.jsx
@@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import Search from 'mastodon/features/compose/containers/search_container';
-import { showTrends } from 'mastodon/initial_state';
+import { trendsEnabled } from 'mastodon/initial_state';
import Links from './links';
import SearchResults from './results';
@@ -26,7 +26,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']),
- isSearching: state.getIn(['search', 'submitted']) || !showTrends,
+ isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
});
class Explore extends PureComponent {
diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx
index 49c667f027..8b199bf47c 100644
--- a/app/javascript/mastodon/features/explore/links.jsx
+++ b/app/javascript/mastodon/features/explore/links.jsx
@@ -7,7 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { fetchTrendingLinks } from 'mastodon/actions/trends';
-import DismissableBanner from 'mastodon/components/dismissable_banner';
+import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import Story from './components/story';
diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx
index eb2fe777a6..043492fb89 100644
--- a/app/javascript/mastodon/features/explore/statuses.jsx
+++ b/app/javascript/mastodon/features/explore/statuses.jsx
@@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { fetchTrendingStatuses, expandTrendingStatuses } from 'mastodon/actions/trends';
-import DismissableBanner from 'mastodon/components/dismissable_banner';
+import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import StatusList from 'mastodon/components/status_list';
import { getStatusList } from 'mastodon/selectors';
@@ -52,6 +52,7 @@ class Statuses extends PureComponent {
}
-
+
{(multiColumn && showTrends) && }
diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.jsx b/app/javascript/mastodon/features/home_timeline/components/column_settings.jsx
deleted file mode 100644
index b6e6b9cff6..0000000000
--- a/app/javascript/mastodon/features/home_timeline/components/column_settings.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
-
-import { injectIntl, FormattedMessage } from 'react-intl';
-
-import ImmutablePropTypes from 'react-immutable-proptypes';
-
-import SettingToggle from '../../notifications/components/setting_toggle';
-
-class ColumnSettings extends PureComponent {
-
- static propTypes = {
- settings: ImmutablePropTypes.map.isRequired,
- onChange: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- };
-
- render () {
- const { settings, onChange } = this.props;
-
- return (
-
-
-
-
- } />
-
-
-
- } />
-
-
- );
- }
-
-}
-
-export default injectIntl(ColumnSettings);
diff --git a/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx b/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx
new file mode 100644
index 0000000000..ca09d46c7e
--- /dev/null
+++ b/app/javascript/mastodon/features/home_timeline/components/column_settings.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call,
+ @typescript-eslint/no-unsafe-return,
+ @typescript-eslint/no-unsafe-assignment,
+ @typescript-eslint/no-unsafe-member-access
+ -- the settings store is not yet typed */
+import { useCallback } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { useAppSelector, useAppDispatch } from 'mastodon/store';
+
+import { changeSetting } from '../../../actions/settings';
+import SettingToggle from '../../notifications/components/setting_toggle';
+
+export const ColumnSettings: React.FC = () => {
+ const settings = useAppSelector((state) => state.settings.get('home'));
+
+ const dispatch = useAppDispatch();
+ const onChange = useCallback(
+ (key: string, checked: boolean) => {
+ dispatch(changeSetting(['home', ...key], checked));
+ },
+ [dispatch],
+ );
+
+ return (
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+
+ );
+};
diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx
deleted file mode 100644
index a6993c6418..0000000000
--- a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react';
-
-import { FormattedMessage } from 'react-intl';
-
-import { Link } from 'react-router-dom';
-
-import background from 'mastodon/../images/friends-cropped.png';
-import DismissableBanner from 'mastodon/components/dismissable_banner';
-
-
-export const ExplorePrompt = () => (
-
-
-
-
-
-
-
-
-);
diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx
new file mode 100644
index 0000000000..47113d9b8e
--- /dev/null
+++ b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx
@@ -0,0 +1,46 @@
+import { FormattedMessage } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import background from 'mastodon/../images/friends-cropped.png';
+import { DismissableBanner } from 'mastodon/components/dismissable_banner';
+
+export const ExplorePrompt = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js
deleted file mode 100644
index 1ddec6da9c..0000000000
--- a/app/javascript/mastodon/features/home_timeline/containers/column_settings_container.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { connect } from 'react-redux';
-
-import { changeSetting, saveSettings } from '../../../actions/settings';
-import ColumnSettings from '../components/column_settings';
-
-const mapStateToProps = state => ({
- settings: state.getIn(['settings', 'home']),
-});
-
-const mapDispatchToProps = dispatch => ({
-
- onChange (key, checked) {
- dispatch(changeSetting(['home', ...key], checked));
- },
-
- onSave () {
- dispatch(saveSettings());
- },
-
-});
-
-export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx
index ae98aec0a6..1cd6edd7aa 100644
--- a/app/javascript/mastodon/features/home_timeline/index.jsx
+++ b/app/javascript/mastodon/features/home_timeline/index.jsx
@@ -22,8 +22,8 @@ import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import StatusListContainer from '../ui/containers/status_list_container';
+import { ColumnSettings } from './components/column_settings';
import { ExplorePrompt } from './components/explore_prompt';
-import ColumnSettingsContainer from './containers/column_settings_container';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
@@ -191,7 +191,7 @@ class HomeTimeline extends PureComponent {
extraButton={announcementsButton}
appendContent={hasAnnouncements && showAnnouncements && }
>
-
+
{signedIn ? (
diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
index b63796a8b2..1e62ed9a5a 100644
--- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') {
dispatch(changePushNotifications(path.slice(1), checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
@@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx
index 352baa8336..3bfb25ba73 100644
--- a/app/javascript/mastodon/features/public_timeline/index.jsx
+++ b/app/javascript/mastodon/features/public_timeline/index.jsx
@@ -7,7 +7,7 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux';
-import DismissableBanner from 'mastodon/components/dismissable_banner';
+import { DismissableBanner } from 'mastodon/components/dismissable_banner';
import { domain } from 'mastodon/initial_state';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
diff --git a/app/javascript/mastodon/features/report/comment.jsx b/app/javascript/mastodon/features/report/comment.jsx
index 4888b76bcb..98ac4caa0a 100644
--- a/app/javascript/mastodon/features/report/comment.jsx
+++ b/app/javascript/mastodon/features/report/comment.jsx
@@ -1,87 +1,121 @@
import PropTypes from 'prop-types';
-import { PureComponent } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
-import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
+import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
+
+import { OrderedSet, List as ImmutableList } from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { shallowEqual } from 'react-redux';
+import { createSelector } from 'reselect';
import Toggle from 'react-toggle';
+import { fetchAccount } from 'mastodon/actions/accounts';
import Button from 'mastodon/components/button';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
const messages = defineMessages({
placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' },
});
-class Comment extends PureComponent {
+const selectRepliedToAccountIds = createSelector(
+ [
+ (state) => state.get('statuses'),
+ (_, statusIds) => statusIds,
+ ],
+ (statusesMap, statusIds) => statusIds.map((statusId) => statusesMap.getIn([statusId, 'in_reply_to_account_id'])),
+ {
+ resultEqualityCheck: shallowEqual,
+ }
+);
- static propTypes = {
- onSubmit: PropTypes.func.isRequired,
- comment: PropTypes.string.isRequired,
- onChangeComment: PropTypes.func.isRequired,
- intl: PropTypes.object.isRequired,
- isSubmitting: PropTypes.bool,
- forward: PropTypes.bool,
- isRemote: PropTypes.bool,
- domain: PropTypes.string,
- onChangeForward: PropTypes.func.isRequired,
- };
+const Comment = ({ comment, domain, statusIds, isRemote, isSubmitting, selectedDomains, onSubmit, onChangeComment, onToggleDomain }) => {
+ const intl = useIntl();
- handleClick = () => {
- const { onSubmit } = this.props;
- onSubmit();
- };
+ const dispatch = useAppDispatch();
+ const loadedRef = useRef(false);
- handleChange = e => {
- const { onChangeComment } = this.props;
- onChangeComment(e.target.value);
- };
+ const handleClick = useCallback(() => onSubmit(), [onSubmit]);
+ const handleChange = useCallback((e) => onChangeComment(e.target.value), [onChangeComment]);
+ const handleToggleDomain = useCallback(e => onToggleDomain(e.target.value, e.target.checked), [onToggleDomain]);
- handleKeyDown = e => {
+ const handleKeyDown = useCallback((e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
- this.handleClick();
+ handleClick();
}
- };
+ }, [handleClick]);
- handleForwardChange = e => {
- const { onChangeForward } = this.props;
- onChangeForward(e.target.checked);
- };
+ // Memoize accountIds since we don't want it to trigger `useEffect` on each render
+ const accountIds = useAppSelector((state) => domain ? selectRepliedToAccountIds(state, statusIds) : ImmutableList());
- render () {
- const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props;
+ // While we could memoize `availableDomains`, it is pretty inexpensive to recompute
+ const accountsMap = useAppSelector((state) => state.get('accounts'));
+ const availableDomains = domain ? OrderedSet([domain]).union(accountIds.map((accountId) => accountsMap.getIn([accountId, 'acct'], '').split('@')[1]).filter(domain => !!domain)) : OrderedSet();
- return (
- <>
-
+ useEffect(() => {
+ if (loadedRef.current) {
+ return;
+ }
-
+ loadedRef.current = true;
- {isRemote && (
- <>
-
+ // First, pre-select known domains
+ availableDomains.forEach((domain) => {
+ onToggleDomain(domain, true);
+ });
-