Merge commit 'c15c8c7aa8
' into kb_migration
This commit is contained in:
commit
093cfdbb7b
208 changed files with 2976 additions and 5888 deletions
|
@ -293,6 +293,7 @@ module.exports = {
|
||||||
'.*rc.js',
|
'.*rc.js',
|
||||||
'ide-helper.js',
|
'ide-helper.js',
|
||||||
'config/webpack/**/*',
|
'config/webpack/**/*',
|
||||||
|
'config/formatjs-formatter.js',
|
||||||
],
|
],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
|
@ -323,7 +324,7 @@ module.exports = {
|
||||||
'plugin:import/recommended',
|
'plugin:import/recommended',
|
||||||
'plugin:import/typescript',
|
'plugin:import/typescript',
|
||||||
'plugin:promise/recommended',
|
'plugin:promise/recommended',
|
||||||
'plugin:jsdoc/recommended',
|
'plugin:jsdoc/recommended-typescript',
|
||||||
'plugin:prettier/recommended',
|
'plugin:prettier/recommended',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -25,18 +25,6 @@ updates:
|
||||||
- dependency-name: 'react-hotkeys'
|
- dependency-name: 'react-hotkeys'
|
||||||
versions:
|
versions:
|
||||||
- '>= 2'
|
- '>= 2'
|
||||||
# TODO: This version has breaking changes
|
|
||||||
- dependency-name: 'intl-messageformat'
|
|
||||||
versions:
|
|
||||||
- '>= 3'
|
|
||||||
# TODO: This version has breaking changes
|
|
||||||
- dependency-name: 'react-intl'
|
|
||||||
versions:
|
|
||||||
- '>= 3'
|
|
||||||
# TODO: This version has breaking changes
|
|
||||||
- dependency-name: 'babel-plugin-react-intl'
|
|
||||||
versions:
|
|
||||||
- '>= 7'
|
|
||||||
# TODO: This version requires code changes
|
# TODO: This version requires code changes
|
||||||
- dependency-name: 'webpack-dev-server'
|
- dependency-name: 'webpack-dev-server'
|
||||||
versions:
|
versions:
|
||||||
|
|
3
.github/workflows/check-i18n.yml
vendored
3
.github/workflows/check-i18n.yml
vendored
|
@ -41,8 +41,7 @@ jobs:
|
||||||
|
|
||||||
- name: Check for missing strings in English JSON
|
- name: Check for missing strings in English JSON
|
||||||
run: |
|
run: |
|
||||||
yarn build:development
|
yarn i18n:extract --throws
|
||||||
yarn manage:translations en
|
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
|
|
||||||
- name: Check locale file normalization
|
- name: Check locale file normalization
|
||||||
|
|
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
|
@ -48,4 +48,4 @@ jobs:
|
||||||
- run: echo "::add-matcher::.github/stylelint-matcher.json"
|
- run: echo "::add-matcher::.github/stylelint-matcher.json"
|
||||||
|
|
||||||
- name: Stylelint
|
- name: Stylelint
|
||||||
run: yarn test:lint:sass
|
run: yarn lint:sass
|
||||||
|
|
4
.github/workflows/lint-js.yml
vendored
4
.github/workflows/lint-js.yml
vendored
|
@ -48,7 +48,7 @@ jobs:
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: ESLint
|
- name: ESLint
|
||||||
run: yarn test:lint:js --max-warnings 0
|
run: yarn lint:js --max-warnings 0
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: yarn test:typecheck
|
run: yarn typecheck
|
||||||
|
|
2
.github/workflows/lint-json.yml
vendored
2
.github/workflows/lint-json.yml
vendored
|
@ -40,4 +40,4 @@ jobs:
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn prettier --check "**/*.json"
|
run: yarn lint:json
|
||||||
|
|
5
.github/workflows/lint-md.yml
vendored
5
.github/workflows/lint-md.yml
vendored
|
@ -5,6 +5,7 @@ on:
|
||||||
- 'dependabot/**'
|
- 'dependabot/**'
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/lint-md.yml'
|
- '.github/workflows/lint-md.yml'
|
||||||
|
- '.nvmrc'
|
||||||
- '.prettier*'
|
- '.prettier*'
|
||||||
- '**/*.md'
|
- '**/*.md'
|
||||||
- '!AUTHORS.md'
|
- '!AUTHORS.md'
|
||||||
|
@ -14,6 +15,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/lint-md.yml'
|
- '.github/workflows/lint-md.yml'
|
||||||
|
- '.nvmrc'
|
||||||
- '.prettier*'
|
- '.prettier*'
|
||||||
- '**/*.md'
|
- '**/*.md'
|
||||||
- '!AUTHORS.md'
|
- '!AUTHORS.md'
|
||||||
|
@ -32,9 +34,10 @@ jobs:
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Install all yarn packages
|
- name: Install all yarn packages
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn prettier --check "**/*.md"
|
run: yarn lint:md
|
||||||
|
|
2
.github/workflows/lint-yml.yml
vendored
2
.github/workflows/lint-yml.yml
vendored
|
@ -42,4 +42,4 @@ jobs:
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Prettier
|
- name: Prettier
|
||||||
run: yarn prettier --check "**/*.{yml,yaml}"
|
run: yarn lint:yml
|
||||||
|
|
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
|
@ -44,4 +44,4 @@ jobs:
|
||||||
run: yarn --frozen-lockfile
|
run: yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Jest testing
|
- name: Jest testing
|
||||||
run: yarn test:jest --reporters github-actions summary
|
run: yarn jest --reporters github-actions summary
|
||||||
|
|
118
.rubocop.yml
118
.rubocop.yml
|
@ -53,6 +53,28 @@ Lint/UselessAccessModifier:
|
||||||
ContextCreatingMethods:
|
ContextCreatingMethods:
|
||||||
- class_methods
|
- class_methods
|
||||||
|
|
||||||
|
## Disable most Metrics/*Length cops
|
||||||
|
# Reason: those are often triggered and force significant refactors when this happend
|
||||||
|
# but the team feel they are not really improving the code quality.
|
||||||
|
|
||||||
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength
|
||||||
|
Metrics/ClassLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength
|
||||||
|
Metrics/MethodLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
|
||||||
|
Metrics/ModuleLength:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
## End Disable Metrics/*Length cops
|
||||||
|
|
||||||
# Reason: Currently disabled in .rubocop_todo.yml
|
# Reason: Currently disabled in .rubocop_todo.yml
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
|
@ -61,95 +83,12 @@ Metrics/AbcSize:
|
||||||
- 'lib/mastodon/cli/*.rb'
|
- 'lib/mastodon/cli/*.rb'
|
||||||
- db/*migrate/**/*
|
- db/*migrate/**/*
|
||||||
|
|
||||||
# Reason: Some functions cannot be broken up, but others may be refactor candidates
|
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength
|
|
||||||
Metrics/BlockLength:
|
|
||||||
CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
|
|
||||||
Exclude:
|
|
||||||
- 'config/routes.rb'
|
|
||||||
- 'lib/mastodon/cli/*.rb'
|
|
||||||
- 'lib/tasks/*.rake'
|
|
||||||
- 'app/models/concerns/account_associations.rb'
|
|
||||||
- 'app/models/concerns/account_interactions.rb'
|
|
||||||
- 'app/models/concerns/ldap_authenticable.rb'
|
|
||||||
- 'app/models/concerns/omniauthable.rb'
|
|
||||||
- 'app/models/concerns/pam_authenticable.rb'
|
|
||||||
- 'app/models/concerns/remotable.rb'
|
|
||||||
- 'app/services/suspend_account_service.rb'
|
|
||||||
- 'app/services/unsuspend_account_service.rb'
|
|
||||||
- 'app/views/accounts/show.rss.ruby'
|
|
||||||
- 'app/views/tags/show.rss.ruby'
|
|
||||||
- 'config/environments/development.rb'
|
|
||||||
- 'config/environments/production.rb'
|
|
||||||
- 'config/initializers/devise.rb'
|
|
||||||
- 'config/initializers/doorkeeper.rb'
|
|
||||||
- 'config/initializers/omniauth.rb'
|
|
||||||
- 'config/initializers/simple_form.rb'
|
|
||||||
- 'config/navigation.rb'
|
|
||||||
- 'config/routes.rb'
|
|
||||||
- 'config/routes/*.rb'
|
|
||||||
- 'db/post_migrate/20221101190723_backfill_admin_action_logs.rb'
|
|
||||||
- 'db/post_migrate/20221206114142_backfill_admin_action_logs_again.rb'
|
|
||||||
- 'lib/paperclip/gif_transcoder.rb'
|
|
||||||
|
|
||||||
# Reason:
|
# Reason:
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'lib/mastodon/cli/*.rb'
|
- 'lib/mastodon/cli/*.rb'
|
||||||
|
|
||||||
# Reason: Some Excluded files would be candidates for refactoring but not currently addressed
|
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength
|
|
||||||
Metrics/ClassLength:
|
|
||||||
CountAsOne: ['array', 'hash', 'heredoc', 'method_call']
|
|
||||||
Exclude:
|
|
||||||
- 'lib/mastodon/cli/*.rb'
|
|
||||||
- 'app/controllers/admin/accounts_controller.rb'
|
|
||||||
- 'app/controllers/api/base_controller.rb'
|
|
||||||
- 'app/controllers/api/v1/admin/accounts_controller.rb'
|
|
||||||
- 'app/controllers/application_controller.rb'
|
|
||||||
- 'app/controllers/auth/registrations_controller.rb'
|
|
||||||
- 'app/controllers/auth/sessions_controller.rb'
|
|
||||||
- 'app/lib/activitypub/activity.rb'
|
|
||||||
- 'app/lib/activitypub/activity/create.rb'
|
|
||||||
- 'app/lib/activitypub/activity/undo.rb'
|
|
||||||
- 'app/lib/activitypub/tag_manager.rb'
|
|
||||||
- 'app/lib/feed_manager.rb'
|
|
||||||
- 'app/lib/link_details_extractor.rb'
|
|
||||||
- 'app/lib/request.rb'
|
|
||||||
- 'app/lib/status_reach_finder.rb'
|
|
||||||
- 'app/lib/text_formatter.rb'
|
|
||||||
- 'app/lib/user_settings_decorator.rb'
|
|
||||||
- 'app/mailers/user_mailer.rb'
|
|
||||||
- 'app/models/account.rb'
|
|
||||||
- 'app/models/account_statuses_filter.rb'
|
|
||||||
- 'app/models/admin/account_action.rb'
|
|
||||||
- 'app/models/antenna.rb'
|
|
||||||
- 'app/models/form/account_batch.rb'
|
|
||||||
- 'app/models/media_attachment.rb'
|
|
||||||
- 'app/models/status.rb'
|
|
||||||
- 'app/models/tag.rb'
|
|
||||||
- 'app/models/user.rb'
|
|
||||||
- 'app/policies/status_policy.rb'
|
|
||||||
- 'app/serializers/activitypub/actor_serializer.rb'
|
|
||||||
- 'app/serializers/activitypub/note_serializer.rb'
|
|
||||||
- 'app/serializers/rest/account_serializer.rb'
|
|
||||||
- 'app/serializers/rest/status_serializer.rb'
|
|
||||||
- 'app/services/account_search_service.rb'
|
|
||||||
- 'app/services/activitypub/process_account_service.rb'
|
|
||||||
- 'app/services/activitypub/process_status_update_service.rb'
|
|
||||||
- 'app/services/backup_service.rb'
|
|
||||||
- 'app/services/bulk_import_service.rb'
|
|
||||||
- 'app/services/delete_account_service.rb'
|
|
||||||
- 'app/services/fan_out_on_write_service.rb'
|
|
||||||
- 'app/services/fetch_link_card_service.rb'
|
|
||||||
- 'app/services/import_service.rb'
|
|
||||||
- 'app/services/notify_service.rb'
|
|
||||||
- 'app/services/post_status_service.rb'
|
|
||||||
- 'app/services/search_service.rb'
|
|
||||||
- 'app/services/update_status_service.rb'
|
|
||||||
- 'lib/paperclip/color_extractor.rb'
|
|
||||||
|
|
||||||
# Reason: Currently disabled in .rubocop_todo.yml
|
# Reason: Currently disabled in .rubocop_todo.yml
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
|
@ -160,17 +99,10 @@ Metrics/CyclomaticComplexity:
|
||||||
- lib/mastodon/cli/*.rb
|
- lib/mastodon/cli/*.rb
|
||||||
- db/*migrate/**/*
|
- db/*migrate/**/*
|
||||||
|
|
||||||
# Reason: Currently disabled in .rubocop_todo.yml
|
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength
|
|
||||||
Metrics/MethodLength:
|
|
||||||
CountAsOne: [array, heredoc]
|
|
||||||
Exclude:
|
|
||||||
- 'lib/mastodon/cli/*.rb'
|
|
||||||
|
|
||||||
# Reason:
|
# Reason:
|
||||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
|
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists
|
||||||
Metrics/ModuleLength:
|
Metrics/ParameterLists:
|
||||||
CountAsOne: [array, heredoc]
|
CountKeywordArgs: false
|
||||||
|
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
|
|
@ -154,12 +154,6 @@ Lint/Void:
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 150
|
Max: 150
|
||||||
|
|
||||||
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
|
|
||||||
# AllowedMethods: refine
|
|
||||||
Metrics/BlockLength:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/concerns/status_safe_reblog_insert.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: CountBlocks, Max.
|
# Configuration parameters: CountBlocks, Max.
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Exclude:
|
Exclude:
|
||||||
|
@ -169,27 +163,6 @@ Metrics/BlockNesting:
|
||||||
Metrics/CyclomaticComplexity:
|
Metrics/CyclomaticComplexity:
|
||||||
Max: 25
|
Max: 25
|
||||||
|
|
||||||
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
|
||||||
Metrics/MethodLength:
|
|
||||||
Max: 58
|
|
||||||
|
|
||||||
# Configuration parameters: CountComments, Max, CountAsOne.
|
|
||||||
Metrics/ModuleLength:
|
|
||||||
Exclude:
|
|
||||||
- 'app/controllers/concerns/signature_verification.rb'
|
|
||||||
- 'app/helpers/application_helper.rb'
|
|
||||||
- 'app/helpers/jsonld_helper.rb'
|
|
||||||
- 'app/models/concerns/account_interactions.rb'
|
|
||||||
- 'app/models/concerns/has_user_settings.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
|
|
||||||
Metrics/ParameterLists:
|
|
||||||
Exclude:
|
|
||||||
- 'app/models/concerns/account_interactions.rb'
|
|
||||||
- 'app/services/activitypub/fetch_remote_account_service.rb'
|
|
||||||
- 'app/services/activitypub/fetch_remote_actor_service.rb'
|
|
||||||
- 'app/services/activitypub/fetch_remote_status_service.rb'
|
|
||||||
|
|
||||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 28
|
Max: 28
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -17,7 +17,7 @@ gem 'makara', '~> 0.5'
|
||||||
gem 'pghero'
|
gem 'pghero'
|
||||||
gem 'dotenv-rails', '~> 2.8'
|
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-core', '<= 2.4.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
|
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
|
||||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -109,17 +109,17 @@ GEM
|
||||||
attr_required (1.0.1)
|
attr_required (1.0.1)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.761.0)
|
aws-partitions (1.772.0)
|
||||||
aws-sdk-core (3.172.0)
|
aws-sdk-core (3.174.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.64.0)
|
aws-sdk-kms (1.65.0)
|
||||||
aws-sdk-core (~> 3, >= 3.165.0)
|
aws-sdk-core (~> 3, >= 3.174.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.122.0)
|
aws-sdk-s3 (1.123.0)
|
||||||
aws-sdk-core (~> 3, >= 3.165.0)
|
aws-sdk-core (~> 3, >= 3.174.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.5.2)
|
aws-sigv4 (1.5.2)
|
||||||
|
@ -544,8 +544,9 @@ GEM
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.5.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.19, >= 2.19.1)
|
loofah (~> 2.21)
|
||||||
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (6.0.0)
|
rails-i18n (6.0.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
|
@ -588,7 +589,7 @@ GEM
|
||||||
rspec-mocks (3.12.5)
|
rspec-mocks (3.12.5)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.12.0)
|
||||||
rspec-rails (6.0.2)
|
rspec-rails (6.0.3)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
|
@ -770,7 +771,7 @@ DEPENDENCIES
|
||||||
active_model_serializers (~> 0.10)
|
active_model_serializers (~> 0.10)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.2)
|
annotate (~> 3.2)
|
||||||
aws-sdk-s3 (~> 1.122)
|
aws-sdk-s3 (~> 1.123)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
|
|
|
@ -31,17 +31,23 @@ module Admin
|
||||||
@domain_block = DomainBlock.new(resource_params)
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
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)
|
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||||
@domain_block.save
|
@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
|
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)
|
@domain_block.errors.delete(:domain)
|
||||||
render :new
|
return render :new
|
||||||
else
|
end
|
||||||
|
|
||||||
|
# Allow transparently upgrading a domain block
|
||||||
if existing_domain_block.present?
|
if existing_domain_block.present?
|
||||||
@domain_block = existing_domain_block
|
@domain_block = existing_domain_block
|
||||||
@domain_block.update(resource_params)
|
@domain_block.assign_attributes(resource_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Require explicit confirmation when suspending
|
||||||
|
return render :confirm_suspension if requires_confirmation?
|
||||||
|
|
||||||
if @domain_block.save
|
if @domain_block.save
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
log_action :create, @domain_block
|
log_action :create, @domain_block
|
||||||
|
@ -50,12 +56,16 @@ module Admin
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize :domain_block, :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?)
|
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
|
||||||
log_action :update, @domain_block
|
log_action :update, @domain_block
|
||||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
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
|
def action_from_button
|
||||||
'save' if params[:save]
|
'save' if params[:save]
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@conversations = paginated_conversations
|
@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
|
end
|
||||||
|
|
||||||
def read
|
def read
|
||||||
|
@ -32,7 +32,20 @@ class Api::V1::ConversationsController < Api::BaseController
|
||||||
|
|
||||||
def paginated_conversations
|
def paginated_conversations
|
||||||
AccountConversation.where(account: current_account)
|
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
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
|
|
@ -173,11 +173,11 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def storage_host
|
def storage_host
|
||||||
URI::HTTPS.build(host: storage_host_name).to_s
|
"https://#{storage_host_var}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def storage_host?
|
def storage_host?
|
||||||
storage_host_name.present?
|
storage_host_var.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def quote_wrap(text, line_width: 80, break_sequence: "\n")
|
def quote_wrap(text, line_width: 80, break_sequence: "\n")
|
||||||
|
@ -248,7 +248,7 @@ module ApplicationHelper
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def storage_host_name
|
def storage_host_var
|
||||||
ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
|
ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# rubocop:disable Metrics/ModuleLength
|
|
||||||
|
|
||||||
module LanguagesHelper
|
module LanguagesHelper
|
||||||
ISO_639_1 = {
|
ISO_639_1 = {
|
||||||
aa: ['Afar', 'Afaraf'].freeze,
|
aa: ['Afar', 'Afaraf'].freeze,
|
||||||
|
|
|
@ -5,10 +5,6 @@ module SettingsHelper
|
||||||
LanguagesHelper::SUPPORTED_LOCALES.keys
|
LanguagesHelper::SUPPORTED_LOCALES.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash_to_object(hash)
|
|
||||||
HashObject.new(hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def session_device_icon(session)
|
def session_device_icon(session)
|
||||||
device = session.detection.device
|
device = session.detection.device
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { unescapeHTML } from '../../utils/html';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
|
||||||
obj[`:${emoji.shortcode}:`] = emoji;
|
obj[`:${emoji.shortcode}:`] = emoji;
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) {
|
||||||
export function normalizeAccount(account) {
|
export function normalizeAccount(account) {
|
||||||
account = { ...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;
|
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
|
||||||
|
|
||||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
||||||
|
@ -98,7 +98,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
||||||
|
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
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(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
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(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
const emojiMap = makeEmojiMap(normalStatus);
|
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||||
|
@ -120,22 +120,48 @@ export function normalizeEmojiReactions(emoji_reactions) {
|
||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export function normalizePoll(poll) {
|
||||||
const normalPoll = { ...poll };
|
const normalPoll = { ...poll };
|
||||||
const emojiMap = makeEmojiMap(normalPoll);
|
const emojiMap = makeEmojiMap(poll.emojis);
|
||||||
|
|
||||||
normalPoll.options = poll.options.map((option, index) => ({
|
normalPoll.options = poll.options.map((option, index) => ({
|
||||||
...option,
|
...option,
|
||||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return normalPoll;
|
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) {
|
export function normalizeAnnouncement(announcement) {
|
||||||
const normalAnnouncement = { ...announcement };
|
const normalAnnouncement = { ...announcement };
|
||||||
const emojiMap = makeEmojiMap(normalAnnouncement);
|
const emojiMap = makeEmojiMap.emojis(normalAnnouncement);
|
||||||
|
|
||||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
import { IntlMessageFormat } from 'intl-messageformat';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
|
@ -345,9 +345,10 @@ export const translateStatusFail = (id, error) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const undoStatusTranslation = id => ({
|
export const undoStatusTranslation = (id, pollId) => ({
|
||||||
type: STATUS_TRANSLATE_UNDO,
|
type: STATUS_TRANSLATE_UNDO,
|
||||||
id,
|
id,
|
||||||
|
pollId,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateEmojiReaction = (emoji_reaction) => ({
|
export const updateEmojiReaction = (emoji_reaction) => ({
|
||||||
|
|
91
app/javascript/mastodon/components/admin/ImpactReport.jsx
Normal file
91
app/javascript/mastodon/components/admin/ImpactReport.jsx
Normal file
|
@ -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 (
|
||||||
|
<div className='dimension'>
|
||||||
|
<h4><FormattedMessage id='admin.impact_report.title' defaultMessage='Impact summary' /></h4>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr className='dimension__item'>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<FormattedMessage id='admin.impact_report.instance_accounts' defaultMessage='Accounts profiles this would delete' />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[0].total} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr className={classNames('dimension__item', { negative: !loading && data[1].total > 0 })}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<FormattedMessage id='admin.impact_report.instance_follows' defaultMessage='Followers their users would lose' />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[1].total} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr className={classNames('dimension__item', { negative: !loading && data[2].total > 0 })}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<FormattedMessage id='admin.impact_report.instance_followers' defaultMessage='Followers our users would lose' />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
{loading ? <Skeleton width={60} /> : <FormattedNumber value={data[2].total} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import type { InjectedIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
|
@ -15,9 +14,11 @@ const messages = defineMessages({
|
||||||
interface Props {
|
interface Props {
|
||||||
domain: string;
|
domain: string;
|
||||||
onUnblockDomain: (domain: string) => void;
|
onUnblockDomain: (domain: string) => void;
|
||||||
intl: InjectedIntl;
|
|
||||||
}
|
}
|
||||||
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
|
|
||||||
|
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleDomainUnblock = useCallback(() => {
|
const handleDomainUnblock = useCallback(() => {
|
||||||
onUnblockDomain(domain);
|
onUnblockDomain(domain);
|
||||||
}, [domain, onUnblockDomain]);
|
}, [domain, onUnblockDomain]);
|
||||||
|
@ -41,5 +42,3 @@ const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Domain = injectIntl(_Domain);
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import type { InjectedIntl } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
@ -13,10 +12,11 @@ interface Props {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
maxId: string;
|
maxId: string;
|
||||||
onClick: (maxId: string) => void;
|
onClick: (maxId: string) => void;
|
||||||
intl: InjectedIntl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
|
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onClick(maxId);
|
onClick(maxId);
|
||||||
}, [maxId, onClick]);
|
}, [maxId, onClick]);
|
||||||
|
@ -32,5 +32,3 @@ const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LoadGap = injectIntl(_LoadGap);
|
|
||||||
|
|
|
@ -51,8 +51,9 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, lang, width, height } = this.props;
|
const { status, width, height } = this.props;
|
||||||
const mediaAttachments = status.get('media_attachments');
|
const mediaAttachments = status.get('media_attachments');
|
||||||
|
const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;
|
||||||
|
|
||||||
if (mediaAttachments.size === 0) {
|
if (mediaAttachments.size === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -60,14 +61,15 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
|
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
|
||||||
const audio = mediaAttachments.get(0);
|
const audio = mediaAttachments.get(0);
|
||||||
|
const description = audio.getIn(['translation', 'description']) || audio.get('description');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
{Component => (
|
{Component => (
|
||||||
<Component
|
<Component
|
||||||
src={audio.get('url')}
|
src={audio.get('url')}
|
||||||
alt={audio.get('description')}
|
alt={description}
|
||||||
lang={lang || status.get('language')}
|
lang={language}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
|
@ -81,6 +83,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
|
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
|
||||||
const video = mediaAttachments.get(0);
|
const video = mediaAttachments.get(0);
|
||||||
|
const description = video.getIn(['translation', 'description']) || video.get('description');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
|
@ -90,8 +93,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
||||||
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
|
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
blurhash={video.get('blurhash')}
|
blurhash={video.get('blurhash')}
|
||||||
src={video.get('url')}
|
src={video.get('url')}
|
||||||
alt={video.get('description')}
|
alt={description}
|
||||||
lang={lang || status.get('language')}
|
lang={language}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
inline
|
inline
|
||||||
|
@ -107,7 +110,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
||||||
{Component => (
|
{Component => (
|
||||||
<Component
|
<Component
|
||||||
media={mediaAttachments}
|
media={mediaAttachments}
|
||||||
lang={lang || status.get('language')}
|
lang={language}
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
defaultWidth={width}
|
defaultWidth={width}
|
||||||
height={height}
|
height={height}
|
||||||
|
|
|
@ -121,10 +121,12 @@ class Item extends PureComponent {
|
||||||
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
|
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
|
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||||
<Blurhash
|
<Blurhash
|
||||||
hash={attachment.get('blurhash')}
|
hash={attachment.get('blurhash')}
|
||||||
className='media-gallery__preview'
|
className='media-gallery__preview'
|
||||||
|
@ -162,8 +164,8 @@ class Item extends PureComponent {
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
srcSet={srcSet}
|
srcSet={srcSet}
|
||||||
sizes={sizes}
|
sizes={sizes}
|
||||||
alt={attachment.get('description')}
|
alt={description}
|
||||||
title={attachment.get('description')}
|
title={description}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
style={{ objectPosition: `${x}% ${y}%` }}
|
style={{ objectPosition: `${x}% ${y}%` }}
|
||||||
onLoad={this.handleImageLoad}
|
onLoad={this.handleImageLoad}
|
||||||
|
@ -179,8 +181,8 @@ class Item extends PureComponent {
|
||||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||||
<video
|
<video
|
||||||
className='media-gallery__item-gifv-thumbnail'
|
className='media-gallery__item-gifv-thumbnail'
|
||||||
aria-label={attachment.get('description')}
|
aria-label={description}
|
||||||
title={attachment.get('description')}
|
title={description}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
role='application'
|
role='application'
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
|
|
|
@ -138,10 +138,12 @@ class Poll extends ImmutablePureComponent {
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
const active = !!this.state.selected[`${optionIndex}`];
|
||||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||||
|
|
||||||
let titleEmojified = option.get('title_emojified');
|
const title = option.getIn(['translation', 'title']) || option.get('title');
|
||||||
if (!titleEmojified) {
|
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
|
||||||
|
|
||||||
|
if (!titleHtml) {
|
||||||
const emojiMap = makeEmojiMap(poll);
|
const emojiMap = makeEmojiMap(poll);
|
||||||
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -163,7 +165,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||||
onKeyPress={this.handleOptionKeyPress}
|
onKeyPress={this.handleOptionKeyPress}
|
||||||
aria-checked={active}
|
aria-checked={active}
|
||||||
aria-label={option.get('title')}
|
aria-label={title}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
data-index={optionIndex}
|
data-index={optionIndex}
|
||||||
/>
|
/>
|
||||||
|
@ -182,7 +184,7 @@ class Poll extends ImmutablePureComponent {
|
||||||
<span
|
<span
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
lang={lang}
|
lang={lang}
|
||||||
dangerouslySetInnerHTML={{ __html: titleEmojified }}
|
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!!voted && <span className='poll__voted'>
|
{!!voted && <span className='poll__voted'>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
import type { InjectedIntl } from 'react-intl';
|
import type { IntlShape } from 'react-intl';
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -103,7 +103,7 @@ const getUnitDelay = (units: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const timeAgoString = (
|
export const timeAgoString = (
|
||||||
intl: InjectedIntl,
|
intl: IntlShape,
|
||||||
date: Date,
|
date: Date,
|
||||||
now: number,
|
now: number,
|
||||||
year: number,
|
year: number,
|
||||||
|
@ -155,7 +155,7 @@ export const timeAgoString = (
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeRemainingString = (
|
const timeRemainingString = (
|
||||||
intl: InjectedIntl,
|
intl: IntlShape,
|
||||||
date: Date,
|
date: Date,
|
||||||
now: number,
|
now: number,
|
||||||
timeGiven = true
|
timeGiven = true
|
||||||
|
@ -190,7 +190,7 @@ const timeRemainingString = (
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
intl: InjectedIntl;
|
intl: IntlShape;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
year: number;
|
year: number;
|
||||||
futureDate?: boolean;
|
futureDate?: boolean;
|
||||||
|
@ -201,7 +201,7 @@ interface States {
|
||||||
}
|
}
|
||||||
class RelativeTimestamp extends Component<Props, States> {
|
class RelativeTimestamp extends Component<Props, States> {
|
||||||
state = {
|
state = {
|
||||||
now: this.props.intl.now(),
|
now: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -223,7 +223,7 @@ class RelativeTimestamp extends Component<Props, States> {
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
||||||
if (this.props.timestamp !== nextProps.timestamp) {
|
if (this.props.timestamp !== nextProps.timestamp) {
|
||||||
this.setState({ now: this.props.intl.now() });
|
this.setState({ now: Date.now() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +253,7 @@ class RelativeTimestamp extends Component<Props, States> {
|
||||||
: Math.max(updateInterval, unitRemainder);
|
: Math.max(updateInterval, unitRemainder);
|
||||||
|
|
||||||
this._timer = window.setTimeout(() => {
|
this._timer = window.setTimeout(() => {
|
||||||
this.setState({ now: this.props.intl.now() });
|
this.setState({ now: Date.now() });
|
||||||
}, delay);
|
}, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,18 @@ import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusEmojiReactionsBar from './status_emoji_reactions_bar';
|
import StatusEmojiReactionsBar from './status_emoji_reactions_bar';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||||
const displayName = status.getIn(['account', 'display_name']);
|
const displayName = status.getIn(['account', 'display_name']);
|
||||||
|
|
||||||
|
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
|
||||||
|
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
|
||||||
|
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
|
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
|
||||||
status.get('spoiler_text') && status.get('hidden') ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
|
spoilerText && status.get('hidden') ? spoilerText : contentText,
|
||||||
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
|
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
|
||||||
status.getIn(['account', 'acct']),
|
status.getIn(['account', 'acct']),
|
||||||
];
|
];
|
||||||
|
@ -206,12 +212,14 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleOpenVideo = (options) => {
|
handleOpenVideo = (options) => {
|
||||||
const status = this._properStatus();
|
const status = this._properStatus();
|
||||||
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleOpenMedia = (media, index) => {
|
handleOpenMedia = (media, index) => {
|
||||||
const status = this._properStatus();
|
const status = this._properStatus();
|
||||||
this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
this.props.onOpenMedia(status.get('id'), media, index, lang);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyOpenMedia = e => {
|
handleHotkeyOpenMedia = e => {
|
||||||
|
@ -221,7 +229,7 @@ class Status extends ImmutablePureComponent {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (status.get('media_attachments').size > 0) {
|
||||||
const lang = status.get('language');
|
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
|
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
|
||||||
} else {
|
} else {
|
||||||
|
@ -439,6 +447,8 @@ class Status extends ImmutablePureComponent {
|
||||||
if (pictureInPicture.get('inUse')) {
|
if (pictureInPicture.get('inUse')) {
|
||||||
media = <PictureInPicturePlaceholder />;
|
media = <PictureInPicturePlaceholder />;
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
if (this.props.muted) {
|
if (this.props.muted) {
|
||||||
media = (
|
media = (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
|
@ -448,14 +458,15 @@ class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||||
{Component => (
|
{Component => (
|
||||||
<Component
|
<Component
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={attachment.get('description')}
|
alt={description}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
|
@ -475,6 +486,7 @@ class Status extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||||
|
@ -484,8 +496,8 @@ class Status extends ImmutablePureComponent {
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={attachment.get('description')}
|
alt={description}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
inline
|
inline
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
@ -502,7 +514,7 @@ class Status extends ImmutablePureComponent {
|
||||||
{Component => (
|
{Component => (
|
||||||
<Component
|
<Component
|
||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
height={110}
|
height={110}
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
|
|
@ -231,11 +231,11 @@ class StatusContent extends PureComponent {
|
||||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
||||||
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
|
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||||
|
|
||||||
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
|
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
|
||||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||||
const lang = status.get('translation') ? intl.locale : status.get('language');
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
'status__content--with-action': this.props.onClick && this.context.router,
|
'status__content--with-action': this.props.onClick && this.context.router,
|
||||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||||
|
@ -253,7 +253,7 @@ class StatusContent extends PureComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
const poll = !!status.get('poll') && (
|
const poll = !!status.get('poll') && (
|
||||||
<PollContainer pollId={status.get('poll')} lang={status.get('language')} />
|
<PollContainer pollId={status.get('poll')} lang={language} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status.get('spoiler_text').length > 0) {
|
if (status.get('spoiler_text').length > 0) {
|
||||||
|
@ -274,24 +274,24 @@ class StatusContent extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
|
||||||
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} />
|
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
|
||||||
{' '}
|
{' '}
|
||||||
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
|
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{mentionsPlaceholder}
|
{mentionsPlaceholder}
|
||||||
|
|
||||||
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={lang} dangerouslySetInnerHTML={content} />
|
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={language} dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{!hidden && poll}
|
{!hidden && poll}
|
||||||
{!hidden && translateButton}
|
{translateButton}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (this.props.onClick) {
|
} else if (this.props.onClick) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
|
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{poll}
|
{poll}
|
||||||
{translateButton}
|
{translateButton}
|
||||||
|
@ -303,7 +303,7 @@ class StatusContent extends PureComponent {
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
|
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
{poll}
|
{poll}
|
||||||
{translateButton}
|
{translateButton}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { getLocale } from '../locales';
|
import { getLocale, onProviderError } from '../locales';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
export default class AdminComponent extends PureComponent {
|
export default class AdminComponent extends PureComponent {
|
||||||
|
|
||||||
|
@ -19,7 +18,7 @@ export default class AdminComponent extends PureComponent {
|
||||||
const { locale, children } = this.props;
|
const { locale, children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
|
||||||
{children}
|
{children}
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
@ -9,11 +9,10 @@ import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||||
import { hydrateStore } from '../actions/store';
|
import { hydrateStore } from '../actions/store';
|
||||||
import Compose from '../features/standalone/compose';
|
import Compose from '../features/standalone/compose';
|
||||||
import initialState from '../initial_state';
|
import initialState from '../initial_state';
|
||||||
import { getLocale } from '../locales';
|
import { getLocale, onProviderError } from '../locales';
|
||||||
import { store } from '../store';
|
import { store } from '../store';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
if (initialState) {
|
if (initialState) {
|
||||||
store.dispatch(hydrateStore(initialState));
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
@ -31,7 +30,7 @@ export default class TimelineContainer extends PureComponent {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Compose />
|
<Compose />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { BrowserRouter, Route } from 'react-router-dom';
|
import { BrowserRouter, Route } from 'react-router-dom';
|
||||||
|
@ -17,11 +17,10 @@ import { connectUserStream } from 'mastodon/actions/streaming';
|
||||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||||
import UI from 'mastodon/features/ui';
|
import UI from 'mastodon/features/ui';
|
||||||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale, onProviderError } from 'mastodon/locales';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ export default class Mastodon extends PureComponent {
|
||||||
const { locale } = this.props;
|
const { locale } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
|
|
||||||
import { fromJS } from 'immutable';
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
@ -14,11 +14,10 @@ import Audio from 'mastodon/features/audio';
|
||||||
import Card from 'mastodon/features/status/components/card';
|
import Card from 'mastodon/features/status/components/card';
|
||||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
||||||
import Video from 'mastodon/features/video';
|
import Video from 'mastodon/features/video';
|
||||||
import { getLocale } from 'mastodon/locales';
|
import { getLocale, onProviderError } from 'mastodon/locales';
|
||||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
const { messages } = getLocale();
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
|
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
|
||||||
|
|
||||||
|
@ -84,7 +83,7 @@ export default class MediaContainer extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
<IntlProvider locale={locale} messages={messages} onError={onProviderError}>
|
||||||
<>
|
<>
|
||||||
{[].map.call(components, (component, i) => {
|
{[].map.call(components, (component, i) => {
|
||||||
const componentName = component.getAttribute('data-component');
|
const componentName = component.getAttribute('data-component');
|
||||||
|
|
|
@ -194,7 +194,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
|
|
||||||
onTranslate (status) {
|
onTranslate (status) {
|
||||||
if (status.get('translation')) {
|
if (status.get('translation')) {
|
||||||
dispatch(undoStatusTranslation(status.get('id')));
|
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(translateStatus(status.get('id')));
|
dispatch(translateStatus(status.get('id')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, FormattedHTMLMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -77,7 +77,7 @@ class Follows extends PureComponent {
|
||||||
{loadedContent}
|
{loadedContent}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' /></p>
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
|
||||||
|
|
||||||
<div className='onboarding__footer'>
|
<div className='onboarding__footer'>
|
||||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
|
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedHTMLMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
@ -168,9 +168,9 @@ class Share extends PureComponent {
|
||||||
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
||||||
|
|
||||||
<TipCarousel>
|
<TipCarousel>
|
||||||
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' /></p></div>
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain }} /></p></div>
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
<div><p className='onboarding__lead'><FormattedHTMLMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' /></p></div>
|
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
|
||||||
</TipCarousel>
|
</TipCarousel>
|
||||||
|
|
||||||
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
|
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
|
||||||
|
|
|
@ -143,17 +143,20 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
if (pictureInPicture.get('inUse')) {
|
if (pictureInPicture.get('inUse')) {
|
||||||
media = <PictureInPicturePlaceholder />;
|
media = <PictureInPicturePlaceholder />;
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Audio
|
<Audio
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={attachment.get('description')}
|
alt={description}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
|
@ -168,6 +171,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
);
|
);
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
media = (
|
media = (
|
||||||
<Video
|
<Video
|
||||||
|
@ -175,8 +179,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
blurhash={attachment.get('blurhash')}
|
blurhash={attachment.get('blurhash')}
|
||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={attachment.get('description')}
|
alt={description}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
width={300}
|
width={300}
|
||||||
height={150}
|
height={150}
|
||||||
inline
|
inline
|
||||||
|
@ -192,7 +196,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
standalone
|
standalone
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
media={status.get('media_attachments')}
|
media={status.get('media_attachments')}
|
||||||
lang={status.get('language')}
|
lang={language}
|
||||||
height={300}
|
height={300}
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
visible={this.props.showMedia}
|
visible={this.props.showMedia}
|
||||||
|
|
|
@ -456,7 +456,7 @@ class Status extends ImmutablePureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
if (status.get('translation')) {
|
if (status.get('translation')) {
|
||||||
dispatch(undoStatusTranslation(status.get('id')));
|
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(translateStatus(status.get('id')));
|
dispatch(translateStatus(status.get('id')));
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Audio from 'mastodon/features/audio';
|
||||||
import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
||||||
|
|
||||||
const mapStateToProps = (state, { statusId }) => ({
|
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']),
|
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ class AudioModal extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
statusId: PropTypes.string.isRequired,
|
statusId: PropTypes.string.isRequired,
|
||||||
language: PropTypes.string,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
accountStaticAvatar: PropTypes.string.isRequired,
|
accountStaticAvatar: PropTypes.string.isRequired,
|
||||||
options: PropTypes.shape({
|
options: PropTypes.shape({
|
||||||
autoPlay: PropTypes.bool,
|
autoPlay: PropTypes.bool,
|
||||||
|
@ -27,15 +27,17 @@ class AudioModal extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
|
const { media, status, accountStaticAvatar, onClose } = this.props;
|
||||||
const options = this.props.options || {};
|
const options = this.props.options || {};
|
||||||
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
const description = media.getIn(['translation', 'description']) || media.get('description');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal audio-modal'>
|
<div className='modal-root__modal audio-modal'>
|
||||||
<div className='audio-modal__container'>
|
<div className='audio-modal__container'>
|
||||||
<Audio
|
<Audio
|
||||||
src={media.get('url')}
|
src={media.get('url')}
|
||||||
alt={media.get('description')}
|
alt={description}
|
||||||
lang={language}
|
lang={language}
|
||||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
height={150}
|
height={150}
|
||||||
|
@ -48,7 +50,7 @@ class AudioModal extends ImmutablePureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='media-modal__overlay'>
|
<div className='media-modal__overlay'>
|
||||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -145,6 +145,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||||
const content = media.map((image) => {
|
const content = media.map((image) => {
|
||||||
const width = image.getIn(['meta', 'original', 'width']) || null;
|
const width = image.getIn(['meta', 'original', 'width']) || null;
|
||||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||||
|
const description = image.getIn(['translation', 'description']) || image.get('description');
|
||||||
|
|
||||||
if (image.get('type') === 'image') {
|
if (image.get('type') === 'image') {
|
||||||
return (
|
return (
|
||||||
|
@ -153,7 +154,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||||
src={image.get('url')}
|
src={image.get('url')}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
alt={image.get('description')}
|
alt={description}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
key={image.get('url')}
|
key={image.get('url')}
|
||||||
onClick={this.toggleNavigation}
|
onClick={this.toggleNavigation}
|
||||||
|
@ -176,7 +177,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||||
volume={volume || 1}
|
volume={volume || 1}
|
||||||
onCloseVideo={onClose}
|
onCloseVideo={onClose}
|
||||||
detailed
|
detailed
|
||||||
alt={image.get('description')}
|
alt={description}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
key={image.get('url')}
|
key={image.get('url')}
|
||||||
/>
|
/>
|
||||||
|
@ -188,7 +189,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
key={image.get('url')}
|
key={image.get('url')}
|
||||||
alt={image.get('description')}
|
alt={description}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
onClick={this.toggleNavigation}
|
onClick={this.toggleNavigation}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
||||||
import Video from 'mastodon/features/video';
|
import Video from 'mastodon/features/video';
|
||||||
|
|
||||||
const mapStateToProps = (state, { statusId }) => ({
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
language: state.getIn(['statuses', statusId, 'language']),
|
status: state.getIn(['statuses', statusId]),
|
||||||
});
|
});
|
||||||
|
|
||||||
class VideoModal extends ImmutablePureComponent {
|
class VideoModal extends ImmutablePureComponent {
|
||||||
|
@ -17,7 +17,7 @@ class VideoModal extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
media: ImmutablePropTypes.map.isRequired,
|
||||||
statusId: PropTypes.string,
|
statusId: PropTypes.string,
|
||||||
language: PropTypes.string,
|
status: ImmutablePropTypes.map,
|
||||||
options: PropTypes.shape({
|
options: PropTypes.shape({
|
||||||
startTime: PropTypes.number,
|
startTime: PropTypes.number,
|
||||||
autoPlay: PropTypes.bool,
|
autoPlay: PropTypes.bool,
|
||||||
|
@ -38,8 +38,10 @@ class VideoModal extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, statusId, language, onClose } = this.props;
|
const { media, status, onClose } = this.props;
|
||||||
const options = this.props.options || {};
|
const options = this.props.options || {};
|
||||||
|
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
const description = media.getIn(['translation', 'description']) || media.get('description');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal video-modal'>
|
<div className='modal-root__modal video-modal'>
|
||||||
|
@ -55,13 +57,13 @@ class VideoModal extends ImmutablePureComponent {
|
||||||
onCloseVideo={onClose}
|
onCloseVideo={onClose}
|
||||||
autoFocus
|
autoFocus
|
||||||
detailed
|
detailed
|
||||||
alt={media.get('description')}
|
alt={description}
|
||||||
lang={language}
|
lang={language}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='media-modal__overlay'>
|
<div className='media-modal__overlay'>
|
||||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
14
app/javascript/mastodon/load_locale.js
Normal file
14
app/javascript/mastodon/load_locale.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { setLocale } from "./locales";
|
||||||
|
|
||||||
|
export async function loadLocale() {
|
||||||
|
const locale = document.querySelector('html').lang || 'en';
|
||||||
|
|
||||||
|
const localeData = await import(
|
||||||
|
/* webpackMode: "lazy" */
|
||||||
|
/* webpackChunkName: "locale/[request]" */
|
||||||
|
/* webpackInclude: /\.json$/ */
|
||||||
|
/* webpackPreload: true */
|
||||||
|
`mastodon/locales/${locale}.json`);
|
||||||
|
|
||||||
|
setLocale({ messages: localeData });
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -75,6 +75,10 @@
|
||||||
"admin.dashboard.retention.average": "Average",
|
"admin.dashboard.retention.average": "Average",
|
||||||
"admin.dashboard.retention.cohort": "Sign-up month",
|
"admin.dashboard.retention.cohort": "Sign-up month",
|
||||||
"admin.dashboard.retention.cohort_size": "New users",
|
"admin.dashboard.retention.cohort_size": "New users",
|
||||||
|
"admin.impact_report.instance_accounts": "Accounts profiles this would delete",
|
||||||
|
"admin.impact_report.instance_followers": "Followers our users would lose",
|
||||||
|
"admin.impact_report.instance_follows": "Followers their users would lose",
|
||||||
|
"admin.impact_report.title": "Impact summary",
|
||||||
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
|
"alert.rate_limited.message": "Please retry after {retry_time, time, medium}.",
|
||||||
"alert.rate_limited.title": "Rate limited",
|
"alert.rate_limited.title": "Rate limited",
|
||||||
"alert.unexpected.message": "An unexpected error occurred.",
|
"alert.unexpected.message": "An unexpected error occurred.",
|
||||||
|
|
|
@ -7,3 +7,16 @@ export function setLocale(locale) {
|
||||||
export function getLocale() {
|
export function getLocale() {
|
||||||
return theLocale;
|
return theLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onProviderError(error) {
|
||||||
|
// 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(typeof error === "object" && error.message.match("MISSING_DATA")) {
|
||||||
|
console.warn(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,5 +0,0 @@
|
||||||
[
|
|
||||||
"account.badges.bot",
|
|
||||||
"compose_form.publish_loud",
|
|
||||||
"search_results.hashtags"
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[
|
|
||||||
]
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue