Merge remote-tracking branch 'origin/kb_migration' into kb_development
This commit is contained in:
commit
aa86bf9e96
309 changed files with 4605 additions and 6803 deletions
12
.eslintrc.js
12
.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: {
|
||||
|
@ -323,7 +333,7 @@ module.exports = {
|
|||
'plugin:import/recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:promise/recommended',
|
||||
'plugin:jsdoc/recommended',
|
||||
'plugin:jsdoc/recommended-typescript',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
|
||||
|
|
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
|
@ -25,18 +25,6 @@ updates:
|
|||
- dependency-name: 'react-hotkeys'
|
||||
versions:
|
||||
- '>= 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
|
||||
- dependency-name: 'webpack-dev-server'
|
||||
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
|
||||
run: |
|
||||
yarn build:development
|
||||
yarn manage:translations en
|
||||
yarn i18n:extract --throws
|
||||
git diff --exit-code
|
||||
|
||||
- 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"
|
||||
|
||||
- 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
|
||||
|
||||
- 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
|
||||
|
|
2
.github/workflows/lint-json.yml
vendored
2
.github/workflows/lint-json.yml
vendored
|
@ -40,4 +40,4 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- 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/**'
|
||||
paths:
|
||||
- '.github/workflows/lint-md.yml'
|
||||
- '.nvmrc'
|
||||
- '.prettier*'
|
||||
- '**/*.md'
|
||||
- '!AUTHORS.md'
|
||||
|
@ -14,6 +15,7 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/lint-md.yml'
|
||||
- '.nvmrc'
|
||||
- '.prettier*'
|
||||
- '**/*.md'
|
||||
- '!AUTHORS.md'
|
||||
|
@ -32,9 +34,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
|
||||
|
|
2
.github/workflows/lint-yml.yml
vendored
2
.github/workflows/lint-yml.yml
vendored
|
@ -42,4 +42,4 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- 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
|
||||
|
||||
- name: Jest testing
|
||||
run: yarn test:jest --reporters github-actions summary
|
||||
run: yarn jest --reporters github-actions summary
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
122
.rubocop.yml
122
.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
|
||||
|
@ -53,6 +54,28 @@ Lint/UselessAccessModifier:
|
|||
ContextCreatingMethods:
|
||||
- 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
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize
|
||||
Metrics/AbcSize:
|
||||
|
@ -61,95 +84,12 @@ Metrics/AbcSize:
|
|||
- 'lib/mastodon/cli/*.rb'
|
||||
- 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:
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocknesting
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
- '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
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity
|
||||
Metrics/CyclomaticComplexity:
|
||||
|
@ -160,17 +100,10 @@ Metrics/CyclomaticComplexity:
|
|||
- lib/mastodon/cli/*.rb
|
||||
- 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:
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength
|
||||
Metrics/ModuleLength:
|
||||
CountAsOne: [array, heredoc]
|
||||
# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists
|
||||
Metrics/ParameterLists:
|
||||
CountKeywordArgs: false
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Exclude:
|
||||
|
@ -281,3 +214,6 @@ Style/TrailingCommaInArrayLiteral:
|
|||
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
EnforcedStyleForMultiline: 'comma'
|
||||
|
||||
Style/MiddleDot:
|
||||
Enabled: true
|
||||
|
|
|
@ -154,12 +154,6 @@ Lint/Void:
|
|||
Metrics/AbcSize:
|
||||
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.
|
||||
Metrics/BlockNesting:
|
||||
Exclude:
|
||||
|
@ -169,27 +163,6 @@ Metrics/BlockNesting:
|
|||
Metrics/CyclomaticComplexity:
|
||||
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.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 28
|
||||
|
|
7
Gemfile
7
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'
|
||||
|
|
54
Gemfile.lock
54
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)
|
||||
|
|
|
@ -31,17 +31,23 @@ 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
|
||||
return render :new
|
||||
end
|
||||
|
||||
# Allow transparently upgrading a domain block
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params)
|
||||
@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
|
||||
|
@ -50,12 +56,16 @@ module Admin
|
|||
render :new
|
||||
end
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -44,6 +44,6 @@ class Api::V1::ListsController < Api::BaseController
|
|||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:title, :replies_policy)
|
||||
params.permit(:title, :replies_policy, :exclusive)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -173,11 +173,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")
|
||||
|
@ -248,7 +248,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
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Metrics/ModuleLength
|
||||
|
||||
module LanguagesHelper
|
||||
ISO_639_1 = {
|
||||
aa: ['Afar', 'Afaraf'].freeze,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
@ -98,7 +98,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(/<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.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||
|
@ -120,22 +120,48 @@ export function normalizeEmojiReactions(emoji_reactions) {
|
|||
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) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -345,9 +345,10 @@ export const translateStatusFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const undoStatusTranslation = id => ({
|
||||
export const undoStatusTranslation = (id, pollId) => ({
|
||||
type: STATUS_TRANSLATE_UNDO,
|
||||
id,
|
||||
pollId,
|
||||
});
|
||||
|
||||
export const updateEmojiReaction = (emoji_reaction) => ({
|
||||
|
|
|
@ -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
|
||||
|
@ -125,6 +125,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Function} dispatch
|
||||
|
|
|
@ -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 {
|
||||
|
@ -43,6 +44,7 @@ class Account extends ImmutablePureComponent {
|
|||
onMuteNotifications: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
hideButtons: PropTypes.bool,
|
||||
minimal: PropTypes.bool,
|
||||
actionIcon: PropTypes.string,
|
||||
actionTitle: PropTypes.string,
|
||||
|
@ -80,7 +82,7 @@ class Account extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
|
||||
const { account, intl, hidden, hideButtons, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return <EmptyAccount size={size} minimal={minimal} />;
|
||||
|
@ -97,39 +99,39 @@ class Account extends ImmutablePureComponent {
|
|||
|
||||
let buttons;
|
||||
|
||||
if (actionIcon) {
|
||||
if (onActionClick) {
|
||||
if (actionIcon && onActionClick) {
|
||||
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
|
||||
}
|
||||
} else if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
} else if (!hideButtons && !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 = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
|
||||
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={this.handleFollow} />;
|
||||
} else if (blocking) {
|
||||
buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
} else if (muting) {
|
||||
let hidingNotificationsButton;
|
||||
|
||||
if (account.getIn(['relationship', 'muting_notifications'])) {
|
||||
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
|
||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.unmute_notifications)} onClick={this.handleUnmuteNotifications} />;
|
||||
} else {
|
||||
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
|
||||
hidingNotificationsButton = <Button text={intl.formatMessage(messages.mute_notifications)} onClick={this.handleMuteNotifications} />;
|
||||
}
|
||||
|
||||
buttons = (
|
||||
<>
|
||||
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
|
||||
<Button text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />
|
||||
{hidingNotificationsButton}
|
||||
</>
|
||||
);
|
||||
} else if (defaultAction === 'mute') {
|
||||
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
|
||||
buttons = <Button title={intl.formatMessage(messages.mute)} onClick={this.handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <IconButton icon='lock' title={intl.formatMessage(messages.block, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={this.handleBlock} />;
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +146,7 @@ class Account extends ImmutablePureComponent {
|
|||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
||||
|
||||
if (firstVerifiedField) {
|
||||
verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} /></>;
|
||||
verification = <VerifiedBadge link={firstVerifiedField.get('value')} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -155,9 +157,13 @@ class Account extends ImmutablePureComponent {
|
|||
<Avatar account={account} size={size} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
{!minimal && <><ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}</>}
|
||||
{!minimal && (
|
||||
<div className='account__details'>
|
||||
<ShortNumber value={account.get('followers_count')} isHide={account.getIn(['other_settings', 'hide_followers_count']) || false} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
|
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 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<Props> = ({ domain, onUnblockDomain, intl }) => {
|
||||
|
||||
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDomainUnblock = useCallback(() => {
|
||||
onUnblockDomain(domain);
|
||||
}, [domain, onUnblockDomain]);
|
||||
|
@ -41,5 +42,3 @@ const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Domain = injectIntl(_Domain);
|
||||
|
|
|
@ -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<Props> = ({ disabled, maxId, onClick, intl }) => {
|
||||
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(maxId);
|
||||
}, [maxId, onClick]);
|
||||
|
@ -32,5 +32,3 @@ const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
|
|||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadGap = injectIntl(_LoadGap);
|
||||
|
|
|
@ -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 (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
lang={lang || status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
width={width}
|
||||
height={height}
|
||||
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') {
|
||||
const video = mediaAttachments.get(0);
|
||||
const description = video.getIn(['translation', 'description']) || video.get('description');
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
|
@ -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 => (
|
||||
<Component
|
||||
media={mediaAttachments}
|
||||
lang={lang || status.get('language')}
|
||||
lang={language}
|
||||
sensitive={status.get('sensitive')}
|
||||
defaultWidth={width}
|
||||
height={height}
|
||||
|
|
|
@ -121,10 +121,12 @@ class Item extends PureComponent {
|
|||
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') {
|
||||
return (
|
||||
<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
|
||||
hash={attachment.get('blurhash')}
|
||||
className='media-gallery__preview'
|
||||
|
@ -162,8 +164,8 @@ class Item extends PureComponent {
|
|||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
alt={description}
|
||||
title={description}
|
||||
lang={lang}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
|
@ -179,8 +181,8 @@ class Item extends PureComponent {
|
|||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
aria-label={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
aria-label={description}
|
||||
title={description}
|
||||
lang={lang}
|
||||
role='application'
|
||||
src={attachment.get('url')}
|
||||
|
|
|
@ -57,9 +57,9 @@ class Poll extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
const { poll, intl } = props;
|
||||
const { poll } = props;
|
||||
const expires_at = poll.get('expires_at');
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
|
||||
return (expired === state.expired) ? null : { expired };
|
||||
}
|
||||
|
||||
|
@ -76,10 +76,10 @@ class Poll extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
_setupTimer () {
|
||||
const { poll, intl } = this.props;
|
||||
const { poll } = this.props;
|
||||
clearTimeout(this._timer);
|
||||
if (!this.state.expired) {
|
||||
const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
|
||||
const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
|
||||
this._timer = setTimeout(() => {
|
||||
this.setState({ expired: true });
|
||||
}, delay);
|
||||
|
@ -138,10 +138,12 @@ class Poll extends ImmutablePureComponent {
|
|||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||
|
||||
let titleEmojified = option.get('title_emojified');
|
||||
if (!titleEmojified) {
|
||||
const title = option.getIn(['translation', 'title']) || option.get('title');
|
||||
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
|
||||
|
||||
if (!titleHtml) {
|
||||
const emojiMap = makeEmojiMap(poll);
|
||||
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -163,7 +165,7 @@ class Poll extends ImmutablePureComponent {
|
|||
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||
onKeyPress={this.handleOptionKeyPress}
|
||||
aria-checked={active}
|
||||
aria-label={option.get('title')}
|
||||
aria-label={title}
|
||||
lang={lang}
|
||||
data-index={optionIndex}
|
||||
/>
|
||||
|
@ -182,7 +184,7 @@ class Poll extends ImmutablePureComponent {
|
|||
<span
|
||||
className='poll__option__text translate'
|
||||
lang={lang}
|
||||
dangerouslySetInnerHTML={{ __html: titleEmojified }}
|
||||
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
||||
/>
|
||||
|
||||
{!!voted && <span className='poll__voted'>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component } from 'react';
|
||||
|
||||
import type { InjectedIntl } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -103,7 +103,7 @@ const getUnitDelay = (units: string) => {
|
|||
};
|
||||
|
||||
export const timeAgoString = (
|
||||
intl: InjectedIntl,
|
||||
intl: IntlShape,
|
||||
date: Date,
|
||||
now: number,
|
||||
year: number,
|
||||
|
@ -155,7 +155,7 @@ export const timeAgoString = (
|
|||
};
|
||||
|
||||
const timeRemainingString = (
|
||||
intl: InjectedIntl,
|
||||
intl: IntlShape,
|
||||
date: Date,
|
||||
now: number,
|
||||
timeGiven = true
|
||||
|
@ -190,7 +190,7 @@ const timeRemainingString = (
|
|||
};
|
||||
|
||||
interface Props {
|
||||
intl: InjectedIntl;
|
||||
intl: IntlShape;
|
||||
timestamp: string;
|
||||
year: number;
|
||||
futureDate?: boolean;
|
||||
|
@ -201,7 +201,7 @@ interface States {
|
|||
}
|
||||
class RelativeTimestamp extends Component<Props, States> {
|
||||
state = {
|
||||
now: this.props.intl.now(),
|
||||
now: Date.now(),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -223,7 +223,7 @@ class RelativeTimestamp extends Component<Props, States> {
|
|||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
||||
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);
|
||||
|
||||
this._timer = window.setTimeout(() => {
|
||||
this.setState({ now: this.props.intl.now() });
|
||||
this.setState({ now: Date.now() });
|
||||
}, delay);
|
||||
}
|
||||
|
||||
|
|
|
@ -28,12 +28,18 @@ import StatusActionBar from './status_action_bar';
|
|||
import StatusContent from './status_content';
|
||||
import StatusEmojiReactionsBar from './status_emoji_reactions_bar';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
|
||||
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 = [
|
||||
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' }),
|
||||
status.getIn(['account', 'acct']),
|
||||
];
|
||||
|
@ -206,12 +212,14 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleOpenVideo = (options) => {
|
||||
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) => {
|
||||
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 => {
|
||||
|
@ -221,7 +229,7 @@ class Status extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
|
||||
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') {
|
||||
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
|
||||
} else {
|
||||
|
@ -439,6 +447,8 @@ class Status extends ImmutablePureComponent {
|
|||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
<AttachmentList
|
||||
|
@ -448,14 +458,15 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
} 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 = (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
|
@ -475,6 +486,7 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
|
@ -484,8 +496,8 @@ class Status extends ImmutablePureComponent {
|
|||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
|
@ -502,7 +514,7 @@ class Status extends ImmutablePureComponent {
|
|||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
lang={status.get('language')}
|
||||
lang={language}
|
||||
sensitive={status.get('sensitive')}
|
||||
height={110}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
|
|
|
@ -231,11 +231,11 @@ class StatusContent extends PureComponent {
|
|||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||
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 spoilerContent = { __html: status.get('spoilerHtml') };
|
||||
const lang = status.get('translation') ? intl.locale : status.get('language');
|
||||
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
|
||||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': this.props.onClick && this.context.router,
|
||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||
|
@ -253,7 +253,7 @@ class StatusContent extends PureComponent {
|
|||
);
|
||||
|
||||
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) {
|
||||
|
@ -274,24 +274,24 @@ class StatusContent extends PureComponent {
|
|||
return (
|
||||
<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 }}>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
{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 && translateButton}
|
||||
{translateButton}
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.onClick) {
|
||||
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='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}
|
||||
{translateButton}
|
||||
|
@ -303,7 +303,7 @@ class StatusContent extends PureComponent {
|
|||
} else {
|
||||
return (
|
||||
<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}
|
||||
{translateButton}
|
||||
|
|
|
@ -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 (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
|
|
@ -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 (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<Provider store={store}>
|
||||
<Compose />
|
||||
</Provider>
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -17,12 +15,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);
|
||||
|
@ -43,10 +38,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,
|
||||
|
@ -82,10 +73,8 @@ export default class Mastodon extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { locale } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
|
|
|
@ -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 (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<>
|
||||
{[].map.call(components, (component, i) => {
|
||||
const componentName = component.getAttribute('data-component');
|
||||
|
|
|
@ -194,7 +194,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')));
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchEmojiReactions } from 'mastodon/actions/interactions';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { fetchEmojiReactions } from 'mastodon/actions/interactions';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import AccountContainer from 'mastodon/containers/account_container';
|
||||
import Column from 'mastodon/features/ui/components/column';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
|
||||
import EmojiView from '../../components/emoji_view';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -86,7 +92,7 @@ class EmojiReactions extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{Object.keys(groups).map((key) =>(
|
||||
<AccountContainer key={key} id={key} withNote={false}>
|
||||
<AccountContainer key={key} id={key} withNote={false} hideButtons>
|
||||
<div style={{ 'maxWidth': '100px' }}>
|
||||
{groups[key].map((value, index2) => <EmojiView key={index2} name={value.name} url={value.url} staticUrl={value.static_url} />)}
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,7 @@ class Explore extends PureComponent {
|
|||
<Search />
|
||||
</div>
|
||||
|
||||
<div className='scrollable scrollable--flex'>
|
||||
<div className='scrollable scrollable--flex' data-nosnippet>
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
|
|
|
@ -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';
|
||||
|
@ -142,10 +144,21 @@ class ListTimeline extends PureComponent {
|
|||
}));
|
||||
};
|
||||
|
||||
handleEditAntennaClick = (e) => {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
window.open(`/antennas/${id}/edit`, '_blank');
|
||||
}
|
||||
|
||||
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 +167,8 @@ 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;
|
||||
const antennas = list ? (list.get('antennas')?.toArray() || []) : [];
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
|
@ -191,6 +206,13 @@ class ListTimeline extends PureComponent {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ replies_policy !== undefined && (
|
||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||
|
@ -203,6 +225,23 @@ class ListTimeline extends PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ antennas.length > 0 && (
|
||||
<div>
|
||||
<span className='column-settings__section column-settings__section--with-margin'>
|
||||
<FormattedMessage id='lists.antennas' defaultMessage='Related antennas:' />
|
||||
</span>
|
||||
<ul className='column-settings__row'>
|
||||
{ antennas.map(antenna => (
|
||||
<li key={antenna.get('id')} className='column-settings__row__antenna'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' data-id={antenna.get('id')} onClick={this.handleEditAntennaClick}>
|
||||
{antenna.get('title')}{antenna.get('stl') && ' [STL]'}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
|
|
|
@ -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}
|
||||
</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'>
|
||||
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
|
||||
|
|
|
@ -120,7 +120,7 @@ class Onboarding extends ImmutablePureComponent {
|
|||
|
||||
<div className='onboarding__steps'>
|
||||
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
|
||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Follow {count, plural, one {one person} other {# people}}' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own feed. Let's fill it with interesting people." />} />
|
||||
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
|
||||
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' />} />
|
||||
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
|
||||
</div>
|
||||
|
|
|
@ -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 {
|
|||
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
|
||||
|
||||
<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'><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'><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.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'><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'><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>
|
||||
|
||||
<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`;
|
||||
}
|
||||
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media = <PictureInPicturePlaceholder />;
|
||||
} 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 = (
|
||||
<Audio
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
|
@ -168,6 +171,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media = (
|
||||
<Video
|
||||
|
@ -175,8 +179,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
width={300}
|
||||
height={150}
|
||||
inline
|
||||
|
@ -192,7 +196,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
standalone
|
||||
sensitive={status.get('sensitive')}
|
||||
media={status.get('media_attachments')}
|
||||
lang={status.get('language')}
|
||||
lang={language}
|
||||
height={300}
|
||||
onOpenMedia={this.props.onOpenMedia}
|
||||
visible={this.props.showMedia}
|
||||
|
@ -243,7 +247,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
} else if (this.context.router) {
|
||||
reblogLink = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Icon id={reblogIcon} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
|
@ -255,7 +259,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
} else {
|
||||
reblogLink = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<Icon id={reblogIcon} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
|
@ -309,7 +313,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
if (status.get('edited_at')) {
|
||||
edited = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -456,7 +456,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')));
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import Audio from 'mastodon/features/audio';
|
|||
import Footer from 'mastodon/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,
|
||||
|
@ -27,15 +27,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 (
|
||||
<div className='modal-root__modal audio-modal'>
|
||||
<div className='audio-modal__container'>
|
||||
<Audio
|
||||
src={media.get('url')}
|
||||
alt={media.get('description')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||
height={150}
|
||||
|
@ -48,7 +50,7 @@ class AudioModal extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -145,6 +145,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 (
|
||||
|
@ -153,7 +154,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}
|
||||
|
@ -176,7 +177,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
volume={volume || 1}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={image.get('description')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
/>
|
||||
|
@ -188,7 +189,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
width={width}
|
||||
height={height}
|
||||
key={image.get('url')}
|
||||
alt={image.get('description')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
onClick={this.toggleNavigation}
|
||||
/>
|
||||
|
|
|
@ -73,6 +73,7 @@ class NavigationPanel extends Component {
|
|||
<>
|
||||
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
|
||||
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -96,7 +97,6 @@ class NavigationPanel extends Component {
|
|||
<>
|
||||
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
|
||||
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
|
||||
<hr />
|
||||
|
||||
<ColumnLink transparent href='/settings/preferences' icon='cog' text={intl.formatMessage(messages.preferences)} />
|
||||
|
|
|
@ -9,7 +9,7 @@ import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
|||
import Video from 'mastodon/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 (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
|
@ -55,13 +57,13 @@ class VideoModal extends ImmutablePureComponent {
|
|||
onCloseVideo={onClose}
|
||||
autoFocus
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -79,7 +79,7 @@ export function Favourites () {
|
|||
}
|
||||
|
||||
export function EmojiReactions () {
|
||||
return import(/* webpackChunkName: "features/favourites" */'../../emoji_reactions');
|
||||
return import(/* webpackChunkName: "features/emoji_reactions" */'../../emoji_reactions');
|
||||
}
|
||||
|
||||
export function FollowRequests () {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -19,9 +19,10 @@
|
|||
"account.badges.group": "Group",
|
||||
"account.block": "Block @{name}",
|
||||
"account.block_domain": "Block domain {domain}",
|
||||
"account.block_short": "Block",
|
||||
"account.blocked": "Blocked",
|
||||
"account.browse_more_on_origin_server": "Browse more on the original profile",
|
||||
"account.cancel_follow_request": "Withdraw follow request",
|
||||
"account.cancel_follow_request": "Cancel follow",
|
||||
"account.direct": "Privately mention @{name}",
|
||||
"account.disable_notifications": "Stop notifying me when @{name} posts",
|
||||
"account.domain_blocked": "Domain blocked",
|
||||
|
@ -50,7 +51,8 @@
|
|||
"account.mention": "Mention @{name}",
|
||||
"account.moved_to": "{name} has indicated that their new account is now:",
|
||||
"account.mute": "Mute @{name}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||
"account.mute_notifications_short": "Mute notifications",
|
||||
"account.mute_short": "Mute",
|
||||
"account.muted": "Muted",
|
||||
"account.open_original_page": "Open original page",
|
||||
"account.posts": "Posts",
|
||||
|
@ -67,7 +69,7 @@
|
|||
"account.unendorse": "Don't feature on profile",
|
||||
"account.unfollow": "Unfollow",
|
||||
"account.unmute": "Unmute @{name}",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account.unmute_notifications_short": "Unmute notifications",
|
||||
"account.unmute_short": "Unmute",
|
||||
"account_note.placeholder": "Click to add note",
|
||||
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
|
||||
|
@ -75,6 +77,10 @@
|
|||
"admin.dashboard.retention.average": "Average",
|
||||
"admin.dashboard.retention.cohort": "Sign-up month",
|
||||
"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.title": "Rate limited",
|
||||
"alert.unexpected.message": "An unexpected error occurred.",
|
||||
|
@ -355,9 +361,11 @@
|
|||
"limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.",
|
||||
"lists.account.add": "Add to list",
|
||||
"lists.account.remove": "Remove from list",
|
||||
"lists.antennas": "Related antennas",
|
||||
"lists.delete": "Delete list",
|
||||
"lists.edit": "Edit list",
|
||||
"lists.edit.submit": "Change title",
|
||||
"lists.exclusive": "Hide these posts from home",
|
||||
"lists.new.create": "Add list",
|
||||
"lists.new.title_placeholder": "New list title",
|
||||
"lists.replies_policy.followed": "Any followed user",
|
||||
|
@ -465,8 +473,8 @@
|
|||
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
||||
"onboarding.start.skip": "Want to skip right ahead?",
|
||||
"onboarding.start.title": "You've made it!",
|
||||
"onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.",
|
||||
"onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}",
|
||||
"onboarding.steps.follow_people.body": "You curate your own home feed. Let's fill it with interesting people.",
|
||||
"onboarding.steps.follow_people.title": "Find at least {count, plural, one {one person} other {# people}} to follow",
|
||||
"onboarding.steps.publish_status.body": "Say hello to the world.",
|
||||
"onboarding.steps.publish_status.title": "Make your first post",
|
||||
"onboarding.steps.setup_profile.body": "Others are more likely to interact with you with a filled out profile.",
|
||||
|
|
22
app/javascript/mastodon/locales/global_locale.ts
Normal file
22
app/javascript/mastodon/locales/global_locale.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export interface LocaleData {
|
||||
locale: string;
|
||||
messages: Record<string, string>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
let theLocale;
|
||||
|
||||
export function setLocale(locale) {
|
||||
theLocale = locale;
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
return theLocale;
|
||||
}
|
5
app/javascript/mastodon/locales/index.ts
Normal file
5
app/javascript/mastodon/locales/index.ts
Normal file
|
@ -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';
|
57
app/javascript/mastodon/locales/intl_provider.tsx
Normal file
57
app/javascript/mastodon/locales/intl_provider.tsx
Normal file
|
@ -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<React.ComponentProps<typeof BaseIntlProvider>, '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 (
|
||||
<BaseIntlProvider
|
||||
locale={locale}
|
||||
messages={messages}
|
||||
onError={onProviderError}
|
||||
textComponent='span'
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</BaseIntlProvider>
|
||||
);
|
||||
};
|
|
@ -357,6 +357,7 @@
|
|||
"limited_account_hint.title": "このプロフィールは{domain}のモデレーターによって非表示にされています。",
|
||||
"lists.account.add": "リストに追加",
|
||||
"lists.account.remove": "リストから外す",
|
||||
"lists.antennas": "関連付けられたアンテナ",
|
||||
"lists.delete": "リストを削除",
|
||||
"lists.edit": "リストを編集",
|
||||
"lists.edit.submit": "タイトルを変更",
|
||||
|
|
29
app/javascript/mastodon/locales/load_locale.ts
Normal file
29
app/javascript/mastodon/locales/load_locale.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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<HTMLElement>('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 localeData = (await import(
|
||||
/* webpackMode: "lazy" */
|
||||
/* webpackChunkName: "locale/[request]" */
|
||||
/* webpackInclude: /\.json$/ */
|
||||
/* webpackPreload: true */
|
||||
`mastodon/locales/${locale}.json`
|
||||
)) as LocaleData['messages'];
|
||||
|
||||
setLocale({ messages: localeData, locale });
|
||||
});
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
# Custom Locale Data
|
||||
|
||||
This folder is used to store custom locale data. These custom locale data are
|
||||
not yet provided by [Unicode Common Locale Data Repository](http://cldr.unicode.org/development/new-cldr-developers)
|
||||
and hence not provided in [react-intl/locale-data/*](https://github.com/yahoo/react-intl).
|
||||
|
||||
The locale data should support [Locale Data APIs](https://github.com/yahoo/react-intl/wiki/API#locale-data-apis)
|
||||
of the react-intl library.
|
||||
|
||||
It is recommended to start your custom locale data from this sample English
|
||||
locale data ([*](#plural-rules)):
|
||||
|
||||
```javascript
|
||||
/*eslint eqeqeq: "off"*/
|
||||
/*eslint no-nested-ternary: "off"*/
|
||||
|
||||
export default [
|
||||
{
|
||||
locale: "en",
|
||||
pluralRuleFunction: function(e, a) {
|
||||
var n = String(e).split("."),
|
||||
l = !n[1],
|
||||
o = Number(n[0]) == e,
|
||||
t = o && n[0].slice(-1),
|
||||
r = o && n[0].slice(-2);
|
||||
return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other"
|
||||
},
|
||||
fields: {
|
||||
year: {
|
||||
displayName: "year",
|
||||
relative: {
|
||||
0: "this year",
|
||||
1: "next year",
|
||||
"-1": "last year"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} year",
|
||||
other: "in {0} years"
|
||||
},
|
||||
past: {
|
||||
one: "{0} year ago",
|
||||
other: "{0} years ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
month: {
|
||||
displayName: "month",
|
||||
relative: {
|
||||
0: "this month",
|
||||
1: "next month",
|
||||
"-1": "last month"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} month",
|
||||
other: "in {0} months"
|
||||
},
|
||||
past: {
|
||||
one: "{0} month ago",
|
||||
other: "{0} months ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
day: {
|
||||
displayName: "day",
|
||||
relative: {
|
||||
0: "today",
|
||||
1: "tomorrow",
|
||||
"-1": "yesterday"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} day",
|
||||
other: "in {0} days"
|
||||
},
|
||||
past: {
|
||||
one: "{0} day ago",
|
||||
other: "{0} days ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
hour: {
|
||||
displayName: "hour",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} hour",
|
||||
other: "in {0} hours"
|
||||
},
|
||||
past: {
|
||||
one: "{0} hour ago",
|
||||
other: "{0} hours ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
minute: {
|
||||
displayName: "minute",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} minute",
|
||||
other: "in {0} minutes"
|
||||
},
|
||||
past: {
|
||||
one: "{0} minute ago",
|
||||
other: "{0} minutes ago"
|
||||
}
|
||||
}
|
||||
},
|
||||
second: {
|
||||
displayName: "second",
|
||||
relative: {
|
||||
0: "now"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} second",
|
||||
other: "in {0} seconds"
|
||||
},
|
||||
past: {
|
||||
one: "{0} second ago",
|
||||
other: "{0} seconds ago"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### Plural Rules
|
||||
|
||||
The function `pluralRuleFunction()` should return the key to proper string of
|
||||
a plural form(s). The purpose of the function is to provide key of translate
|
||||
strings of correct plural form according. The different forms are described in
|
||||
[CLDR's Plural Rules][cldr-plural-rules],
|
||||
|
||||
[cldr-plural-rules]: http://cldr.unicode.org/index/cldr-spec/plural-rules
|
||||
|
||||
#### Quick Overview on CLDR Rules
|
||||
|
||||
Let's take English as an example.
|
||||
|
||||
When you describe a number, you can be either describe it as:
|
||||
* Cardinals: 1st, 2nd, 3rd ... 11th, 12th ... 21st, 22nd, 23nd ....
|
||||
* Ordinals: 1, 2, 3 ...
|
||||
|
||||
In any of these cases, the nouns will reflect the number with singular or plural
|
||||
form. For example:
|
||||
* in 0 days
|
||||
* in 1 day
|
||||
* in 2 days
|
||||
|
||||
The `pluralRuleFunction` receives 2 parameters:
|
||||
* `e`: a string representation of the number. Such as, "`1`", "`2`", "`2.1`".
|
||||
* `a`: `true` if this is "cardinal" type of description. `false` for ordinal and other case.
|
||||
|
||||
#### How you should write `pluralRuleFunction`
|
||||
|
||||
The first rule to write pluralRuleFunction is never translate the output string
|
||||
into your language. [Plural Rules][cldr-plural-rules] specified you should use
|
||||
these as the return values:
|
||||
|
||||
* "`zero`"
|
||||
* "`one`" (singular)
|
||||
* "`two`" (dual)
|
||||
* "`few`" (paucal)
|
||||
* "`many`" (also used for fractions if they have a separate class)
|
||||
* "`other`" (required—general plural form—also used if the language only has a single form)
|
||||
|
||||
Again, we'll use English as the example here.
|
||||
|
||||
Let's read the `return` statement in the pluralRuleFunction above:
|
||||
```javascript
|
||||
return a ? 1 == t && 11 != r ? "one" : 2 == t && 12 != r ? "two" : 3 == t && 13 != r ? "few" : "other" : 1 == e && l ? "one" : "other"
|
||||
```
|
||||
|
||||
This nested ternary is hard to read. It basically means:
|
||||
```javascript
|
||||
// e: the number variable to examine
|
||||
// a: "true" if cardinals
|
||||
// l: "true" if the variable e has nothin after decimal mark (e.g. "1.0" would be false)
|
||||
// o: "true" if the variable e is an integer
|
||||
// t: the "ones" of the number. e.g. "3" for number "9123"
|
||||
// r: the "ones" and "tens" of the number. e.g. "23" for number "9123"
|
||||
if (a == true) {
|
||||
if (t == 1 && r != 11) {
|
||||
return "one"; // i.e. 1st, 21st, 101st, 121st ...
|
||||
} else if (t == 2 && r != 12) {
|
||||
return "two"; // i.e. 2nd, 22nd, 102nd, 122nd ...
|
||||
} else if (t == 3 && r != 13) {
|
||||
return "few"; // i.e. 3rd, 23rd, 103rd, 123rd ...
|
||||
} else {
|
||||
return "other"; // i.e. 4th, 11th, 12th, 24th ...
|
||||
}
|
||||
} else {
|
||||
if (e == 1 && l) {
|
||||
return "one"; // i.e. 1 day
|
||||
} else {
|
||||
return "other"; // i.e. 0 days, 2 days, 3 days
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If your language, like French, do not have complicated cardinal rules, you may
|
||||
use the French's version of it:
|
||||
```javascript
|
||||
function (e, a) {
|
||||
return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other";
|
||||
}
|
||||
```
|
||||
|
||||
If your language, like Chinese, do not have any pluralization rule at all you
|
||||
may use the Chinese's version of it:
|
||||
```javascript
|
||||
function (e, a) {
|
||||
return "other";
|
||||
}
|
||||
```
|
|
@ -1,110 +0,0 @@
|
|||
/*eslint eqeqeq: "off"*/
|
||||
/*eslint no-nested-ternary: "off"*/
|
||||
/*eslint quotes: "off"*/
|
||||
|
||||
const rules = [{
|
||||
locale: "co",
|
||||
pluralRuleFunction: function (e, a) {
|
||||
return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other";
|
||||
},
|
||||
fields: {
|
||||
year: {
|
||||
displayName: "annu",
|
||||
relative: {
|
||||
0: "quist'annu",
|
||||
1: "l'annu chì vene",
|
||||
"-1": "l'annu passatu",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} annu",
|
||||
other: "in {0} anni",
|
||||
},
|
||||
past: {
|
||||
one: "{0} annu fà",
|
||||
other: "{0} anni fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
month: {
|
||||
displayName: "mese",
|
||||
relative: {
|
||||
0: "Questu mese",
|
||||
1: "u mese chì vene",
|
||||
"-1": "u mese passatu",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} mese",
|
||||
other: "in {0} mesi",
|
||||
},
|
||||
past: {
|
||||
one: "{0} mese fà",
|
||||
other: "{0} mesi fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
day: {
|
||||
displayName: "ghjornu",
|
||||
relative: {
|
||||
0: "oghje",
|
||||
1: "dumane",
|
||||
"-1": "eri",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} ghjornu",
|
||||
other: "in {0} ghjornu",
|
||||
},
|
||||
past: {
|
||||
one: "{0} ghjornu fà",
|
||||
other: "{0} ghjorni fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
hour: {
|
||||
displayName: "ora",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} ora",
|
||||
other: "in {0} ore",
|
||||
},
|
||||
past: {
|
||||
one: "{0} ora fà",
|
||||
other: "{0} ore fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
minute: {
|
||||
displayName: "minuta",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} minuta",
|
||||
other: "in {0} minute",
|
||||
},
|
||||
past: {
|
||||
one: "{0} minuta fà",
|
||||
other: "{0} minute fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
second: {
|
||||
displayName: "siconda",
|
||||
relative: {
|
||||
0: "avà",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "in {0} siconda",
|
||||
other: "in {0} siconde",
|
||||
},
|
||||
past: {
|
||||
one: "{0} siconda fà",
|
||||
other: "{0} siconde fà",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
export default rules;
|
|
@ -1,110 +0,0 @@
|
|||
/*eslint eqeqeq: "off"*/
|
||||
/*eslint no-nested-ternary: "off"*/
|
||||
/*eslint quotes: "off"*/
|
||||
|
||||
const rules = [{
|
||||
locale: "oc",
|
||||
pluralRuleFunction: function (e, a) {
|
||||
return a ? 1 == e ? "one" : "other" : e >= 0 && e < 2 ? "one" : "other";
|
||||
},
|
||||
fields: {
|
||||
year: {
|
||||
displayName: "an",
|
||||
relative: {
|
||||
0: "ongan",
|
||||
1: "l'an que ven",
|
||||
"-1": "l'an passat",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} an",
|
||||
other: "d’aquí {0} ans",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} an",
|
||||
other: "fa {0} ans",
|
||||
},
|
||||
},
|
||||
},
|
||||
month: {
|
||||
displayName: "mes",
|
||||
relative: {
|
||||
0: "aqueste mes",
|
||||
1: "lo mes que ven",
|
||||
"-1": "lo mes passat",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} mes",
|
||||
other: "d’aquí {0} meses",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} mes",
|
||||
other: "fa {0} meses",
|
||||
},
|
||||
},
|
||||
},
|
||||
day: {
|
||||
displayName: "jorn",
|
||||
relative: {
|
||||
0: "uèi",
|
||||
1: "deman",
|
||||
"-1": "ièr",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} jorn",
|
||||
other: "d’aquí {0} jorns",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} jorn",
|
||||
other: "fa {0} jorns",
|
||||
},
|
||||
},
|
||||
},
|
||||
hour: {
|
||||
displayName: "ora",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} ora",
|
||||
other: "d’aquí {0} oras",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} ora",
|
||||
other: "fa {0} oras",
|
||||
},
|
||||
},
|
||||
},
|
||||
minute: {
|
||||
displayName: "minuta",
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} minuta",
|
||||
other: "d’aquí {0} minutas",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} minuta",
|
||||
other: "fa {0} minutas",
|
||||
},
|
||||
},
|
||||
},
|
||||
second: {
|
||||
displayName: "segonda",
|
||||
relative: {
|
||||
0: "ara",
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
one: "d’aquí {0} segonda",
|
||||
other: "d’aquí {0} segondas",
|
||||
},
|
||||
past: {
|
||||
one: "fa {0} segonda",
|
||||
other: "fa {0} segondas",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}];
|
||||
|
||||
export default rules;
|
|
@ -1,98 +0,0 @@
|
|||
/*eslint eqeqeq: "off"*/
|
||||
/*eslint no-nested-ternary: "off"*/
|
||||
/*eslint quotes: "off"*/
|
||||
/*eslint comma-dangle: "off"*/
|
||||
|
||||
const rules = [
|
||||
{
|
||||
locale: "sa",
|
||||
fields: {
|
||||
year: {
|
||||
displayName: "year",
|
||||
relative: {
|
||||
0: "this year",
|
||||
1: "next year",
|
||||
"-1": "last year"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} y"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} y"
|
||||
}
|
||||
}
|
||||
},
|
||||
month: {
|
||||
displayName: "month",
|
||||
relative: {
|
||||
0: "this month",
|
||||
1: "next month",
|
||||
"-1": "last month"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} m"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} m"
|
||||
}
|
||||
}
|
||||
},
|
||||
day: {
|
||||
displayName: "day",
|
||||
relative: {
|
||||
0: "अद्य",
|
||||
1: "श्वः",
|
||||
"-1": "गतदिनम्"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} d"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} d"
|
||||
}
|
||||
}
|
||||
},
|
||||
hour: {
|
||||
displayName: "hour",
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} h"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} h"
|
||||
}
|
||||
}
|
||||
},
|
||||
minute: {
|
||||
displayName: "minute",
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} min"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} min"
|
||||
}
|
||||
}
|
||||
},
|
||||
second: {
|
||||
displayName: "second",
|
||||
relative: {
|
||||
0: "now"
|
||||
},
|
||||
relativeTime: {
|
||||
future: {
|
||||
other: "+{0} s"
|
||||
},
|
||||
past: {
|
||||
other: "-{0} s"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export default rules;
|
|
@ -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 @@
|
|||
[
|
||||
]
|
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