diff --git a/.eslintrc.js b/.eslintrc.js index 8bc6e08951..206faa1c7a 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 0000000000..1ae40d4161 --- /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 e282e2ab72..b67c503e95 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 e13d227bdb..4d3c2ce5af 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 2ddbca7818..56d817123a 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 7700e48512..1f0cfd1e70 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 98f101ad95..8712d8bd80 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 6f76dd60c2..d19a0470db 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 de54fe9ae5..0395c8639f 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 6f79babcfd..295e9610b3 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 6a8035210c..131a62a576 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 1c4958550e..3306105f9e 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 212b2cfe73..a91fd819a2 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 310153929d..50266fb8a0 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 f284745ea4..07cb1d41f8 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 12ca463422..d1ed30b260 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 af0411e9cc..27b6d5458a 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 bd561df1d2..eff89bdaee 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 2cb7bd0e5c..4964cf856c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -239,79 +239,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: @@ -468,30 +395,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 @@ -1336,11 +1239,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: @@ -1474,7 +1372,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' @@ -1488,52 +1385,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 51308c24db..84f210f481 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 36f7d72012..c4fa1822ea 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) @@ -894,3 +882,9 @@ DEPENDENCIES webpacker (~> 5.4) webpush! xorcist (~> 1.1) + +RUBY VERSION + ruby 3.2.2p53 + +BUNDLED WITH + 2.4.13 diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 099512248f..3a6df662ea 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 081550b762..b9691c5a3a 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 1ed3fd18ab..01d9ba8ce2 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 8af4242ba3..ddb94d5ca4 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 9034e8a2f4..c55500f761 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 843ca2ec2b..4bbbed2673 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 5891da6f6d..205df48d44 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 bdbf8796fe..983caf22fa 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 faf778a7e5..c1f8b49898 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 014e3a3f96..3148756b75 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 fc08de13dd..ce616e8306 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 3d5592867c..ae89cec780 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 f0d47f95e2..540e6cba78 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 2faa54b955..b0789cd426 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 47d45d0012..a80746b756 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 9bc9bf3876..5bdd31c343 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 e16a7409e6..f1c44d2e29 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 0000000000..9ec1460fcf --- /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 d61e4074ee..50c5c256ec 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 03bf6e685a..5294244ece 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 52d701d5d5..c86e70f9fc 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 1cdf6836d2..0000000000 --- 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 0000000000..8b5746ad30 --- /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 2c1c2831eb..4e777437a0 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/list_timeline/index.jsx b/app/javascript/flavours/glitch/features/list_timeline/index.jsx index 554a3a0e89..4b002f2b14 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 5badc90791..929391927f 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx @@ -275,7 +275,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(
    @@ -297,7 +302,7 @@ class DetailedStatus extends ImmutablePureComponent { if (status.get('edited_at')) { edited = ( <> - · + {' · '} ); diff --git a/app/javascript/flavours/glitch/features/status/index.jsx b/app/javascript/flavours/glitch/features/status/index.jsx index d961a0d980..2b451a3b03 100644 --- a/app/javascript/flavours/glitch/features/status/index.jsx +++ b/app/javascript/flavours/glitch/features/status/index.jsx @@ -66,7 +66,7 @@ const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, - redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' }, @@ -481,7 +481,7 @@ class Status extends ImmutablePureComponent { const { dispatch } = this.props; 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/flavours/glitch/features/ui/components/audio_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx index 5c15642390..a6e15e5439 100644 --- a/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx @@ -8,7 +8,7 @@ import Audio from 'flavours/glitch/features/audio'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer'; const mapStateToProps = (state, { statusId }) => ({ - language: state.getIn(['statuses', statusId, 'language']), + status: state.getIn(['statuses', statusId]), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), }); @@ -17,7 +17,7 @@ class AudioModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, statusId: PropTypes.string.isRequired, - language: PropTypes.string, + status: ImmutablePropTypes.map.isRequired, accountStaticAvatar: PropTypes.string.isRequired, options: PropTypes.shape({ autoPlay: PropTypes.bool, @@ -31,15 +31,17 @@ class AudioModal extends ImmutablePureComponent { }; render () { - const { media, language, accountStaticAvatar, statusId, onClose } = this.props; + const { media, status, accountStaticAvatar, onClose } = this.props; const options = this.props.options || {}; + const language = status.getIn(['translation', 'language']) || status.get('language'); + const description = media.getIn(['translation', 'description']) || media.get('description'); return (
    ); diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx index ed70814093..344f97fc00 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.jsx @@ -147,6 +147,7 @@ class MediaModal extends ImmutablePureComponent { const content = media.map((image) => { const width = image.getIn(['meta', 'original', 'width']) || null; const height = image.getIn(['meta', 'original', 'height']) || null; + const description = image.getIn(['translation', 'description']) || image.get('description'); if (image.get('type') === 'image') { return ( @@ -155,7 +156,7 @@ class MediaModal extends ImmutablePureComponent { src={image.get('url')} width={width} height={height} - alt={image.get('description')} + alt={description} lang={lang} key={image.get('url')} onClick={this.toggleNavigation} @@ -178,7 +179,7 @@ class MediaModal extends ImmutablePureComponent { volume={volume || 1} onCloseVideo={onClose} detailed - alt={image.get('description')} + alt={description} lang={lang} key={image.get('url')} /> @@ -190,7 +191,7 @@ class MediaModal extends ImmutablePureComponent { width={width} height={height} key={image.get('url')} - alt={image.get('description')} + alt={description} lang={lang} onClick={this.toggleNavigation} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/video_modal.jsx index 8f0b7254dc..489e26a909 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.jsx @@ -9,7 +9,7 @@ import Footer from 'flavours/glitch/features/picture_in_picture/components/foote import Video from 'flavours/glitch/features/video'; const mapStateToProps = (state, { statusId }) => ({ - language: state.getIn(['statuses', statusId, 'language']), + status: state.getIn(['statuses', statusId]), }); class VideoModal extends ImmutablePureComponent { @@ -17,7 +17,7 @@ class VideoModal extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, statusId: PropTypes.string, - language: PropTypes.string, + status: ImmutablePropTypes.map, options: PropTypes.shape({ startTime: PropTypes.number, autoPlay: PropTypes.bool, @@ -38,8 +38,10 @@ class VideoModal extends ImmutablePureComponent { } render () { - const { media, statusId, language, onClose } = this.props; + const { media, status, onClose } = this.props; const options = this.props.options || {}; + const language = status.getIn(['translation', 'language']) || status.get('language'); + const description = media.getIn(['translation', 'description']) || media.get('description'); return (
    @@ -55,13 +57,13 @@ class VideoModal extends ImmutablePureComponent { onCloseVideo={onClose} autoFocus detailed - alt={media.get('description')} + alt={description} lang={language} />
    - {statusId &&
    } + {status &&
    }
    ); diff --git a/app/javascript/flavours/glitch/locales/defaultMessages.json b/app/javascript/flavours/glitch/locales/defaultMessages.json deleted file mode 100644 index bf21fdef2d..0000000000 --- a/app/javascript/flavours/glitch/locales/defaultMessages.json +++ /dev/null @@ -1,1114 +0,0 @@ -[ - { - "descriptors": [ - { - "defaultMessage": "We're sorry, but something went wrong with the Mastodon app.", - "id": "web_app_crash.title" - }, - { - "defaultMessage": "You could try any of the following:", - "id": "web_app_crash.content" - }, - { - "defaultMessage": "Disable browser add-ons or built-in translation tools", - "id": "web_app_crash.disable_addons" - }, - { - "defaultMessage": "Report a bug in the {issuetracker}", - "id": "web_app_crash.report_issue" - }, - { - "defaultMessage": "issue tracker", - "id": "web_app_crash.issue_tracker" - }, - { - "defaultMessage": "Debug information", - "id": "web_app_crash.debug_info" - }, - { - "defaultMessage": "{reload} the current page", - "id": "web_app_crash.reload_page" - }, - { - "defaultMessage": "Reload", - "id": "web_app_crash.reload" - }, - { - "defaultMessage": "Change your {settings}", - "id": "web_app_crash.change_your_settings" - }, - { - "defaultMessage": "settings", - "id": "web_app_crash.settings" - } - ], - "path": "app/javascript/flavours/glitch/components/error_boundary.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Sensitive", - "id": "media_gallery.sensitive" - }, - { - "defaultMessage": "Click to view", - "id": "status.sensitive_toggle" - } - ], - "path": "app/javascript/flavours/glitch/components/media_gallery.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Select\nall", - "id": "notification_purge.btn_all" - }, - { - "defaultMessage": "Select\nnone", - "id": "notification_purge.btn_none" - }, - { - "defaultMessage": "Invert\nselection", - "id": "notification_purge.btn_invert" - }, - { - "defaultMessage": "Clear\nselected", - "id": "notification_purge.btn_apply" - } - ], - "path": "app/javascript/flavours/glitch/components/notification_purge_buttons.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Collapse", - "id": "status.collapse" - }, - { - "defaultMessage": "Uncollapse", - "id": "status.uncollapse" - }, - { - "defaultMessage": "This toot is a reply", - "id": "status.in_reply_to" - }, - { - "defaultMessage": "Features an attached preview card", - "id": "status.has_preview_card" - }, - { - "defaultMessage": "Features attached pictures", - "id": "status.has_pictures" - }, - { - "defaultMessage": "This toot is a poll", - "id": "status.is_poll" - }, - { - "defaultMessage": "Features attached videos", - "id": "status.has_video" - }, - { - "defaultMessage": "Features attached audio files", - "id": "status.has_audio" - }, - { - "defaultMessage": "Only visible from your instance", - "id": "status.local_only" - } - ], - "path": "app/javascript/flavours/glitch/components/status_icons.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Are you sure you want to permanently clear all selected notifications?", - "id": "notifications.marked_clear_confirmation" - }, - { - "defaultMessage": "Clear selected notifications", - "id": "notifications.marked_clear" - } - ], - "path": "app/javascript/flavours/glitch/containers/notification_purge_buttons_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Show", - "id": "confirmations.unfilter.confirm" - }, - { - "defaultMessage": "Author", - "id": "confirmations.unfilter.author" - }, - { - "defaultMessage": "Matching {count, plural, one {filter} other {filters}}", - "id": "confirmations.unfilter.filters" - }, - { - "defaultMessage": "Edit filter", - "id": "confirmations.unfilter.edit_filter" - } - ], - "path": "app/javascript/flavours/glitch/containers/status_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Glitch-soc is free open source software forked from Mastodon.", - "id": "about.fork_disclaimer" - } - ], - "path": "app/javascript/flavours/glitch/features/about/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "No comment provided", - "id": "account_note.glitch_placeholder" - }, - { - "defaultMessage": "Cancel", - "id": "account_note.cancel" - }, - { - "defaultMessage": "Save", - "id": "account_note.save" - }, - { - "defaultMessage": "Edit", - "id": "account_note.edit" - } - ], - "path": "app/javascript/flavours/glitch/features/account/components/account_note.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This user has been suspended by a moderator.", - "id": "account.suspended_disclaimer_full" - }, - { - "defaultMessage": "Information below may reflect the user's profile incompletely.", - "id": "account.disclaimer_full" - }, - { - "defaultMessage": "View full profile", - "id": "account.view_full_profile" - }, - { - "defaultMessage": "Follows", - "id": "account.follows" - } - ], - "path": "app/javascript/flavours/glitch/features/account/components/action_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Add note for @{name}", - "id": "account.add_account_note" - }, - { - "defaultMessage": "Joined {date}", - "id": "account.joined" - } - ], - "path": "app/javascript/flavours/glitch/features/account/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Profile", - "id": "column_header.profile" - } - ], - "path": "app/javascript/flavours/glitch/features/account/components/profile_column_header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Filter out by regular expressions", - "id": "home.column_settings.filter_regex" - }, - { - "defaultMessage": "Column settings", - "id": "home.settings" - }, - { - "defaultMessage": "Advanced", - "id": "home.column_settings.advanced" - } - ], - "path": "app/javascript/flavours/glitch/features/community_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.", - "id": "confirmations.missing_media_description.message" - }, - { - "defaultMessage": "Send anyway", - "id": "confirmations.missing_media_description.confirm" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/compose_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "App settings", - "id": "navigation_bar.app_settings" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Advanced options", - "id": "advanced_options.icon_title" - }, - { - "defaultMessage": "Attach...", - "id": "compose.attach" - }, - { - "defaultMessage": "Content type", - "id": "content-type.change" - }, - { - "defaultMessage": "Draw something", - "id": "compose.attach.doodle" - }, - { - "defaultMessage": "HTML", - "id": "compose.content-type.html" - }, - { - "defaultMessage": "Do not post to other instances", - "id": "advanced_options.local-only.long" - }, - { - "defaultMessage": "Local-only", - "id": "advanced_options.local-only.short" - }, - { - "defaultMessage": "Markdown", - "id": "compose.content-type.markdown" - }, - { - "defaultMessage": "Plain text", - "id": "compose.content-type.plain" - }, - { - "defaultMessage": "Hide text behind warning", - "id": "compose_form.spoiler" - }, - { - "defaultMessage": "Automatically opens a reply on posting", - "id": "advanced_options.threaded_mode.long" - }, - { - "defaultMessage": "Threaded mode", - "id": "advanced_options.threaded_mode.short" - }, - { - "defaultMessage": "Upload a file", - "id": "compose.attach.upload" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/options.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Allow one choice", - "id": "compose_form.poll.single_choice" - }, - { - "defaultMessage": "Allow multiple choices", - "id": "compose_form.poll.multiple_choices" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/poll_form.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", - "id": "search_popout.tips.full_text" - }, - { - "defaultMessage": "Simple text returns matching display names, usernames and hashtags", - "id": "search_popout.tips.text" - }, - { - "defaultMessage": "Advanced search format", - "id": "search_popout.search_format" - }, - { - "defaultMessage": "hashtag", - "id": "search_popout.tips.hashtag" - }, - { - "defaultMessage": "user", - "id": "search_popout.tips.user" - }, - { - "defaultMessage": "status", - "id": "search_popout.tips.status" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/search.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This post is local-only", - "id": "advanced_options.local-only.tooltip" - }, - { - "defaultMessage": "Threaded mode enabled", - "id": "advanced_options.threaded_mode.tooltip" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/components/textarea_icons.json" - }, - { - "descriptors": [ - { - "defaultMessage": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.", - "id": "confirmations.missing_media_description.message" - }, - { - "defaultMessage": "Send anyway", - "id": "confirmations.missing_media_description.confirm" - }, - { - "defaultMessage": "Edit media", - "id": "confirmations.missing_media_description.edit" - } - ], - "path": "app/javascript/flavours/glitch/features/compose/containers/compose_form_container.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Filter out by regular expressions", - "id": "home.column_settings.filter_regex" - }, - { - "defaultMessage": "Column settings", - "id": "home.settings" - }, - { - "defaultMessage": "Group by conversation", - "id": "direct.group_by_conversations" - }, - { - "defaultMessage": "Advanced", - "id": "home.column_settings.advanced" - } - ], - "path": "app/javascript/flavours/glitch/features/direct_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Favourited by", - "id": "column.favourited_by" - } - ], - "path": "app/javascript/flavours/glitch/features/favourites/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Follow people you'd like to see posts from! Here are some suggestions.", - "id": "follow_recommendations.heading" - }, - { - "defaultMessage": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", - "id": "follow_recommendations.lead" - }, - { - "defaultMessage": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", - "id": "empty_column.follow_recommendations" - }, - { - "defaultMessage": "Done", - "id": "follow_recommendations.done" - } - ], - "path": "app/javascript/flavours/glitch/features/follow_recommendations/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Misc", - "id": "column.heading" - }, - { - "defaultMessage": "Miscellaneous options", - "id": "column.subheading" - }, - { - "defaultMessage": "Show me around", - "id": "getting_started.onboarding" - }, - { - "defaultMessage": "Keyboard shortcuts", - "id": "navigation_bar.keyboard_shortcuts" - }, - { - "defaultMessage": "Featured users", - "id": "navigation_bar.featured_users" - } - ], - "path": "app/javascript/flavours/glitch/features/getting_started_misc/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Navigation", - "id": "column_subheading.navigation" - }, - { - "defaultMessage": "App settings", - "id": "navigation_bar.app_settings" - }, - { - "defaultMessage": "Keyboard shortcuts", - "id": "navigation_bar.keyboard_shortcuts" - }, - { - "defaultMessage": "Lists", - "id": "column_subheading.lists" - }, - { - "defaultMessage": "Misc", - "id": "navigation_bar.misc" - } - ], - "path": "app/javascript/flavours/glitch/features/getting_started/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Filter out by regular expressions", - "id": "home.column_settings.filter_regex" - }, - { - "defaultMessage": "Column settings", - "id": "home.settings" - }, - { - "defaultMessage": "Show DMs", - "id": "home.column_settings.show_direct" - }, - { - "defaultMessage": "Advanced", - "id": "home.column_settings.advanced" - } - ], - "path": "app/javascript/flavours/glitch/features/home_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "to bookmark", - "id": "keyboard_shortcuts.bookmark" - }, - { - "defaultMessage": "to collapse/uncollapse toots", - "id": "keyboard_shortcuts.toggle_collapse" - }, - { - "defaultMessage": "to send toot using secondary privacy setting", - "id": "keyboard_shortcuts.secondary_toot" - } - ], - "path": "app/javascript/flavours/glitch/features/keyboard_shortcuts/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "General", - "id": "settings.general" - }, - { - "defaultMessage": "Compose box", - "id": "settings.compose_box_opts" - }, - { - "defaultMessage": "Content Warnings", - "id": "settings.content_warnings" - }, - { - "defaultMessage": "Collapsed toots", - "id": "settings.collapsed_statuses" - }, - { - "defaultMessage": "Media", - "id": "settings.media" - }, - { - "defaultMessage": "Preferences", - "id": "settings.preferences" - }, - { - "defaultMessage": "Close", - "id": "settings.close" - } - ], - "path": "app/javascript/flavours/glitch/features/local_settings/navigation/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Auto", - "id": "layout.auto" - }, - { - "defaultMessage": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.", - "id": "layout.hint.auto" - }, - { - "defaultMessage": "Desktop", - "id": "layout.desktop" - }, - { - "defaultMessage": "Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.", - "id": "layout.hint.desktop" - }, - { - "defaultMessage": "Mobile", - "id": "layout.single" - }, - { - "defaultMessage": "Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.", - "id": "layout.hint.single" - }, - { - "defaultMessage": "None", - "id": "settings.side_arm.none" - }, - { - "defaultMessage": "Keep its set privacy", - "id": "settings.side_arm_reply_mode.keep" - }, - { - "defaultMessage": "Copy privacy setting of the toot being replied to", - "id": "settings.side_arm_reply_mode.copy" - }, - { - "defaultMessage": "Restrict privacy setting to that of the toot being replied to", - "id": "settings.side_arm_reply_mode.restrict" - }, - { - "defaultMessage": "Regular expression", - "id": "settings.content_warnings.regexp" - }, - { - "defaultMessage": "Do not rewrite mentions", - "id": "settings.rewrite_mentions_no" - }, - { - "defaultMessage": "Rewrite with username and domain (when the account is remote)", - "id": "settings.rewrite_mentions_acct" - }, - { - "defaultMessage": "Rewrite with username", - "id": "settings.rewrite_mentions_username" - }, - { - "defaultMessage": "Left", - "id": "settings.pop_in_left" - }, - { - "defaultMessage": "Right", - "id": "settings.pop_in_right" - }, - { - "defaultMessage": "General", - "id": "settings.general" - }, - { - "defaultMessage": "Display an estimate of the reply count", - "id": "settings.show_reply_counter" - }, - { - "defaultMessage": "High color privacy icons", - "id": "settings.hicolor_privacy_icons" - }, - { - "defaultMessage": "Display privacy icons in bright and easily distinguishable colors", - "id": "settings.hicolor_privacy_icons.hint" - }, - { - "defaultMessage": "Show confirmation dialog before boosting toots lacking media descriptions", - "id": "settings.confirm_boost_missing_media_description" - }, - { - "defaultMessage": "Tag misleading links", - "id": "settings.tag_misleading_links" - }, - { - "defaultMessage": "Add a visual indication with the link target host to every link not mentioning it explicitly", - "id": "settings.tag_misleading_links.hint" - }, - { - "defaultMessage": "Rewrite mentions in displayed statuses", - "id": "settings.rewrite_mentions" - }, - { - "defaultMessage": "Notifications options", - "id": "settings.notifications_opts" - }, - { - "defaultMessage": "Unread notifications badge", - "id": "settings.notifications.tab_badge" - }, - { - "defaultMessage": "Display a badge for unread notifications in the column icons when the notifications column isn't open", - "id": "settings.notifications.tab_badge.hint" - }, - { - "defaultMessage": "Unread notifications favicon badge", - "id": "settings.notifications.favicon_badge" - }, - { - "defaultMessage": "Add a badge for unread notifications to the favicon", - "id": "settings.notifications.favicon_badge.hint" - }, - { - "defaultMessage": "Toot icons", - "id": "settings.status_icons" - }, - { - "defaultMessage": "Language indicator", - "id": "settings.status_icons_language" - }, - { - "defaultMessage": "Reply indicator", - "id": "settings.status_icons_reply" - }, - { - "defaultMessage": "Local-only indicator", - "id": "settings.status_icons_local_only" - }, - { - "defaultMessage": "Media and poll indicators", - "id": "settings.status_icons_media" - }, - { - "defaultMessage": "Toot privacy indicator", - "id": "settings.status_icons_visibility" - }, - { - "defaultMessage": "Layout options", - "id": "settings.layout_opts" - }, - { - "defaultMessage": "Layout:", - "id": "settings.layout" - }, - { - "defaultMessage": "Wide view (Desktop mode only)", - "id": "settings.wide_view" - }, - { - "defaultMessage": "Stretches columns to better fill the available space.", - "id": "settings.wide_view_hint" - }, - { - "defaultMessage": "Compose box", - "id": "settings.compose_box_opts" - }, - { - "defaultMessage": "Always enable the Content Warning field", - "id": "settings.always_show_spoilers_field" - }, - { - "defaultMessage": "Prepend “re: ” to content warnings when replying", - "id": "settings.prepend_cw_re" - }, - { - "defaultMessage": "Pre-select usernames on reply", - "id": "settings.preselect_on_reply" - }, - { - "defaultMessage": "When replying to a conversation with multiple participants, pre-select usernames past the first", - "id": "settings.preselect_on_reply_hint" - }, - { - "defaultMessage": "Show confirmation dialog before sending toots lacking media descriptions", - "id": "settings.confirm_missing_media_description" - }, - { - "defaultMessage": "Show confirmation dialog before overwriting the message being composed", - "id": "settings.confirm_before_clearing_draft" - }, - { - "defaultMessage": "Show content-type choice when authoring toots", - "id": "settings.show_content_type_choice" - }, - { - "defaultMessage": "Secondary toot button:", - "id": "settings.side_arm" - }, - { - "defaultMessage": "When replying to a toot, the secondary toot button should:", - "id": "settings.side_arm_reply_mode" - }, - { - "defaultMessage": "Content warnings", - "id": "settings.content_warnings" - }, - { - "defaultMessage": "Show/hide content of all copies at once", - "id": "settings.content_warnings_shared_state" - }, - { - "defaultMessage": "Reproduce upstream Mastodon behavior by having the Content Warning button affect all copies of a post at once. This will prevent automatic collapsing of any copy of a toot with unfolded CW", - "id": "settings.content_warnings_shared_state_hint" - }, - { - "defaultMessage": "Display media attachments outside content warnings", - "id": "settings.content_warnings_media_outside" - }, - { - "defaultMessage": "Reproduce upstream Mastodon behavior by having the Content Warning toggle not affect media attachments", - "id": "settings.content_warnings_media_outside_hint" - }, - { - "defaultMessage": "Auto-unfolding options", - "id": "settings.content_warnings_unfold_opts" - }, - { - "defaultMessage": "Automatically unfold content-warnings", - "id": "settings.enable_content_warnings_auto_unfold" - }, - { - "defaultMessage": "This setting is now controlled from Mastodon's {settings_page_link}", - "id": "settings.deprecated_setting" - }, - { - "defaultMessage": "user preferences", - "id": "settings.shared_settings_link" - }, - { - "defaultMessage": "Content warnings to not automatically unfold:", - "id": "settings.content_warnings_filter" - }, - { - "defaultMessage": "Collapsed toots", - "id": "settings.collapsed_statuses" - }, - { - "defaultMessage": "Enable collapsed toots", - "id": "settings.enable_collapsed" - }, - { - "defaultMessage": "Collapsed posts have parts of their contents hidden to take up less screen space. This is distinct from the Content Warning feature", - "id": "settings.enable_collapsed_hint" - }, - { - "defaultMessage": "Show action buttons in collapsed toots", - "id": "settings.show_action_bar" - }, - { - "defaultMessage": "Automatic collapsing", - "id": "settings.auto_collapse" - }, - { - "defaultMessage": "Everything", - "id": "settings.auto_collapse_all" - }, - { - "defaultMessage": "Notifications", - "id": "settings.auto_collapse_notifications" - }, - { - "defaultMessage": "Lengthy toots", - "id": "settings.auto_collapse_lengthy" - }, - { - "defaultMessage": "Boosts", - "id": "settings.auto_collapse_reblogs" - }, - { - "defaultMessage": "Replies", - "id": "settings.auto_collapse_replies" - }, - { - "defaultMessage": "Toots with media", - "id": "settings.auto_collapse_media" - }, - { - "defaultMessage": "Height (in pixels) for a toot to be considered lengthy", - "id": "settings.auto_collapse_height" - }, - { - "defaultMessage": "Image backgrounds", - "id": "settings.image_backgrounds" - }, - { - "defaultMessage": "Give collapsed toots an image background", - "id": "settings.image_backgrounds_users" - }, - { - "defaultMessage": "Preview collapsed toot media", - "id": "settings.image_backgrounds_media" - }, - { - "defaultMessage": "If the post has any media attachment, use the first one as a background", - "id": "settings.image_backgrounds_media_hint" - }, - { - "defaultMessage": "Media", - "id": "settings.media" - }, - { - "defaultMessage": "Letterbox media", - "id": "settings.media_letterbox" - }, - { - "defaultMessage": "Scale down and letterbox media to fill the image containers instead of stretching and cropping them", - "id": "settings.media_letterbox_hint" - }, - { - "defaultMessage": "Full-width media previews", - "id": "settings.media_fullwidth" - }, - { - "defaultMessage": "Inline preview cards for external links", - "id": "settings.inline_preview_cards" - }, - { - "defaultMessage": "Reveal sensitive media behind a CW by default", - "id": "settings.media_reveal_behind_cw" - }, - { - "defaultMessage": "Enable pop-in player", - "id": "settings.pop_in_player" - }, - { - "defaultMessage": "Pop-in player position:", - "id": "settings.pop_in_position" - } - ], - "path": "app/javascript/flavours/glitch/features/local_settings/page/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Mark for deletion", - "id": "notification.markForDeletion" - } - ], - "path": "app/javascript/flavours/glitch/features/notifications/components/overlay.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Enter notification cleaning mode", - "id": "notification_purge.start" - } - ], - "path": "app/javascript/flavours/glitch/features/notifications/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Featured accounts", - "id": "endorsed_accounts_editor.endorsed_accounts" - } - ], - "path": "app/javascript/flavours/glitch/features/pinned_accounts_editor/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Filter out by regular expressions", - "id": "home.column_settings.filter_regex" - }, - { - "defaultMessage": "Show local-only toots", - "id": "community.column_settings.allow_local_only" - }, - { - "defaultMessage": "Advanced", - "id": "home.column_settings.advanced" - } - ], - "path": "app/javascript/flavours/glitch/features/public_timeline/components/column_settings.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Boosted by", - "id": "column.reblogged_by" - } - ], - "path": "app/javascript/flavours/glitch/features/reblogs/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This toot contains some media without description", - "id": "boost_modal.missing_description" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/boost_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Do not ask for confirmation again", - "id": "confirmation_modal.do_not_ask_again" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/confirmation_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Use Mastodon preferences", - "id": "confirmations.deprecated_settings.confirm" - }, - { - "defaultMessage": "Automatically unfold content-warnings", - "id": "settings.enable_content_warnings_auto_unfold" - }, - { - "defaultMessage": "Allow swiping to change columns (Mobile only)", - "id": "settings.swipe_to_change_columns" - }, - { - "defaultMessage": "Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:", - "id": "confirmations.deprecated_settings.message" - }, - { - "defaultMessage": "App settings", - "id": "navigation_bar.app_settings" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/deprecated_settings_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "You can press {combo} to skip this next time", - "id": "favourite_modal.combo" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/favourite_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "App settings", - "id": "navigation_bar.app_settings" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/navigation_panel.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Welcome to {domain}!", - "id": "onboarding.page_one.welcome" - }, - { - "defaultMessage": "{domain} is an \"instance\" of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.", - "id": "onboarding.page_one.federation" - }, - { - "defaultMessage": "You are on {domain}, so your full handle is {handle}", - "id": "onboarding.page_one.handle" - }, - { - "defaultMessage": "Write posts from the compose column. You can upload images, change privacy settings, and add content warnings with the icons below.", - "id": "onboarding.page_two.compose" - }, - { - "defaultMessage": "Use the search bar to find people and look at hashtags, such as {illustration} and {introductions}. To look for a person who is not on this instance, use their full handle.", - "id": "onboarding.page_three.search" - }, - { - "defaultMessage": "Edit your profile to change your avatar, bio, and display name. There, you will also find other preferences.", - "id": "onboarding.page_three.profile" - }, - { - "defaultMessage": "The home timeline shows posts from people you follow.", - "id": "onboarding.page_four.home" - }, - { - "defaultMessage": "The notifications column shows when someone interacts with you.", - "id": "onboarding.page_four.notifications" - }, - { - "defaultMessage": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", - "id": "onboarding.page_five.public_timelines" - }, - { - "defaultMessage": "Your instance's admin is {admin}.", - "id": "onboarding.page_six.admin" - }, - { - "defaultMessage": "Please read {domain}'s {guidelines}!", - "id": "onboarding.page_six.read_guidelines" - }, - { - "defaultMessage": "community guidelines", - "id": "onboarding.page_six.guidelines" - }, - { - "defaultMessage": "Almost done...", - "id": "onboarding.page_six.almost_done" - }, - { - "defaultMessage": "{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}. Glitchsoc is fully compatible with all Mastodon apps and instances. Glitchsoc is free open-source software. You can report bugs, request features, or contribute to the code on {github}.", - "id": "onboarding.page_six.github" - }, - { - "defaultMessage": "There are {apps} available for iOS, Android and other platforms.", - "id": "onboarding.page_six.apps_available" - }, - { - "defaultMessage": "mobile apps", - "id": "onboarding.page_six.various_app" - }, - { - "defaultMessage": "Bon Appetoot!", - "id": "onboarding.page_six.appetoot" - }, - { - "defaultMessage": "Next", - "id": "onboarding.next" - }, - { - "defaultMessage": "Done", - "id": "onboarding.done" - }, - { - "defaultMessage": "Skip", - "id": "onboarding.skip" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/components/onboarding_modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.", - "id": "moved_to_warning" - } - ], - "path": "app/javascript/flavours/glitch/features/ui/index.json" - } -] \ No newline at end of file diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 856a171b36..e9fe5a0ec3 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -4,7 +4,9 @@ "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.follows": "Follows", "account.joined": "Joined {date}", + "account.mute_notifications": "Mute notifications from @{name}", "account.suspended_disclaimer_full": "This user has been suspended by a moderator.", + "account.unmute_notifications": "Unmute notifications from @{name}", "account.view_full_profile": "View full profile", "account_note.cancel": "Cancel", "account_note.edit": "Edit", diff --git a/app/javascript/flavours/glitch/locales/global_locale.ts b/app/javascript/flavours/glitch/locales/global_locale.ts new file mode 100644 index 0000000000..01133ca239 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/global_locale.ts @@ -0,0 +1,22 @@ +export interface LocaleData { + locale: string; + messages: Record; +} + +let loadedLocale: LocaleData; + +export function setLocale(locale: LocaleData) { + loadedLocale = locale; +} + +export function getLocale() { + if (!loadedLocale && process.env.NODE_ENV === 'development') { + throw new Error('getLocale() called before any locale has been set'); + } + + return loadedLocale; +} + +export function isLocaleLoaded() { + return !!loadedLocale; +} diff --git a/app/javascript/flavours/glitch/locales/index.ts b/app/javascript/flavours/glitch/locales/index.ts new file mode 100644 index 0000000000..63f45c3047 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/index.ts @@ -0,0 +1,5 @@ +export type { LocaleData } from './global_locale'; +export { setLocale, getLocale, isLocaleLoaded } from './global_locale'; +export { loadLocale } from './load_locale'; + +export { IntlProvider } from './intl_provider'; diff --git a/app/javascript/flavours/glitch/locales/intl_provider.tsx b/app/javascript/flavours/glitch/locales/intl_provider.tsx new file mode 100644 index 0000000000..4fa8b2247c --- /dev/null +++ b/app/javascript/flavours/glitch/locales/intl_provider.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from 'react'; + +import { IntlProvider as BaseIntlProvider } from 'react-intl'; + +import { getLocale, isLocaleLoaded } from './global_locale'; +import { loadLocale } from './load_locale'; + +function onProviderError(error: unknown) { + // Silent the error, like upstream does + if (process.env.NODE_ENV === 'production') return; + + // This browser does not advertise Intl support for this locale, we only print a warning + // As-per the spec, the browser should select the best matching locale + if ( + error && + typeof error === 'object' && + error instanceof Error && + error.message.match('MISSING_DATA') + ) { + console.warn(error.message); + } + + console.error(error); +} + +export const IntlProvider: React.FC< + Omit, 'locale' | 'messages'> +> = ({ children, ...props }) => { + const [localeLoaded, setLocaleLoaded] = useState(false); + + useEffect(() => { + async function loadLocaleData() { + if (!isLocaleLoaded()) { + await loadLocale(); + } + + setLocaleLoaded(true); + } + void loadLocaleData(); + }, []); + + if (!localeLoaded) return null; + + const { locale, messages } = getLocale(); + + return ( + + {children} + + ); +}; diff --git a/app/javascript/flavours/glitch/locales/load_locale.ts b/app/javascript/flavours/glitch/locales/load_locale.ts new file mode 100644 index 0000000000..fa5d13bbcc --- /dev/null +++ b/app/javascript/flavours/glitch/locales/load_locale.ts @@ -0,0 +1,37 @@ +import { Semaphore } from 'async-mutex'; + +import type { LocaleData } from './global_locale'; +import { isLocaleLoaded, setLocale } from './global_locale'; + +const localeLoadingSemaphore = new Semaphore(1); + +export async function loadLocale() { + const locale = document.querySelector('html')?.lang || 'en'; + + // We use a Semaphore here so only one thing can try to load the locales at + // the same time. If one tries to do it while its in progress, it will wait + // for the initial load to finish before it is resumed (and will see that locale + // data is already loaded) + await localeLoadingSemaphore.runExclusive(async () => { + // if the locale is already set, then do nothing + if (isLocaleLoaded()) return; + + const upstreamLocaleData = await import( + /* webpackMode: "lazy" */ + /* webpackChunkName: "locales/vanilla/[request]" */ + /* webpackInclude: /\.json$/ */ + /* webpackPreload: true */ + `mastodon/locales/${locale}.json` + ) as LocaleData['messages']; + + const localeData = await import( + /* webpackMode: "lazy" */ + /* webpackChunkName: "locales/glitch/[request]" */ + /* webpackInclude: /\.json$/ */ + /* webpackPreload: true */ + `flavours/glitch/locales/${locale}.json` + ) as LocaleData['messages']; + + setLocale({ messages: { ...upstreamLocaleData, ...localeData }, locale }); + }); +} diff --git a/app/javascript/flavours/glitch/locales/whitelist_af.json b/app/javascript/flavours/glitch/locales/whitelist_af.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_af.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_an.json b/app/javascript/flavours/glitch/locales/whitelist_an.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_an.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ar.json b/app/javascript/flavours/glitch/locales/whitelist_ar.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ar.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ast.json b/app/javascript/flavours/glitch/locales/whitelist_ast.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ast.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_be.json b/app/javascript/flavours/glitch/locales/whitelist_be.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_be.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_bg.json b/app/javascript/flavours/glitch/locales/whitelist_bg.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_bg.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_bn.json b/app/javascript/flavours/glitch/locales/whitelist_bn.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_bn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_br.json b/app/javascript/flavours/glitch/locales/whitelist_br.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_br.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_bs.json b/app/javascript/flavours/glitch/locales/whitelist_bs.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_bs.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ca.json b/app/javascript/flavours/glitch/locales/whitelist_ca.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ca.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ckb.json b/app/javascript/flavours/glitch/locales/whitelist_ckb.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ckb.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_co.json b/app/javascript/flavours/glitch/locales/whitelist_co.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_co.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_cs.json b/app/javascript/flavours/glitch/locales/whitelist_cs.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_cs.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_cy.json b/app/javascript/flavours/glitch/locales/whitelist_cy.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_cy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_da.json b/app/javascript/flavours/glitch/locales/whitelist_da.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_da.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_de.json b/app/javascript/flavours/glitch/locales/whitelist_de.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_de.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_el.json b/app/javascript/flavours/glitch/locales/whitelist_el.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_el.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_en-GB.json b/app/javascript/flavours/glitch/locales/whitelist_en-GB.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_en-GB.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_en.json b/app/javascript/flavours/glitch/locales/whitelist_en.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_en.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_eo.json b/app/javascript/flavours/glitch/locales/whitelist_eo.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_eo.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_es-AR.json b/app/javascript/flavours/glitch/locales/whitelist_es-AR.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_es-AR.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_es-MX.json b/app/javascript/flavours/glitch/locales/whitelist_es-MX.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_es-MX.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_es.json b/app/javascript/flavours/glitch/locales/whitelist_es.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_es.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_et.json b/app/javascript/flavours/glitch/locales/whitelist_et.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_et.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_eu.json b/app/javascript/flavours/glitch/locales/whitelist_eu.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_eu.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fa.json b/app/javascript/flavours/glitch/locales/whitelist_fa.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fi.json b/app/javascript/flavours/glitch/locales/whitelist_fi.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fo.json b/app/javascript/flavours/glitch/locales/whitelist_fo.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fo.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fr-QC.json b/app/javascript/flavours/glitch/locales/whitelist_fr-QC.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fr-QC.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fr.json b/app/javascript/flavours/glitch/locales/whitelist_fr.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_fy.json b/app/javascript/flavours/glitch/locales/whitelist_fy.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_fy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ga.json b/app/javascript/flavours/glitch/locales/whitelist_ga.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ga.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_gd.json b/app/javascript/flavours/glitch/locales/whitelist_gd.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_gd.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_gl.json b/app/javascript/flavours/glitch/locales/whitelist_gl.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_gl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_he.json b/app/javascript/flavours/glitch/locales/whitelist_he.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_he.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_hi.json b/app/javascript/flavours/glitch/locales/whitelist_hi.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_hi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_hr.json b/app/javascript/flavours/glitch/locales/whitelist_hr.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_hr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_hu.json b/app/javascript/flavours/glitch/locales/whitelist_hu.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_hu.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_hy.json b/app/javascript/flavours/glitch/locales/whitelist_hy.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_hy.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_id.json b/app/javascript/flavours/glitch/locales/whitelist_id.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_id.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ig.json b/app/javascript/flavours/glitch/locales/whitelist_ig.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ig.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_io.json b/app/javascript/flavours/glitch/locales/whitelist_io.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_io.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_is.json b/app/javascript/flavours/glitch/locales/whitelist_is.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_is.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_it.json b/app/javascript/flavours/glitch/locales/whitelist_it.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_it.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ja.json b/app/javascript/flavours/glitch/locales/whitelist_ja.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ja.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ka.json b/app/javascript/flavours/glitch/locales/whitelist_ka.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ka.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_kab.json b/app/javascript/flavours/glitch/locales/whitelist_kab.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_kab.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_kk.json b/app/javascript/flavours/glitch/locales/whitelist_kk.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_kk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_kn.json b/app/javascript/flavours/glitch/locales/whitelist_kn.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_kn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ko.json b/app/javascript/flavours/glitch/locales/whitelist_ko.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ko.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ku.json b/app/javascript/flavours/glitch/locales/whitelist_ku.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ku.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_kw.json b/app/javascript/flavours/glitch/locales/whitelist_kw.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_kw.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_la.json b/app/javascript/flavours/glitch/locales/whitelist_la.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_la.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_lt.json b/app/javascript/flavours/glitch/locales/whitelist_lt.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_lt.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_lv.json b/app/javascript/flavours/glitch/locales/whitelist_lv.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_lv.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_mk.json b/app/javascript/flavours/glitch/locales/whitelist_mk.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_mk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ml.json b/app/javascript/flavours/glitch/locales/whitelist_ml.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ml.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_mr.json b/app/javascript/flavours/glitch/locales/whitelist_mr.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_mr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ms.json b/app/javascript/flavours/glitch/locales/whitelist_ms.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ms.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_my.json b/app/javascript/flavours/glitch/locales/whitelist_my.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_my.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_nl.json b/app/javascript/flavours/glitch/locales/whitelist_nl.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_nl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_nn.json b/app/javascript/flavours/glitch/locales/whitelist_nn.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_nn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_no.json b/app/javascript/flavours/glitch/locales/whitelist_no.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_no.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_oc.json b/app/javascript/flavours/glitch/locales/whitelist_oc.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_oc.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_pa.json b/app/javascript/flavours/glitch/locales/whitelist_pa.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_pa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_pl.json b/app/javascript/flavours/glitch/locales/whitelist_pl.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_pl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_pt-BR.json b/app/javascript/flavours/glitch/locales/whitelist_pt-BR.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_pt-BR.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_pt-PT.json b/app/javascript/flavours/glitch/locales/whitelist_pt-PT.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_pt-PT.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ro.json b/app/javascript/flavours/glitch/locales/whitelist_ro.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ro.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ru.json b/app/javascript/flavours/glitch/locales/whitelist_ru.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ru.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sa.json b/app/javascript/flavours/glitch/locales/whitelist_sa.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sa.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sc.json b/app/javascript/flavours/glitch/locales/whitelist_sc.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sc.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sco.json b/app/javascript/flavours/glitch/locales/whitelist_sco.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sco.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_si.json b/app/javascript/flavours/glitch/locales/whitelist_si.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_si.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sk.json b/app/javascript/flavours/glitch/locales/whitelist_sk.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sl.json b/app/javascript/flavours/glitch/locales/whitelist_sl.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sq.json b/app/javascript/flavours/glitch/locales/whitelist_sq.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sq.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sr-Latn.json b/app/javascript/flavours/glitch/locales/whitelist_sr-Latn.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sr-Latn.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sr.json b/app/javascript/flavours/glitch/locales/whitelist_sr.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_sv.json b/app/javascript/flavours/glitch/locales/whitelist_sv.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_sv.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_szl.json b/app/javascript/flavours/glitch/locales/whitelist_szl.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_szl.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ta.json b/app/javascript/flavours/glitch/locales/whitelist_ta.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ta.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_tai.json b/app/javascript/flavours/glitch/locales/whitelist_tai.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_tai.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_te.json b/app/javascript/flavours/glitch/locales/whitelist_te.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_te.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_th.json b/app/javascript/flavours/glitch/locales/whitelist_th.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_th.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_tr.json b/app/javascript/flavours/glitch/locales/whitelist_tr.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_tr.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_tt.json b/app/javascript/flavours/glitch/locales/whitelist_tt.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_tt.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ug.json b/app/javascript/flavours/glitch/locales/whitelist_ug.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ug.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_uk.json b/app/javascript/flavours/glitch/locales/whitelist_uk.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_uk.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_ur.json b/app/javascript/flavours/glitch/locales/whitelist_ur.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_ur.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_vi.json b/app/javascript/flavours/glitch/locales/whitelist_vi.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_vi.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_zgh.json b/app/javascript/flavours/glitch/locales/whitelist_zgh.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_zgh.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_zh-CN.json b/app/javascript/flavours/glitch/locales/whitelist_zh-CN.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_zh-CN.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_zh-HK.json b/app/javascript/flavours/glitch/locales/whitelist_zh-HK.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_zh-HK.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/locales/whitelist_zh-TW.json b/app/javascript/flavours/glitch/locales/whitelist_zh-TW.json deleted file mode 100644 index 0d4f101c7a..0000000000 --- a/app/javascript/flavours/glitch/locales/whitelist_zh-TW.json +++ /dev/null @@ -1,2 +0,0 @@ -[ -] diff --git a/app/javascript/flavours/glitch/packs/admin.jsx b/app/javascript/flavours/glitch/packs/admin.jsx index 4a5a50b494..596611dad4 100644 --- a/app/javascript/flavours/glitch/packs/admin.jsx +++ b/app/javascript/flavours/glitch/packs/admin.jsx @@ -6,14 +6,14 @@ import ready from 'flavours/glitch/ready'; ready(() => { [].forEach.call(document.querySelectorAll('[data-admin-component]'), element => { const componentName = element.getAttribute('data-admin-component'); - const { locale, ...componentProps } = JSON.parse(element.getAttribute('data-props')); + const { ...componentProps } = JSON.parse(element.getAttribute('data-props')); import('flavours/glitch/containers/admin_component').then(({ default: AdminComponent }) => { return import('flavours/glitch/components/admin/' + componentName).then(({ default: Component }) => { const root = createRoot(element); root.render ( - + , ); diff --git a/app/javascript/flavours/glitch/packs/home.js b/app/javascript/flavours/glitch/packs/home.js index a28625d33b..842430354d 100644 --- a/app/javascript/flavours/glitch/packs/home.js +++ b/app/javascript/flavours/glitch/packs/home.js @@ -1,10 +1,11 @@ import 'packs/public-path'; +import { loadLocale } from 'flavours/glitch/locales'; +import main from "flavours/glitch/main"; import { loadPolyfills } from 'flavours/glitch/polyfills'; -loadPolyfills().then(async () => { - const { default: main } = await import('flavours/glitch/main'); - - return main(); -}).catch(e => { - console.error(e); -}); +loadPolyfills() + .then(loadLocale) + .then(main) + .catch(e => { + console.error(e); + }); diff --git a/app/javascript/flavours/glitch/packs/public.jsx b/app/javascript/flavours/glitch/packs/public.jsx index 92e2e9e086..22a3e1214d 100644 --- a/app/javascript/flavours/glitch/packs/public.jsx +++ b/app/javascript/flavours/glitch/packs/public.jsx @@ -1,7 +1,7 @@ import 'packs/public-path'; import { createRoot } from 'react-dom/client'; -import * as IntlMessageFormat from 'intl-messageformat'; +import { IntlMessageFormat } from 'intl-messageformat'; import { defineMessages } from 'react-intl'; import { delegate } from '@rails/ujs'; @@ -12,9 +12,9 @@ import { throttle } from 'lodash'; import { timeAgoString } from 'flavours/glitch/components/relative_timestamp'; import emojify from 'flavours/glitch/features/emoji/emoji'; import loadKeyboardExtensions from 'flavours/glitch/load_keyboard_extensions'; +import { loadLocale, getLocale } from 'flavours/glitch/locales'; import { loadPolyfills } from 'flavours/glitch/polyfills'; import ready from 'flavours/glitch/ready'; -import { getLocale } from 'locales'; const messages = defineMessages({ usernameTaken: { id: 'username.taken', defaultMessage: 'That username is taken. Try another' }, @@ -23,7 +23,7 @@ const messages = defineMessages({ }); function main() { - const { localeData } = getLocale(); + const { messages: localeData } = getLocale(); const scrollToDetailedStatus = () => { const history = createBrowserHistory(); @@ -237,6 +237,7 @@ function main() { } loadPolyfills() + .then(loadLocale) .then(main) .then(loadKeyboardExtensions) .catch(error => { diff --git a/app/javascript/flavours/glitch/polyfills/base_polyfills.ts b/app/javascript/flavours/glitch/polyfills/base_polyfills.ts index e008d8f025..3cde1b1ede 100644 --- a/app/javascript/flavours/glitch/polyfills/base_polyfills.ts +++ b/app/javascript/flavours/glitch/polyfills/base_polyfills.ts @@ -1,5 +1,3 @@ -import 'intl'; -import 'intl/locale-data/jsonp/en'; import 'core-js/features/object/assign'; import 'core-js/features/object/values'; import 'core-js/features/symbol'; diff --git a/app/javascript/flavours/glitch/polyfills/index.ts b/app/javascript/flavours/glitch/polyfills/index.ts index 6d2e5426e4..b2dbfdac0a 100644 --- a/app/javascript/flavours/glitch/polyfills/index.ts +++ b/app/javascript/flavours/glitch/polyfills/index.ts @@ -2,6 +2,8 @@ // If there are no polyfills, then this is just Promise.resolve() which means // it will execute in the same tick of the event loop (i.e. near-instant). +import { loadIntlPolyfills } from './intl'; + function importBasePolyfills() { return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills'); } @@ -13,7 +15,6 @@ function importExtraPolyfills() { export function loadPolyfills() { const needsBasePolyfills = !( 'toBlob' in HTMLCanvasElement.prototype && - 'Intl' in window && 'assign' in Object && 'values' in Object && 'Symbol' in window && @@ -32,6 +33,7 @@ export function loadPolyfills() { ); return Promise.all([ + loadIntlPolyfills(), needsBasePolyfills && importBasePolyfills(), needsExtraPolyfills && importExtraPolyfills(), ]); diff --git a/app/javascript/flavours/glitch/polyfills/intl.ts b/app/javascript/flavours/glitch/polyfills/intl.ts new file mode 100644 index 0000000000..4d5ee3ccf9 --- /dev/null +++ b/app/javascript/flavours/glitch/polyfills/intl.ts @@ -0,0 +1,105 @@ +// import { shouldPolyfill as shouldPolyfillCanonicalLocales } from '@formatjs/intl-getcanonicallocales/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillLocale } from '@formatjs/intl-locale/should-polyfill'; +import { shouldPolyfill as shoudPolyfillPluralRules } from '@formatjs/intl-pluralrules/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillNumberFormat } from '@formatjs/intl-numberformat/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillIntlDateTimeFormat } from '@formatjs/intl-datetimeformat/should-polyfill'; +// import { shouldPolyfill as shouldPolyfillIntlRelativeTimeFormat } from '@formatjs/intl-relativetimeformat/should-polyfill'; + +// async function loadGetCanonicalLocalesPolyfill() { +// // This platform already supports Intl.getCanonicalLocales +// if (shouldPolyfillCanonicalLocales()) { +// await import('@formatjs/intl-getcanonicallocales/polyfill'); +// } +// } + +// async function loadLocalePolyfill() { +// // This platform already supports Intl.Locale +// if (shouldPolyfillLocale()) { +// await import('@formatjs/intl-locale/polyfill'); +// } +// } + +// async function loadIntlNumberFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillNumberFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import('@formatjs/intl-numberformat/polyfill-force'); +// await import(`@formatjs/intl-numberformat/locale-data/${unsupportedLocale}`); +// } + +// async function loadIntlDateTimeFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillIntlDateTimeFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import('@formatjs/intl-datetimeformat/polyfill-force'); + +// // Parallelize CLDR data loading +// const dataPolyfills = [ +// import('@formatjs/intl-datetimeformat/add-all-tz'), +// import(`@formatjs/intl-datetimeformat/locale-data/${unsupportedLocale}`), +// ]; +// await Promise.all(dataPolyfills); +// } + +async function loadIntlPluralRulesPolyfills(locale: string) { + const unsupportedLocale = shoudPolyfillPluralRules(locale); + // This locale is supported + if (!unsupportedLocale) { + return; + } + // Load the polyfill 1st BEFORE loading data + await import( + /* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force' + ); + await import( + /* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}` + ); +} + +// async function loadIntlRelativeTimeFormatPolyfill(locale: string) { +// const unsupportedLocale = shouldPolyfillIntlRelativeTimeFormat(locale); +// // This locale is supported +// if (!unsupportedLocale) { +// return; +// } +// // Load the polyfill 1st BEFORE loading data +// await import( +// /* webpackChunkName: "i18n-relativetimeformat-polyfill" */ +// '@formatjs/intl-relativetimeformat/polyfill-force' +// ); +// await import( +// /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */ +// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}` +// ); +// } + +export async function loadIntlPolyfills() { + const locale = document.querySelector('html')?.lang || 'en'; + + // order is important here + + // Supported in IE11 and most other browsers, not useful + // await loadGetCanonicalLocalesPolyfill() + + // Supported in IE11 and most other browsers, not useful + // await loadLocalePolyfill() + + // Supported in IE11 and most other browsers, not useful + // await loadIntlNumberFormatPolyfill(locale) + + // Supported in IE11 and most other browsers, not useful + // await loadIntlDateTimeFormatPolyfill(locale) + + // Supported from Safari 13+, may still be useful + await loadIntlPluralRulesPolyfills(locale); + + // This is not used yet in the codebase yet + // Supported from Safari 14+ + // await loadIntlRelativeTimeFormatPolyfill(locale); +} diff --git a/app/javascript/flavours/glitch/reducers/list_editor.js b/app/javascript/flavours/glitch/reducers/list_editor.js index ceceb27c7a..d3fd62adec 100644 --- a/app/javascript/flavours/glitch/reducers/list_editor.js +++ b/app/javascript/flavours/glitch/reducers/list_editor.js @@ -25,6 +25,7 @@ const initialState = ImmutableMap({ isSubmitting: false, isChanged: false, title: '', + isExclusive: false, accounts: ImmutableMap({ items: ImmutableList(), @@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) { return state.withMutations(map => { map.set('listId', action.list.get('id')); map.set('title', action.list.get('title')); + map.set('isExclusive', action.list.get('is_exclusive')); map.set('isSubmitting', false); }); case LIST_EDITOR_TITLE_CHANGE: diff --git a/app/javascript/flavours/glitch/reducers/polls.js b/app/javascript/flavours/glitch/reducers/polls.js index 8712c6ebf3..3a15cb49cc 100644 --- a/app/javascript/flavours/glitch/reducers/polls.js +++ b/app/javascript/flavours/glitch/reducers/polls.js @@ -2,14 +2,43 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; import { POLLS_IMPORT } from 'flavours/glitch/actions/importer'; +import { normalizePollOptionTranslation } from '../actions/importer/normalizer'; +import { STATUS_TRANSLATE_SUCCESS, STATUS_TRANSLATE_UNDO } from '../actions/statuses'; + const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll)))); +const statusTranslateSuccess = (state, pollTranslation) => { + return state.withMutations(map => { + if (pollTranslation) { + const poll = state.get(pollTranslation.id); + + pollTranslation.options.forEach((item, index) => { + map.setIn([pollTranslation.id, 'options', index, 'translation'], fromJS(normalizePollOptionTranslation(item, poll))); + }); + } + }); +}; + +const statusTranslateUndo = (state, id) => { + return state.withMutations(map => { + const options = map.getIn([id, 'options']); + + if (options) { + options.forEach((item, index) => map.deleteIn([id, 'options', index, 'translation'])); + } + }); +}; + const initialState = ImmutableMap(); export default function polls(state = initialState, action) { switch(action.type) { case POLLS_IMPORT: return importPolls(state, action.polls); + case STATUS_TRANSLATE_SUCCESS: + return statusTranslateSuccess(state, action.translation.poll); + case STATUS_TRANSLATE_UNDO: + return statusTranslateUndo(state, action.pollId); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 8fb9443dd6..b65c7ef8b4 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -25,6 +25,7 @@ import { } from 'flavours/glitch/actions/timelines'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; +import { normalizeStatusTranslation } from '../actions/importer/normalizer'; const importStatus = (state, status) => state.set(status.id, fromJS(status)); @@ -39,6 +40,27 @@ const deleteStatus = (state, id, references) => { return state.delete(id); }; +const statusTranslateSuccess = (state, id, translation) => { + return state.withMutations(map => { + map.setIn([id, 'translation'], fromJS(normalizeStatusTranslation(translation, map.get(id)))); + + const list = map.getIn([id, 'media_attachments']); + if (translation.media_attachments && list) { + translation.media_attachments.forEach(item => { + const index = list.findIndex(i => i.get('id') === item.id); + map.setIn([id, 'media_attachments', index, 'translation'], fromJS({ description: item.description })); + }); + } + }); +}; + +const statusTranslateUndo = (state, id) => { + return state.withMutations(map => { + map.deleteIn([id, 'translation']); + map.getIn([id, 'media_attachments']).forEach((item, index) => map.deleteIn([id, 'media_attachments', index, 'translation'])); + }); +}; + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -90,9 +112,9 @@ export default function statuses(state = initialState, action) { case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); case STATUS_TRANSLATE_SUCCESS: - return state.setIn([action.id, 'translation'], fromJS(action.translation)); + return statusTranslateSuccess(state, action.id, action.translation); case STATUS_TRANSLATE_UNDO: - return state.deleteIn([action.id, 'translation']); + return statusTranslateUndo(state, action.id); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index d10169e02a..ad0dfe0177 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -3,11 +3,8 @@ display: block; text-decoration: none; color: inherit; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); - - @media screen and (max-width: $no-gap-breakpoint) { - box-shadow: none; - } + overflow: hidden; + border-radius: 4px; &:hover, &:active, @@ -22,7 +19,6 @@ height: 130px; position: relative; background: darken($ui-base-color, 12%); - border-radius: 4px 4px 0 0; img { display: block; @@ -30,7 +26,6 @@ height: 100%; margin: 0; object-fit: cover; - border-radius: 4px 4px 0 0; } @media screen and (width <= 600px) { @@ -45,11 +40,6 @@ justify-content: flex-start; align-items: center; background: lighten($ui-base-color, 4%); - border-radius: 0 0 4px 4px; - - @media screen and (max-width: $no-gap-breakpoint) { - border-radius: 0; - } .avatar { flex: 0 0 auto; diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 2adee62d6b..a57e014e9e 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1315,6 +1315,15 @@ a.sparkline { &:last-child { border-bottom: 0; } + + &.negative { + color: $error-value-color; + font-weight: 700; + + .dimension__item__value { + color: $error-value-color; + } + } } } diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 92633fc3fc..535af9d0f2 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -236,7 +236,7 @@ background: transparent; box-sizing: border-box; border: 0; - color: rgba($primary-text-color, 0.7); + color: rgba($white, 0.7); cursor: pointer; display: flex; align-items: center; @@ -251,7 +251,7 @@ &:hover, &:focus, &:active { - color: $primary-text-color; + color: $white; } } diff --git a/app/javascript/flavours/glitch/styles/components/misc.scss b/app/javascript/flavours/glitch/styles/components/misc.scss index d6666acd23..53620eeb3c 100644 --- a/app/javascript/flavours/glitch/styles/components/misc.scss +++ b/app/javascript/flavours/glitch/styles/components/misc.scss @@ -534,36 +534,18 @@ body > [data-popper-placement] { } .dropdown-animation { - animation: dropdown 300ms cubic-bezier(0.1, 0.7, 0.1, 1); + animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1); @keyframes dropdown { from { opacity: 0; - transform: scaleX(0.85) scaleY(0.75); } to { opacity: 1; - transform: scaleX(1) scaleY(1); } } - &.top { - transform-origin: bottom; - } - - &.right { - transform-origin: left; - } - - &.bottom { - transform-origin: top; - } - - &.left { - transform-origin: right; - } - .reduce-motion & { animation: none; } @@ -579,16 +561,17 @@ body > [data-popper-placement] { } .dropdown-menu__separator { - border-bottom: 1px solid darken($ui-secondary-color, 8%); - margin: 5px 7px 6px; + border-bottom: 1px solid var(--dropdown-border-color); + margin: 5px 0; height: 0; } .dropdown-menu { - background: $ui-secondary-color; - padding: 4px 0; + background: var(--dropdown-background-color); + border: 1px solid var(--dropdown-border-color); + padding: 4px; border-radius: 4px; - box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + box-shadow: var(--dropdown-shadow); z-index: 9999; &__text-button { @@ -609,12 +592,13 @@ body > [data-popper-placement] { &__container { &__header { - border-bottom: 1px solid darken($ui-secondary-color, 8%); - padding: 4px 14px; - padding-bottom: 8px; + border-bottom: 1px solid var(--dropdown-border-color); + padding: 10px 14px; + padding-bottom: 14px; + margin-bottom: 4px; font-size: 13px; line-height: 18px; - color: $inverted-text-color; + color: $darker-text-color; } &__list { @@ -635,103 +619,43 @@ body > [data-popper-placement] { } } -.dropdown-menu__arrow { - position: absolute; - - &::before { - content: ''; - display: block; - width: 14px; - height: 5px; - background-color: $ui-secondary-color; - mask-image: url("data:image/svg+xml;utf8,"); - } - - &.top { - bottom: -5px; - - &::before { - transform: rotate(180deg); - } - } - - &.right { - inset-inline-start: -9px; - - &::before { - transform: rotate(-90deg); - } - } - - &.bottom { - top: -5px; - } - - &.left { - inset-inline-end: -9px; - - &::before { - transform: rotate(90deg); - } - } -} - .dropdown-menu__item { font-size: 13px; line-height: 18px; + font-weight: 500; display: block; - color: $inverted-text-color; + + &--dangerous { + color: $error-value-color; + } a, button { - font-family: inherit; - font-size: inherit; - line-height: inherit; + font: inherit; display: block; width: 100%; - padding: 4px 14px; + padding: 10px 14px; border: 0; margin: 0; + background: transparent; box-sizing: border-box; text-decoration: none; - background: $ui-secondary-color; color: inherit; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: inherit; + border-radius: 4px; &:focus, &:hover, &:active { - background: $ui-highlight-color; - color: $secondary-text-color; + background: var(--dropdown-border-color); outline: 0; } } } -.dropdown-menu__item--text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding: 4px 14px; -} - -.dropdown-menu__item.edited-timestamp__history__item { - border-bottom: 1px solid darken($ui-secondary-color, 8%); - - &:last-child { - border-bottom: 0; - } - - &.dropdown-menu__item--text, - a, - button { - padding: 8px 14px; - } -} - .inline-account { display: inline-flex; align-items: center; @@ -747,62 +671,6 @@ body > [data-popper-placement] { } } -.dropdown--active .dropdown__content { - display: block; - line-height: 18px; - max-width: 311px; - inset-inline-end: 0; - text-align: start; - z-index: 9999; - - & > ul { - list-style: none; - background: $ui-secondary-color; - padding: 4px 0; - border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); - min-width: 140px; - position: relative; - } - - &.dropdown__right { - inset-inline-end: 0; - } - - &.dropdown__left { - & > ul { - inset-inline-start: -98px; - } - } - - & > ul > li > a { - font-size: 13px; - line-height: 18px; - display: block; - padding: 4px 14px; - box-sizing: border-box; - text-decoration: none; - background: $ui-secondary-color; - color: $inverted-text-color; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - &:focus { - outline: 0; - } - - &:hover { - background: $ui-highlight-color; - color: $secondary-text-color; - } - } -} - -.dropdown__icon { - vertical-align: middle; -} - .static-content { padding: 10px; padding-top: 20px; diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 6bb21c1899..6c49879ee1 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -138,6 +138,10 @@ code { color: $secondary-text-color; margin-bottom: 30px; + &.invited-by { + margin-bottom: 15px; + } + a { color: $highlight-text-color; } diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 0132da51f3..8608fec723 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -67,3 +67,10 @@ $ui-avatar-border-size: 8%; // More variables $dismiss-overlay-width: 4rem; + +:root { + --dropdown-border-color: #{lighten($ui-base-color, 12%)}; + --dropdown-background-color: #{lighten($ui-base-color, 4%)}; + --dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)}, + 0 8px 10px -6px #{rgba($base-shadow-color, 0.25)}; +} diff --git a/app/javascript/locales/index.js b/app/javascript/locales/index.js deleted file mode 100644 index 421cb7fab0..0000000000 --- a/app/javascript/locales/index.js +++ /dev/null @@ -1,9 +0,0 @@ -let theLocale; - -export function setLocale(locale) { - theLocale = locale; -} - -export function getLocale() { - return theLocale; -} diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 61062fd2c8..9ed6b583b5 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -6,7 +6,7 @@ import { unescapeHTML } from '../../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); @@ -86,7 +86,7 @@ export function normalizeStatus(status, normalOldStatus) { 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); @@ -97,22 +97,48 @@ export function normalizeStatus(status, normalOldStatus) { 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/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js index 2faa54b955..b0789cd426 100644 --- a/app/javascript/mastodon/actions/lists.js +++ b/app/javascript/mastodon/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/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index c040edb58b..6e8ddb2279 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/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/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 84a1271b8b..3aed807358 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -343,7 +343,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/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 562e72655c..9daeb3c60f 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -24,8 +24,6 @@ import { fillListTimelineGaps, } from './timelines'; -const { messages } = getLocale(); - /** * @param {number} max * @returns {number} @@ -43,8 +41,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 @@ -121,6 +121,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, }; }); +}; /** * @param {Function} dispatch diff --git a/app/javascript/mastodon/components/account.jsx b/app/javascript/mastodon/components/account.jsx index 68a456c1b1..0f3b85388c 100644 --- a/app/javascript/mastodon/components/account.jsx +++ b/app/javascript/mastodon/components/account.jsx @@ -16,6 +16,7 @@ import { VerifiedBadge } from 'mastodon/components/verified_badge'; import { me } from '../initial_state'; import { Avatar } from './avatar'; +import Button from './button'; import { DisplayName } from './display_name'; import { IconButton } from './icon_button'; import { RelativeTimestamp } from './relative_timestamp'; @@ -23,13 +24,13 @@ import { RelativeTimestamp } from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' }, - unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' }, + unmute_notifications: { id: 'account.unmute_notifications_short', defaultMessage: 'Unmute notifications' }, + mute: { id: 'account.mute_short', defaultMessage: 'Mute' }, + block: { id: 'account.block_short', defaultMessage: 'Block' }, }); class Account extends ImmutablePureComponent { @@ -96,39 +97,39 @@ class Account extends ImmutablePureComponent { let buttons; - if (actionIcon) { - if (onActionClick) { - buttons = ; - } - } else if (account.get('id') !== me && account.get('relationship', null) !== null) { + if (actionIcon && onActionClick) { + buttons = ; + } else if (!actionIcon && account.get('id') !== me && account.get('relationship', null) !== null) { const following = account.getIn(['relationship', 'following']); const requested = account.getIn(['relationship', 'requested']); const blocking = account.getIn(['relationship', 'blocking']); const muting = account.getIn(['relationship', 'muting']); if (requested) { - buttons = ; + buttons =

    -
    +
    - {!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 0000000000..c27ee0ab08 --- /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 db18635be1..f4a3b9d4b6 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 4cadf907e7..3cfa0ee125 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 e6d3060eb3..7e2cd447b9 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 6b7ecdea0a..0000000000 --- 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 0000000000..8b5746ad30 --- /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 d2f1712437..7b945a0ea2 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 f5fa53f08e..7400111293 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 b93399aa91..f76550678e 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 5be163f5a4..4538db050d 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 7ed8f1719d..fba3c5df78 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 3026dde0a8..6167b404f0 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -180,7 +180,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 e9e5934f46..a206bcc3ba 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 27de4740ca..653a258667 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 b329cae791..b11ac478a4 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 d4854f1869..635b6f4113 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 dbc0400e8e..185db0732a 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 6b053a9dc1..dc1f720220 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 f41e8e6f23..f9f3a7c315 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 3807ce9227..8b4ad0b087 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 cc0c797900..f1447b771e 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 1895af912b..6871793026 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 a6e817c73d..fb9e55c579 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 2ce94d9d84..bc90ce592c 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -219,8 +219,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); @@ -228,16 +228,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]; @@ -247,7 +247,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 187e04ad17..ddda6eaac6 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -133,17 +133,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 = (