diff --git a/.eslintrc.js b/.eslintrc.js index 8bc6e0895..206faa1c7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -81,6 +81,15 @@ module.exports = { { property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substr', message: 'Use .slice instead of .substr.' }, ], + 'no-restricted-syntax': [ + 'error', + { + // eslint-disable-next-line no-restricted-syntax + selector: 'Literal[value=/•/], JSXText[value=/•/]', + // eslint-disable-next-line no-restricted-syntax + message: "Use '·' (middle dot) instead of '•' (bullet)", + }, + ], 'no-self-assign': 'off', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', @@ -293,6 +302,7 @@ module.exports = { '.*rc.js', 'ide-helper.js', 'config/webpack/**/*', + 'config/formatjs-formatter.js', ], env: { diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 000000000..1ae40d416 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,114 @@ +{ + $schema: 'https://docs.renovatebot.com/renovate-schema.json', + extends: [ + 'config:base', + ':dependencyDashboard', + ':labels(dependencies)', + ':maintainLockFilesMonthly', // update non-direct dependencies monthly + ':prConcurrentLimit10', // only 10 open PRs at the same time + ], + stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it + // packageRules order is important, they are applied from top to bottom and are merged, + // so for example grouping rules needs to be at the bottom + packageRules: [ + { + // 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 + + // Requires Webpacker upgrade or replacement + '@types/webpack', + 'babel-loader', + 'compression-webpack-plugin', + 'css-loader', + 'imports-loader', + 'mini-css-extract-plugin', + 'postcss-loader', + 'sass-loader', + 'terser-webpack-plugin', + 'webpack', + 'webpack-assets-manifest', + 'webpack-bundle-analyzer', + 'webpack-dev-server', + 'webpack-cli', + + // react-router: Requires manual upgrade + 'history', + 'react-router-dom', + ], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major version bumps for these Ruby packages + matchManagers: ['bundler'], + matchPackageNames: [ + 'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x + 'strong_migrations', // Requires manual upgrade + '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, + }, + { + // Update Github Actions and Docker images weekly + matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], + extends: ['schedule:weekly'], + }, + { + // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version + matchManagers: ['dockerfile'], + matchPackageNames: ['moritzheiber/ruby-jemalloc'], + matchUpdateTypes: ['minor', 'major'], + enabled: false, + }, + { + // Ignore major bump for the node image, this needs to be synced with .nvmrc + matchManagers: ['dockerfile'], + matchPackageNames: ['node'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Ignore major postgres bumps in the docker-compose file, as those break dev environments + matchManagers: ['docker-compose'], + matchPackageNames: ['postgres'], + matchUpdateTypes: ['major'], + enabled: false, + }, + { + // Update devDependencies every week, with one grouped PR + matchDepTypes: 'devDependencies', + matchUpdateTypes: ['patch', 'minor'], + excludePackageNames: [ + 'typescript', // Typescript has many changes in minor versions, needs to be checked every time + ], + groupName: 'devDependencies (non-major)', + extends: ['schedule:weekly'], + }, + { + // Update @types/* packages every week, with one grouped PR + matchPackagePrefixes: '@types/', + matchUpdateTypes: ['patch', 'minor'], + groupName: 'DefinitelyTyped types (non-major)', + extends: ['schedule:weekly'], + addLabels: ['typescript'], + }, + // Add labels depending on package manager + { matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] }, + { matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] }, + { matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] }, + { matchManagers: ['github-actions'], addLabels: ['github_actions'] }, + ], +} diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index e282e2ab7..b67c503e9 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -41,8 +41,7 @@ jobs: - name: Check for missing strings in English JSON run: | - yarn build:development - yarn manage:translations en + yarn i18n:extract --throws git diff --exit-code - name: Check locale file normalization diff --git a/.github/workflows/lint-css.yml b/.github/workflows/lint-css.yml index e13d227bd..4d3c2ce5a 100644 --- a/.github/workflows/lint-css.yml +++ b/.github/workflows/lint-css.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -48,4 +49,4 @@ jobs: - run: echo "::add-matcher::.github/stylelint-matcher.json" - name: Stylelint - run: yarn test:lint:sass + run: yarn lint:sass diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 2ddbca781..56d817123 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - '.github/workflows/haml-lint-problem-matcher.json' - '.github/workflows/lint-haml.yml' diff --git a/.github/workflows/lint-js.yml b/.github/workflows/lint-js.yml index 7700e4851..1f0cfd1e7 100644 --- a/.github/workflows/lint-js.yml +++ b/.github/workflows/lint-js.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -48,7 +49,7 @@ jobs: run: yarn --frozen-lockfile - name: ESLint - run: yarn test:lint:js --max-warnings 0 + run: yarn lint:js --max-warnings 0 - name: Typecheck - run: yarn test:typecheck + run: yarn typecheck diff --git a/.github/workflows/lint-json.yml b/.github/workflows/lint-json.yml index 98f101ad9..8712d8bd8 100644 --- a/.github/workflows/lint-json.yml +++ b/.github/workflows/lint-json.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -40,4 +41,4 @@ jobs: run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.json" + run: yarn lint:json diff --git a/.github/workflows/lint-md.yml b/.github/workflows/lint-md.yml index 6f76dd60c..d19a0470d 100644 --- a/.github/workflows/lint-md.yml +++ b/.github/workflows/lint-md.yml @@ -3,8 +3,10 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - '.github/workflows/lint-md.yml' + - '.nvmrc' - '.prettier*' - '**/*.md' - '!AUTHORS.md' @@ -14,6 +16,7 @@ on: pull_request: paths: - '.github/workflows/lint-md.yml' + - '.nvmrc' - '.prettier*' - '**/*.md' - '!AUTHORS.md' @@ -32,9 +35,10 @@ jobs: uses: actions/setup-node@v3 with: cache: yarn + node-version-file: '.nvmrc' - name: Install all yarn packages run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.md" + run: yarn lint:md diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index de54fe9ae..0395c8639 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'Gemfile*' - '.rubocop*.yml' diff --git a/.github/workflows/lint-yml.yml b/.github/workflows/lint-yml.yml index 6f79babcf..295e9610b 100644 --- a/.github/workflows/lint-yml.yml +++ b/.github/workflows/lint-yml.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -42,4 +43,4 @@ jobs: run: yarn --frozen-lockfile - name: Prettier - run: yarn prettier --check "**/*.{yml,yaml}" + run: yarn lint:yml diff --git a/.github/workflows/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 6a8035210..131a62a57 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -4,10 +4,12 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' - 'l10n_main' pull_request_target: branches-ignore: - 'dependabot/**' + - 'renovate/**' - 'l10n_main' types: [synchronize] diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 1c4958550..3306105f9 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' paths: - 'package.json' - 'yarn.lock' @@ -44,4 +45,4 @@ jobs: run: yarn --frozen-lockfile - name: Jest testing - run: yarn test:jest --reporters github-actions summary + run: yarn jest --reporters github-actions summary diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations-one-step.yml index 212b2cfe7..a91fd819a 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations-one-step.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: jobs: diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml index 310153929..50266fb8a 100644 --- a/.github/workflows/test-migrations-two-step.yml +++ b/.github/workflows/test-migrations-two-step.yml @@ -3,6 +3,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: jobs: diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index f284745ea..07cb1d41f 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -4,6 +4,7 @@ on: push: branches-ignore: - 'dependabot/**' + - 'renovate/**' pull_request: env: diff --git a/.haml-lint.yml b/.haml-lint.yml index 12ca46342..d1ed30b26 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -4,6 +4,11 @@ exclude: - 'vendor/**/*' - lib/templates/haml/scaffold/_form.html.haml +require: + - ./lib/linter/haml_middle_dot.rb + linters: AltText: enabled: true + MiddleDot: + enabled: true diff --git a/.prettierignore b/.prettierignore index af0411e9c..27b6d5458 100644 --- a/.prettierignore +++ b/.prettierignore @@ -61,7 +61,7 @@ docker-compose.override.yml /app/javascript/mastodon/features/emoji/emoji_map.json # Ignore locale files -/app/javascript/mastodon/locales +/app/javascript/mastodon/locales/*.json /config/locales # Ignore vendored CSS reset diff --git a/.rubocop.yml b/.rubocop.yml index 813cb1c01..deb9c37de 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -11,6 +11,7 @@ require: - rubocop-rspec - rubocop-performance - rubocop-capybara + - ./lib/linter/rubocop_middle_dot AllCops: TargetRubyVersion: 3.0 # Set to minimum supported version of CI @@ -205,3 +206,6 @@ Style/TrailingCommaInArrayLiteral: # https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: 'comma' + +Style/MiddleDot: + Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 082ff3219..7cbad5c75 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -243,79 +243,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SkipBlocks, EnforcedStyle. -# SupportedStyles: described_class, explicit -RSpec/DescribedClass: - Exclude: - - 'spec/controllers/concerns/cache_concern_spec.rb' - - 'spec/controllers/concerns/challengable_concern_spec.rb' - - 'spec/lib/entity_cache_spec.rb' - - 'spec/lib/extractor_spec.rb' - - 'spec/lib/feed_manager_spec.rb' - - 'spec/lib/hash_object_spec.rb' - - 'spec/lib/ostatus/tag_manager_spec.rb' - - 'spec/lib/request_spec.rb' - - 'spec/lib/tag_manager_spec.rb' - - 'spec/lib/webfinger_resource_spec.rb' - - 'spec/mailers/notification_mailer_spec.rb' - - 'spec/mailers/user_mailer_spec.rb' - - 'spec/models/account_conversation_spec.rb' - - 'spec/models/account_domain_block_spec.rb' - - 'spec/models/account_migration_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/block_spec.rb' - - 'spec/models/domain_block_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/export_spec.rb' - - 'spec/models/favourite_spec.rb' - - 'spec/models/follow_spec.rb' - - 'spec/models/identity_spec.rb' - - 'spec/models/import_spec.rb' - - 'spec/models/media_attachment_spec.rb' - - 'spec/models/notification_spec.rb' - - 'spec/models/relationship_filter_spec.rb' - - 'spec/models/report_filter_spec.rb' - - 'spec/models/session_activation_spec.rb' - - 'spec/models/setting_spec.rb' - - 'spec/models/site_upload_spec.rb' - - 'spec/models/status_pin_spec.rb' - - 'spec/models/status_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/policies/account_moderation_note_policy_spec.rb' - - 'spec/presenters/account_relationships_presenter_spec.rb' - - 'spec/presenters/status_relationships_presenter_spec.rb' - - 'spec/serializers/activitypub/note_serializer_spec.rb' - - 'spec/serializers/activitypub/update_poll_serializer_spec.rb' - - 'spec/serializers/rest/account_serializer_spec.rb' - - 'spec/services/activitypub/fetch_remote_account_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_actor_service_spec.rb' - - 'spec/services/activitypub/fetch_remote_key_service_spec.rb' - - 'spec/services/after_block_domain_from_account_service_spec.rb' - - 'spec/services/authorize_follow_service_spec.rb' - - 'spec/services/batched_remove_status_service_spec.rb' - - 'spec/services/block_domain_service_spec.rb' - - 'spec/services/block_service_spec.rb' - - 'spec/services/bootstrap_timeline_service_spec.rb' - - 'spec/services/clear_domain_media_service_spec.rb' - - 'spec/services/favourite_service_spec.rb' - - 'spec/services/follow_service_spec.rb' - - 'spec/services/import_service_spec.rb' - - 'spec/services/post_status_service_spec.rb' - - 'spec/services/precompute_feed_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - - 'spec/services/purge_domain_service_spec.rb' - - 'spec/services/reblog_service_spec.rb' - - 'spec/services/reject_follow_service_spec.rb' - - 'spec/services/remove_from_followers_service_spec.rb' - - 'spec/services/remove_status_service_spec.rb' - - 'spec/services/unallow_domain_service_spec.rb' - - 'spec/services/unblock_service_spec.rb' - - 'spec/services/unfollow_service_spec.rb' - - 'spec/services/unmute_service_spec.rb' - - 'spec/services/update_account_service_spec.rb' - - 'spec/validators/note_length_validator_spec.rb' - # This cop supports unsafe autocorrection (--autocorrect-all). RSpec/EmptyExampleGroup: Exclude: @@ -472,30 +399,6 @@ RSpec/MessageSpies: - 'spec/spec_helper.rb' - 'spec/validators/status_length_validator_spec.rb' -RSpec/MissingExampleGroupArgument: - Exclude: - - 'spec/controllers/accounts_controller_spec.rb' - - 'spec/controllers/activitypub/collections_controller_spec.rb' - - 'spec/controllers/admin/statuses_controller_spec.rb' - - 'spec/controllers/admin/users/roles_controller_spec.rb' - - 'spec/controllers/api/v1/accounts_controller_spec.rb' - - 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb' - - 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb' - - 'spec/controllers/api/v1/statuses_controller_spec.rb' - - 'spec/controllers/auth/registrations_controller_spec.rb' - - 'spec/features/log_in_spec.rb' - - 'spec/lib/activitypub/activity/undo_spec.rb' - - 'spec/lib/status_reach_finder_spec.rb' - - 'spec/models/account_spec.rb' - - 'spec/models/email_domain_block_spec.rb' - - 'spec/models/trends/statuses_spec.rb' - - 'spec/models/trends/tags_spec.rb' - - 'spec/models/user_role_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/services/fetch_link_card_service_spec.rb' - - 'spec/services/notify_service_spec.rb' - - 'spec/services/process_mentions_service_spec.rb' - RSpec/MultipleExpectations: Max: 19 @@ -1345,11 +1248,6 @@ Style/GlobalStdStream: - 'config/environments/development.rb' - 'config/environments/production.rb' -# Configuration parameters: AllowedVariables. -Style/GlobalVars: - Exclude: - - 'config/initializers/statsd.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: @@ -1490,7 +1388,6 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' - - 'config/initializers/statsd.rb' - 'config/locales/sr-Latn.rb' - 'config/locales/sr.rb' @@ -1504,52 +1401,6 @@ Style/RedundantFetchBlock: - 'config/initializers/paperclip.rb' - 'config/puma.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpCharacterClass: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/tag_manager.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/services/fetch_oembed_service.rb' - - 'config/initializers/rack_attack.rb' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantRegexpEscape: - Exclude: - - 'app/lib/webfinger_resource.rb' - - 'app/models/account.rb' - - 'app/models/tag.rb' - - 'app/services/fetch_link_card_service.rb' - - 'config/initializers/twitter_regex.rb' - - 'lib/paperclip/color_extractor.rb' - - 'lib/tasks/mastodon.rake' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, AllowInnerSlashes. -# SupportedStyles: slashes, percent_r, mixed -Style/RegexpLiteral: - Exclude: - - 'app/lib/link_details_extractor.rb' - - 'app/lib/plain_text_formatter.rb' - - 'app/lib/tag_manager.rb' - - 'app/lib/text_formatter.rb' - - 'app/models/account.rb' - - 'app/models/domain_allow.rb' - - 'app/models/domain_block.rb' - - 'app/models/site_upload.rb' - - 'app/models/tag.rb' - - 'app/services/backup_service.rb' - - 'app/services/fetch_oembed_service.rb' - - 'app/services/search_service.rb' - - 'config/initializers/rack_attack.rb' - - 'config/initializers/twitter_regex.rb' - - 'config/routes.rb' - - 'lib/mastodon/premailer_webpack_strategy.rb' - - 'lib/tasks/mastodon.rake' - # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # AllowedMethods: present?, blank?, presence, try, try! diff --git a/Gemfile b/Gemfile index 51308c24d..84f210f48 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,7 @@ ruby '>= 3.0.0' gem 'pkg-config', '~> 1.5' -gem 'puma', '~> 6.2' +gem 'puma', '~> 6.3' gem 'rails', '~> 6.1.7' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 1.2' @@ -17,10 +17,10 @@ gem 'makara', '~> 0.5' gem 'pghero' gem 'dotenv-rails', '~> 2.8' -gem 'aws-sdk-s3', '~> 1.122', require: false +gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false -gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' +gem 'kt-paperclip', '~> 7.2' gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' -gem 'nsa', '~> 0.2' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' diff --git a/Gemfile.lock b/Gemfile.lock index 0e40ce2c0..c4fa1822e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,18 +7,6 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) -GIT - remote: https://github.com/kreeti/kt-paperclip.git - revision: 11abf222dc31bff71160a1d138b445214f434b2b - ref: 11abf222dc31bff71160a1d138b445214f434b2b - specs: - kt-paperclip (7.1.1) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - marcel (~> 1.0.1) - mime-types - terrapin (~> 0.6.0) - GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -109,17 +97,17 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.761.0) - aws-sdk-core (3.172.0) + aws-partitions (1.772.0) + aws-sdk-core (3.174.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.64.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-kms (1.65.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.122.0) - aws-sdk-core (~> 3, >= 3.165.0) + aws-sdk-s3 (1.123.0) + aws-sdk-core (~> 3, >= 3.174.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) aws-sigv4 (1.5.2) @@ -380,6 +368,12 @@ GEM activerecord kaminari-core (= 1.2.2) kaminari-core (1.2.2) + kt-paperclip (7.2.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) + marcel (~> 1.0.1) + mime-types + terrapin (~> 0.6.0) launchy (2.5.2) addressable (~> 2.8) letter_opener (1.8.1) @@ -442,11 +436,6 @@ GEM nokogiri (1.15.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nsa (0.2.8) - activesupport (>= 4.2, < 7) - concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (>= 3.5) - statsd-ruby (~> 1.4, >= 1.4.0) oj (3.14.3) omniauth (1.9.2) hashie (>= 3.4.6) @@ -501,7 +490,7 @@ GEM premailer (~> 1.7, >= 1.7.9) private_address_check (0.5.0) public_suffix (5.0.1) - puma (6.2.2) + puma (6.3.0) nio4r (~> 2.0) pundit (2.3.0) activesupport (>= 3.0.0) @@ -544,8 +533,9 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.5.0) - loofah (~> 2.19, >= 2.19.1) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rails-i18n (6.0.0) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 7) @@ -588,7 +578,7 @@ GEM rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) - rspec-rails (6.0.2) + rspec-rails (6.0.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -648,7 +638,7 @@ GEM redis (>= 4.5.0, < 5) sidekiq-bulk (0.2.0) sidekiq - sidekiq-scheduler (5.0.2) + sidekiq-scheduler (5.0.3) rufus-scheduler (~> 3.2) sidekiq (>= 6, < 8) tilt (>= 1.4.0) @@ -681,7 +671,6 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stackprof (0.2.25) - statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -770,7 +759,7 @@ DEPENDENCIES active_model_serializers (~> 0.10) addressable (~> 2.8) annotate (~> 3.2) - aws-sdk-s3 (~> 1.122) + aws-sdk-s3 (~> 1.123) better_errors (~> 2.9) binding_of_caller (~> 1.0) blurhash (~> 0.1) @@ -818,7 +807,7 @@ DEPENDENCIES json-ld-preloaded (~> 3.2) json-schema (~> 4.0) kaminari (~> 1.2) - kt-paperclip (~> 7.1)! + kt-paperclip (~> 7.2) letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) @@ -830,7 +819,6 @@ DEPENDENCIES net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) - nsa (~> 0.2) oj (~> 3.14) omniauth (~> 1.9) omniauth-cas (~> 2.0) @@ -846,7 +834,7 @@ DEPENDENCIES premailer-rails private_address_check (~> 0.5) public_suffix (~> 5.0) - puma (~> 6.2) + puma (~> 6.3) pundit (~> 2.3) rack (~> 2.2.7) rack-attack (~> 6.6) @@ -896,7 +884,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.2.1p31 + ruby 3.2.2p53 BUNDLED WITH - 2.4.6 + 2.4.13 diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 099512248..3a6df662e 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -14,15 +14,5 @@ module Admin @pending_tags_count = Tag.pending_review.count @pending_appeals_count = Appeal.pending.count end - - private - - def redis_info - @redis_info ||= if redis.is_a?(Redis::Namespace) - redis.redis.info - else - redis.info - end - end end end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 081550b76..b9691c5a3 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -31,31 +31,41 @@ module Admin @domain_block = DomainBlock.new(resource_params) existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil + # Disallow accidentally downgrading a domain block if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe @domain_block.errors.delete(:domain) - render :new - else - if existing_domain_block.present? - @domain_block = existing_domain_block - @domain_block.update(resource_params) - end + return render :new + end - if @domain_block.save - DomainBlockWorker.perform_async(@domain_block.id) - log_action :create, @domain_block - redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') - else - render :new - end + # Allow transparently upgrading a domain block + if existing_domain_block.present? + @domain_block = existing_domain_block + @domain_block.assign_attributes(resource_params) + end + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save + DomainBlockWorker.perform_async(@domain_block.id) + log_action :create, @domain_block + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + else + render :new end end def update authorize :domain_block, :update? - if @domain_block.update(update_params) + @domain_block.assign_attributes(update_params) + + # Require explicit confirmation when suspending + return render :confirm_suspension if requires_confirmation? + + if @domain_block.save DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) log_action :update, @domain_block redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') @@ -92,5 +102,9 @@ module Admin def action_from_button 'save' if params[:save] end + + def requires_confirmation? + @domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm] + end end end diff --git a/app/controllers/admin/webhooks_controller.rb b/app/controllers/admin/webhooks_controller.rb index 1ed3fd18a..01d9ba8ce 100644 --- a/app/controllers/admin/webhooks_controller.rb +++ b/app/controllers/admin/webhooks_controller.rb @@ -71,7 +71,7 @@ module Admin end def resource_params - params.require(:webhook).permit(:url, events: []) + params.require(:webhook).permit(:url, :template, events: []) end end end diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 8af4242ba..ddb94d5ca 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController end def account_params - params.permit(:username, :email, :password, :agreement, :locale, :reason) + params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone) end def check_enabled_registrations diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb index 9034e8a2f..c55500f76 100644 --- a/app/controllers/api/v1/conversations_controller.rb +++ b/app/controllers/api/v1/conversations_controller.rb @@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController def index @conversations = paginated_conversations - render json: @conversations, each_serializer: REST::ConversationSerializer + render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id) end def read @@ -32,7 +32,20 @@ class Api::V1::ConversationsController < Api::BaseController def paginated_conversations AccountConversation.where(account: current_account) - .to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + .includes( + account: :account_stat, + last_status: [ + :media_attachments, + :preview_cards, + :status_stat, + :tags, + { + active_mentions: [account: :account_stat], + account: :account_stat, + }, + ] + ) + .to_a_paginated_by_id(limit_param(LIMIT), **params_slice(:max_id, :since_id, :min_id)) end def insert_pagination_headers diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 843ca2ec2..4bbbed267 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController end def list_params - params.permit(:title, :replies_policy) + params.permit(:title, :replies_policy, :exclusive) end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 5891da6f6..205df48d4 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -11,15 +11,15 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] when :s3 - redirect_to @backup.dump.expiring_url(10) + redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? - redirect_to @backup.dump.expiring_url(Time.now.utc + 10) + redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true else - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end when :filesystem - redirect_to full_asset_url(@backup.dump.url) + redirect_to full_asset_url(@backup.dump.url), allow_other_host: true end end diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index bdbf8796f..983caf22f 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -12,6 +12,7 @@ class Settings::ImportsController < Settings::BaseController muting: 'muted_accounts_failures.csv', domain_blocking: 'blocked_domains_failures.csv', bookmarks: 'bookmarks_failures.csv', + lists: 'lists_failures.csv', }.freeze TYPE_TO_HEADERS_MAP = { @@ -20,6 +21,7 @@ class Settings::ImportsController < Settings::BaseController muting: ['Account address', 'Hide notifications'], domain_blocking: false, bookmarks: false, + lists: false, }.freeze def index @@ -49,6 +51,8 @@ class Settings::ImportsController < Settings::BaseController csv << [row.data['domain']] when :bookmarks csv << [row.data['uri']] + when :lists + csv << [row.data['list_name'], row.data['acct']] end end end diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index faf778a7e..c1f8b4989 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController end def user_params - params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys) + params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 014e3a3f9..3148756b7 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -170,11 +170,11 @@ module ApplicationHelper end def storage_host - URI::HTTPS.build(host: storage_host_name).to_s + "https://#{storage_host_var}" end def storage_host? - storage_host_name.present? + storage_host_var.present? end def quote_wrap(text, line_width: 80, break_sequence: "\n") @@ -235,7 +235,7 @@ module ApplicationHelper private - def storage_host_name + def storage_host_var ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) end end diff --git a/app/helpers/react_component_helper.rb b/app/helpers/react_component_helper.rb index fc08de13d..ce616e830 100644 --- a/app/helpers/react_component_helper.rb +++ b/app/helpers/react_component_helper.rb @@ -11,7 +11,7 @@ module ReactComponentHelper end def react_admin_component(name, props = {}) - data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) } + data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) } div_tag_with_data(data) end diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 3d5592867..ae89cec78 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -5,10 +5,6 @@ module SettingsHelper LanguagesHelper::SUPPORTED_LOCALES.keys end - def hash_to_object(hash) - HashObject.new(hash) - end - def session_device_icon(session) device = session.detection.device diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index f0d47f95e..540e6cba7 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -6,7 +6,7 @@ import { unescapeHTML } from 'flavours/glitch/utils/html'; const domParser = new DOMParser(); -const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { +const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => { obj[`:${emoji.shortcode}:`] = emoji; return obj; }, {}); @@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) { export function normalizeAccount(account) { account = { ...account }; - const emojiMap = makeEmojiMap(account); + const emojiMap = makeEmojiMap(account.emojis); const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); @@ -78,7 +78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) { } else { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus); + const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); @@ -89,22 +89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) { return normalStatus; } +export function normalizeStatusTranslation(translation, status) { + const emojiMap = makeEmojiMap(status.get('emojis').toJS()); + + const normalTranslation = { + detected_source_language: translation.detected_source_language, + language: translation.language, + provider: translation.provider, + contentHtml: emojify(translation.content, emojiMap), + spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + spoiler_text: translation.spoiler_text, + }; + + return normalTranslation; +} + export function normalizePoll(poll) { const normalPoll = { ...poll }; - const emojiMap = makeEmojiMap(normalPoll); + const emojiMap = makeEmojiMap(poll.emojis); normalPoll.options = poll.options.map((option, index) => ({ ...option, voted: poll.own_votes && poll.own_votes.includes(index), - title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), })); return normalPoll; } +export function normalizePollOptionTranslation(translation, poll) { + const emojiMap = makeEmojiMap(poll.get('emojis').toJS()); + + const normalTranslation = { + ...translation, + titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap), + }; + + return normalTranslation; +} + export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement); + const emojiMap = makeEmojiMap(normalAnnouncement.emojis); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 2faa54b95..b0789cd42 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -151,10 +151,10 @@ export const createListFail = error => ({ error, }); -export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => { +export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => { dispatch(updateListRequest(id)); - api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => { + api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => { dispatch(updateListSuccess(data)); if (shouldReset) { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index c201d6bde..f370905de 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -1,4 +1,4 @@ -import IntlMessageFormat from 'intl-messageformat'; +import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 9bc9bf387..5bdd31c34 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({ error, }); -export const undoStatusTranslation = id => ({ +export const undoStatusTranslation = (id, pollId) => ({ type: STATUS_TRANSLATE_UNDO, id, + pollId, }); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index e16a7409e..f1c44d2e2 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -1,6 +1,6 @@ // @ts-check -import { getLocale } from 'mastodon/locales'; +import { getLocale } from 'flavours/glitch/locales'; import { connectStream } from '../stream'; @@ -25,8 +25,6 @@ import { fillListTimelineGaps, } from './timelines'; -const { messages } = getLocale(); - /** * @param {number} max * @returns {number} @@ -44,8 +42,10 @@ const randomUpTo = max => * @param {function(object): boolean} [options.accept] * @returns {function(): void} */ -export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => - connectStream(channelName, params, (dispatch, getState) => { +export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => { + const { messages } = getLocale(); + + return connectStream(channelName, params, (dispatch, getState) => { const locale = getState().getIn(['meta', 'locale']); // @ts-expect-error @@ -122,6 +122,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch diff --git a/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx new file mode 100644 index 000000000..9ec1460fc --- /dev/null +++ b/app/javascript/flavours/glitch/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'flavours/glitch/api'; +import { Skeleton } from 'flavours/glitch/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +

+

+ + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
+ + + {loading ? : } +
+ + + {loading ? : } +
+ + + {loading ? : } +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/domain.tsx b/app/javascript/flavours/glitch/components/domain.tsx index d61e4074e..50c5c256e 100644 --- a/app/javascript/flavours/glitch/components/domain.tsx +++ b/app/javascript/flavours/glitch/components/domain.tsx @@ -1,8 +1,7 @@ import { useCallback } from 'react'; import * as React from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { IconButton } from './icon_button'; @@ -16,9 +15,11 @@ const messages = defineMessages({ interface Props { domain: string; onUnblockDomain: (domain: string) => void; - intl: InjectedIntl; } -const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); }, [domain, onUnblockDomain]); @@ -42,5 +43,3 @@ const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { ); }; - -export const Domain = injectIntl(_Domain); diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index 03bf6e685..5294244ec 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -121,10 +121,10 @@ class DropdownMenu extends PureComponent { return
  • ; } - const { text, href = '#', target = '_blank', method } = option; + const { text, href = '#', target = '_blank', method, dangerous } = option; return ( -
  • +
  • {text} diff --git a/app/javascript/flavours/glitch/components/load_gap.tsx b/app/javascript/flavours/glitch/components/load_gap.tsx index 52d701d5d..c86e70f9f 100644 --- a/app/javascript/flavours/glitch/components/load_gap.tsx +++ b/app/javascript/flavours/glitch/components/load_gap.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { injectIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; import { Icon } from 'flavours/glitch/components/icon'; @@ -13,10 +12,11 @@ interface Props { disabled: boolean; maxId: string; onClick: (maxId: string) => void; - intl: InjectedIntl; } -const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { +export const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + const handleClick = useCallback(() => { onClick(maxId); }, [maxId, onClick]); @@ -32,5 +32,3 @@ const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { ); }; - -export const LoadGap = injectIntl(_LoadGap); diff --git a/app/javascript/flavours/glitch/components/load_more.jsx b/app/javascript/flavours/glitch/components/load_more.jsx deleted file mode 100644 index 1cdf6836d..000000000 --- a/app/javascript/flavours/glitch/components/load_more.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -export default class LoadMore extends PureComponent { - - static propTypes = { - onClick: PropTypes.func, - disabled: PropTypes.bool, - visible: PropTypes.bool, - }; - - static defaultProps = { - visible: true, - }; - - render() { - const { disabled, visible } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/components/load_more.tsx b/app/javascript/flavours/glitch/components/load_more.tsx new file mode 100644 index 000000000..8b5746ad3 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; + visible?: boolean; +} +export const LoadMore: React.FC = ({ + onClick, + disabled, + visible = true, +}) => { + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/media_attachments.jsx b/app/javascript/flavours/glitch/components/media_attachments.jsx index 2c1c2831e..4e777437a 100644 --- a/app/javascript/flavours/glitch/components/media_attachments.jsx +++ b/app/javascript/flavours/glitch/components/media_attachments.jsx @@ -52,8 +52,9 @@ export default class MediaAttachments extends ImmutablePureComponent { }; render () { - const { status, lang, width, height, revealed } = this.props; + const { status, width, height, revealed } = this.props; const mediaAttachments = status.get('media_attachments'); + const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang; if (mediaAttachments.size === 0) { return null; @@ -61,14 +62,15 @@ export default class MediaAttachments extends ImmutablePureComponent { if (mediaAttachments.getIn([0, 'type']) === 'audio') { const audio = mediaAttachments.get(0); + const description = audio.getIn(['translation', 'description']) || audio.get('description'); return ( {Component => ( @@ -91,8 +94,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={video.get('description')} - lang={lang || status.get('language')} + alt={description} + lang={language} width={width} height={height} inline @@ -109,7 +112,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( ALT); } + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (attachment.get('type') === 'unknown') { return (
    - +
    diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 0f6f3330e..1b3aec552 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -34,7 +34,7 @@ const messages = defineMessages({ settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, explore: { id: 'navigation_bar.explore', defaultMessage: 'Explore' }, - direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' }, + direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx index 07d0447a8..52b3ad060 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/components/column_settings.jsx @@ -37,7 +37,7 @@ class ColumnSettings extends PureComponent {
    - } /> + } />
    diff --git a/app/javascript/flavours/glitch/features/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx index 554a3a0e8..4b002f2b1 100644 --- a/app/javascript/flavours/glitch/features/list_timeline/index.jsx +++ b/app/javascript/flavours/glitch/features/list_timeline/index.jsx @@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import Toggle from 'react-toggle'; + import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists'; import { openModal } from 'flavours/glitch/actions/modal'; @@ -145,7 +147,13 @@ class ListTimeline extends PureComponent { handleRepliesPolicyChange = ({ target }) => { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(updateList(id, undefined, false, target.value)); + dispatch(updateList(id, undefined, false, undefined, target.value)); + }; + + onExclusiveToggle = ({ target }) => { + const { dispatch } = this.props; + const { id } = this.props.params; + dispatch(updateList(id, undefined, false, target.checked, undefined)); }; render () { @@ -154,6 +162,7 @@ class ListTimeline extends PureComponent { const pinned = !!columnId; const title = list ? list.get('title') : id; const replies_policy = list ? list.get('replies_policy') : undefined; + const isExclusive = list ? list.get('exclusive') : undefined; if (typeof list === 'undefined') { return ( @@ -191,6 +200,13 @@ class ListTimeline extends PureComponent { +
    + + +
    + { replies_policy !== undefined && (
    diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx index 101df0413..300df8923 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx @@ -283,7 +283,7 @@ class LocalSettingsPage extends PureComponent { ), ({ intl, onChange, settings }) => (
    -

    +

    ); mediaIcons.push('video-camera'); @@ -166,12 +168,13 @@ class DetailedStatus extends ImmutablePureComponent { media.push(); } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { const attachment = status.getIn(['media_attachments', 0]); + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); media.push(
    -
    +
    - {!minimal && <> {verification} {muteTimeRemaining}} + {!minimal && ( +
    + {verification} {muteTimeRemaining} +
    + )}
    diff --git a/app/javascript/mastodon/components/admin/ImpactReport.jsx b/app/javascript/mastodon/components/admin/ImpactReport.jsx new file mode 100644 index 000000000..c27ee0ab0 --- /dev/null +++ b/app/javascript/mastodon/components/admin/ImpactReport.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { FormattedNumber, FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import api from 'mastodon/api'; +import { Skeleton } from 'mastodon/components/skeleton'; + +export default class ImpactReport extends PureComponent { + + static propTypes = { + domain: PropTypes.string.isRequired, + }; + + state = { + loading: true, + data: null, + }; + + componentDidMount () { + const { domain } = this.props; + + const params = { + domain: domain, + include_subdomains: true, + }; + + api().post('/api/v1/admin/measures', { + keys: ['instance_accounts', 'instance_follows', 'instance_followers'], + start_at: null, + end_at: null, + instance_accounts: params, + instance_follows: params, + instance_followers: params, + }).then(res => { + this.setState({ + loading: false, + data: res.data, + }); + }).catch(err => { + console.error(err); + }); + } + + render () { + const { loading, data } = this.state; + + return ( +
    +

    + + + + + + + + + + 0 })}> + + + + + + 0 })}> + + + + + +
    + + + {loading ? : } +
    + + + {loading ? : } +
    + + + {loading ? : } +
    +
    + ); + } + +} diff --git a/app/javascript/mastodon/components/domain.tsx b/app/javascript/mastodon/components/domain.tsx index db18635be..f4a3b9d4b 100644 --- a/app/javascript/mastodon/components/domain.tsx +++ b/app/javascript/mastodon/components/domain.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { IconButton } from './icon_button'; @@ -15,9 +14,11 @@ const messages = defineMessages({ interface Props { domain: string; onUnblockDomain: (domain: string) => void; - intl: InjectedIntl; } -const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => { + +export const Domain: React.FC = ({ domain, onUnblockDomain }) => { + const intl = useIntl(); + const handleDomainUnblock = useCallback(() => { onUnblockDomain(domain); }, [domain, onUnblockDomain]); @@ -41,5 +42,3 @@ const _Domain: React.FC = ({ domain, onUnblockDomain, intl }) => {
    ); }; - -export const Domain = injectIntl(_Domain); diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx index 4cadf907e..3cfa0ee12 100644 --- a/app/javascript/mastodon/components/dropdown_menu.jsx +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -121,10 +121,10 @@ class DropdownMenu extends PureComponent { return
  • ; } - const { text, href = '#', target = '_blank', method } = option; + const { text, href = '#', target = '_blank', method, dangerous } = option; return ( -
  • +
  • {text} diff --git a/app/javascript/mastodon/components/load_gap.tsx b/app/javascript/mastodon/components/load_gap.tsx index e6d3060eb..7e2cd447b 100644 --- a/app/javascript/mastodon/components/load_gap.tsx +++ b/app/javascript/mastodon/components/load_gap.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react'; -import type { InjectedIntl } from 'react-intl'; -import { injectIntl, defineMessages } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; import { Icon } from 'mastodon/components/icon'; @@ -13,10 +12,11 @@ interface Props { disabled: boolean; maxId: string; onClick: (maxId: string) => void; - intl: InjectedIntl; } -const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { +export const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { + const intl = useIntl(); + const handleClick = useCallback(() => { onClick(maxId); }, [maxId, onClick]); @@ -32,5 +32,3 @@ const _LoadGap: React.FC = ({ disabled, maxId, onClick, intl }) => { ); }; - -export const LoadGap = injectIntl(_LoadGap); diff --git a/app/javascript/mastodon/components/load_more.jsx b/app/javascript/mastodon/components/load_more.jsx deleted file mode 100644 index 6b7ecdea0..000000000 --- a/app/javascript/mastodon/components/load_more.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -export default class LoadMore extends PureComponent { - - static propTypes = { - onClick: PropTypes.func, - disabled: PropTypes.bool, - visible: PropTypes.bool, - }; - - static defaultProps = { - visible: true, - }; - - render() { - const { disabled, visible } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/mastodon/components/load_more.tsx b/app/javascript/mastodon/components/load_more.tsx new file mode 100644 index 000000000..8b5746ad3 --- /dev/null +++ b/app/javascript/mastodon/components/load_more.tsx @@ -0,0 +1,24 @@ +import { FormattedMessage } from 'react-intl'; + +interface Props { + onClick: (event: React.MouseEvent) => void; + disabled?: boolean; + visible?: boolean; +} +export const LoadMore: React.FC = ({ + onClick, + disabled, + visible = true, +}) => { + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/media_attachments.jsx b/app/javascript/mastodon/components/media_attachments.jsx index d2f171243..7b945a0ea 100644 --- a/app/javascript/mastodon/components/media_attachments.jsx +++ b/app/javascript/mastodon/components/media_attachments.jsx @@ -51,8 +51,9 @@ export default class MediaAttachments extends ImmutablePureComponent { }; render () { - const { status, lang, width, height } = this.props; + const { status, width, height } = this.props; const mediaAttachments = status.get('media_attachments'); + const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang; if (mediaAttachments.size === 0) { return null; @@ -60,14 +61,15 @@ export default class MediaAttachments extends ImmutablePureComponent { if (mediaAttachments.getIn([0, 'type']) === 'audio') { const audio = mediaAttachments.get(0); + const description = audio.getIn(['translation', 'description']) || audio.get('description'); return ( {Component => ( @@ -90,8 +93,8 @@ export default class MediaAttachments extends ImmutablePureComponent { frameRate={video.getIn(['meta', 'original', 'frame_rate'])} blurhash={video.get('blurhash')} src={video.get('url')} - alt={video.get('description')} - lang={lang || status.get('language')} + alt={description} + lang={language} width={width} height={height} inline @@ -107,7 +110,7 @@ export default class MediaAttachments extends ImmutablePureComponent { {Component => ( ALT); } + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + if (attachment.get('type') === 'unknown') { return (
    - +
    {mentionsPlaceholder} -
    +
    {!hidden && poll} - {!hidden && translateButton} + {translateButton}
    ); } else if (this.props.onClick) { return ( <>
    -
    +
    {poll} {translateButton} @@ -303,7 +303,7 @@ class StatusContent extends PureComponent { } else { return (
    -
    +
    {poll} {translateButton} diff --git a/app/javascript/mastodon/containers/admin_component.jsx b/app/javascript/mastodon/containers/admin_component.jsx index f5fa53f08..740011129 100644 --- a/app/javascript/mastodon/containers/admin_component.jsx +++ b/app/javascript/mastodon/containers/admin_component.jsx @@ -1,25 +1,19 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; - -import { getLocale } from '../locales'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); +import { IntlProvider } from 'mastodon/locales'; export default class AdminComponent extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, children: PropTypes.node.isRequired, }; render () { - const { locale, children } = this.props; + const { children } = this.props; return ( - + {children} ); diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index b93399aa9..f76550678 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -1,19 +1,14 @@ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; - import { Provider } from 'react-redux'; import { fetchCustomEmojis } from '../actions/custom_emojis'; import { hydrateStore } from '../actions/store'; import Compose from '../features/standalone/compose'; import initialState from '../initial_state'; -import { getLocale } from '../locales'; +import { IntlProvider } from '../locales'; import { store } from '../store'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -21,17 +16,11 @@ if (initialState) { store.dispatch(fetchCustomEmojis()); -export default class TimelineContainer extends PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - }; +export default class ComposeContainer extends PureComponent { render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 5be163f5a..4538db050 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -1,8 +1,6 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { IntlProvider, addLocaleData } from 'react-intl'; - import { Helmet } from 'react-helmet'; import { BrowserRouter, Route } from 'react-router-dom'; @@ -16,12 +14,9 @@ import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; import UI from 'mastodon/features/ui'; import initialState, { title as siteTitle } from 'mastodon/initial_state'; -import { getLocale } from 'mastodon/locales'; +import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; const hydrateAction = hydrateStore(initialState); @@ -41,10 +36,6 @@ const createIdentityContext = state => ({ export default class Mastodon extends PureComponent { - static propTypes = { - locale: PropTypes.string.isRequired, - }; - static childContextTypes = { identity: PropTypes.shape({ signedIn: PropTypes.bool.isRequired, @@ -80,10 +71,8 @@ export default class Mastodon extends PureComponent { } render () { - const { locale } = this.props; - return ( - + diff --git a/app/javascript/mastodon/containers/media_container.jsx b/app/javascript/mastodon/containers/media_container.jsx index 7ed8f1719..fba3c5df7 100644 --- a/app/javascript/mastodon/containers/media_container.jsx +++ b/app/javascript/mastodon/containers/media_container.jsx @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { createPortal } from 'react-dom'; -import { IntlProvider, addLocaleData } from 'react-intl'; - import { fromJS } from 'immutable'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; @@ -14,18 +12,14 @@ import Audio from 'mastodon/features/audio'; import Card from 'mastodon/features/status/components/card'; import MediaModal from 'mastodon/features/ui/components/media_modal'; import Video from 'mastodon/features/video'; -import { getLocale } from 'mastodon/locales'; +import { IntlProvider } from 'mastodon/locales'; import { getScrollbarWidth } from 'mastodon/utils/scrollbar'; -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; export default class MediaContainer extends PureComponent { static propTypes = { - locale: PropTypes.string.isRequired, components: PropTypes.object.isRequired, }; @@ -74,7 +68,7 @@ export default class MediaContainer extends PureComponent { }; render () { - const { locale, components } = this.props; + const { components } = this.props; let handleOpenVideo; @@ -84,7 +78,7 @@ export default class MediaContainer extends PureComponent { } return ( - + <> {[].map.call(components, (component, i) => { const componentName = component.getAttribute('data-component'); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index a00c03b37..5e3cc034c 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -190,7 +190,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ onTranslate (status) { if (status.get('translation')) { - dispatch(undoStatusTranslation(status.get('id'))); + dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); } else { dispatch(translateStatus(status.get('id'))); } diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 904148b81..09e9a7df1 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -332,16 +332,16 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'muting'])) { menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true }); } if (account.getIn(['relationship', 'blocking'])) { menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true }); } - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); } if (signedIn && isRemote) { @@ -350,7 +350,7 @@ class Header extends ImmutablePureComponent { if (account.getIn(['relationship', 'domain_blocking'])) { menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true }); } } diff --git a/app/javascript/mastodon/features/account_gallery/index.jsx b/app/javascript/mastodon/features/account_gallery/index.jsx index 27de4740c..653a25866 100644 --- a/app/javascript/mastodon/features/account_gallery/index.jsx +++ b/app/javascript/mastodon/features/account_gallery/index.jsx @@ -9,7 +9,7 @@ import { connect } from 'react-redux'; import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts'; import { openModal } from 'mastodon/actions/modal'; import ColumnBackButton from 'mastodon/components/column_back_button'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import ScrollContainer from 'mastodon/containers/scroll_container'; import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error'; diff --git a/app/javascript/mastodon/features/compose/components/search_results.jsx b/app/javascript/mastodon/features/compose/components/search_results.jsx index b329cae79..b11ac478a 100644 --- a/app/javascript/mastodon/features/compose/components/search_results.jsx +++ b/app/javascript/mastodon/features/compose/components/search_results.jsx @@ -6,7 +6,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Icon } from 'mastodon/components/icon'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import { ImmutableHashtag as Hashtag } from '../../../components/hashtag'; import AccountContainer from '../../../containers/account_container'; diff --git a/app/javascript/mastodon/features/directory/index.jsx b/app/javascript/mastodon/features/directory/index.jsx index d4854f186..635b6f411 100644 --- a/app/javascript/mastodon/features/directory/index.jsx +++ b/app/javascript/mastodon/features/directory/index.jsx @@ -13,7 +13,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodo import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import { RadioButton } from 'mastodon/components/radio_button'; import ScrollContainer from 'mastodon/containers/scroll_container'; diff --git a/app/javascript/mastodon/features/explore/index.jsx b/app/javascript/mastodon/features/explore/index.jsx index dbc0400e8..185db0732 100644 --- a/app/javascript/mastodon/features/explore/index.jsx +++ b/app/javascript/mastodon/features/explore/index.jsx @@ -67,7 +67,7 @@ class Explore extends PureComponent {
    -
    +
    {isSearching ? ( ) : ( diff --git a/app/javascript/mastodon/features/explore/results.jsx b/app/javascript/mastodon/features/explore/results.jsx index 6b053a9dc..dc1f72022 100644 --- a/app/javascript/mastodon/features/explore/results.jsx +++ b/app/javascript/mastodon/features/explore/results.jsx @@ -11,7 +11,7 @@ import { connect } from 'react-redux'; import { expandSearch } from 'mastodon/actions/search'; import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag'; -import LoadMore from 'mastodon/components/load_more'; +import { LoadMore } from 'mastodon/components/load_more'; import LoadingIndicator from 'mastodon/components/loading_indicator'; import Account from 'mastodon/containers/account_container'; import Status from 'mastodon/containers/status_container'; diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx index f41e8e6f2..f9f3a7c31 100644 --- a/app/javascript/mastodon/features/list_timeline/index.jsx +++ b/app/javascript/mastodon/features/list_timeline/index.jsx @@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import Toggle from 'react-toggle'; + import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { fetchList, deleteList, updateList } from 'mastodon/actions/lists'; import { openModal } from 'mastodon/actions/modal'; @@ -145,7 +147,13 @@ class ListTimeline extends PureComponent { handleRepliesPolicyChange = ({ target }) => { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(updateList(id, undefined, false, target.value)); + dispatch(updateList(id, undefined, false, undefined, target.value)); + }; + + onExclusiveToggle = ({ target }) => { + const { dispatch } = this.props; + const { id } = this.props.params; + dispatch(updateList(id, undefined, false, target.checked, undefined)); }; render () { @@ -154,6 +162,7 @@ class ListTimeline extends PureComponent { const pinned = !!columnId; const title = list ? list.get('title') : id; const replies_policy = list ? list.get('replies_policy') : undefined; + const isExclusive = list ? list.get('exclusive') : undefined; if (typeof list === 'undefined') { return ( @@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
    +
    + + +
    + { replies_policy !== undefined && (
    diff --git a/app/javascript/mastodon/features/onboarding/follows.jsx b/app/javascript/mastodon/features/onboarding/follows.jsx index 3807ce922..8b4ad0b08 100644 --- a/app/javascript/mastodon/features/onboarding/follows.jsx +++ b/app/javascript/mastodon/features/onboarding/follows.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; @@ -77,7 +77,7 @@ class Follows extends PureComponent { {loadedContent}
    -

    +

    {chunks} }} />

    diff --git a/app/javascript/mastodon/features/onboarding/index.jsx b/app/javascript/mastodon/features/onboarding/index.jsx index cc0c79790..f1447b771 100644 --- a/app/javascript/mastodon/features/onboarding/index.jsx +++ b/app/javascript/mastodon/features/onboarding/index.jsx @@ -121,7 +121,7 @@ class Onboarding extends ImmutablePureComponent {
    0 && account.get('note').length > 0)} icon='address-book-o' label={} description={} /> - = 7} icon='user-plus' label={} description={} /> + = 7} icon='user-plus' label={} description={} /> = 1} icon='pencil-square-o' label={} description={} /> } description={} />
    diff --git a/app/javascript/mastodon/features/onboarding/share.jsx b/app/javascript/mastodon/features/onboarding/share.jsx index 1895af912..687179302 100644 --- a/app/javascript/mastodon/features/onboarding/share.jsx +++ b/app/javascript/mastodon/features/onboarding/share.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -168,9 +168,9 @@ class Share extends PureComponent { -

    -

    -

    +

    {chunks} }} />

    +

    {chunks} }} />

    +

    {chunks} }} />

    diff --git a/app/javascript/mastodon/features/report/category.jsx b/app/javascript/mastodon/features/report/category.jsx index a6e817c73..fb9e55c57 100644 --- a/app/javascript/mastodon/features/report/category.jsx +++ b/app/javascript/mastodon/features/report/category.jsx @@ -16,6 +16,8 @@ const messages = defineMessages({ dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' }, spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' }, spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' }, + legal: { id: 'report.reasons.legal', defaultMessage: 'It\'s illegal' }, + legal_description: { id: 'report.reasons.legal_description', defaultMessage: 'You believe it violates the law of your or the server\'s country' }, violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' }, violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' }, other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' }, @@ -69,11 +71,13 @@ class Category extends PureComponent { const options = rules.size > 0 ? [ 'dislike', 'spam', + 'legal', 'violation', 'other', ] : [ 'dislike', 'spam', + 'legal', 'other', ]; diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 9c2fd90e7..3e69c3c04 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -229,8 +229,8 @@ class ActionBar extends PureComponent { menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push(null); menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + 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: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push(null); @@ -238,16 +238,16 @@ class ActionBar extends PureComponent { 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.mute, { name: account.get('username') }), action: this.handleMuteClick }); + 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 }); + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true }); } - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true }); if (account.get('acct') !== account.get('username')) { const domain = account.get('acct').split('@')[1]; @@ -257,7 +257,7 @@ class ActionBar extends PureComponent { 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 }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true }); } } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index cb0b70e1d..55db48276 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -137,17 +137,20 @@ class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } + const language = status.getIn(['translation', 'language']) || status.get('language'); + if (pictureInPicture.get('inUse')) { media = ; } else if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { const attachment = status.getIn(['media_attachments', 0]); + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); media = (