Merge pull request #2246 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes
local
Claire 12 months ago committed by GitHub
commit af97a6ac5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      .eslintrc.js
  2. 114
      .github/renovate.json5
  3. 3
      .github/workflows/check-i18n.yml
  4. 3
      .github/workflows/lint-css.yml
  5. 1
      .github/workflows/lint-haml.yml
  6. 5
      .github/workflows/lint-js.yml
  7. 3
      .github/workflows/lint-json.yml
  8. 6
      .github/workflows/lint-md.yml
  9. 1
      .github/workflows/lint-ruby.yml
  10. 3
      .github/workflows/lint-yml.yml
  11. 2
      .github/workflows/rebase-needed.yml
  12. 3
      .github/workflows/test-js.yml
  13. 1
      .github/workflows/test-migrations-one-step.yml
  14. 1
      .github/workflows/test-migrations-two-step.yml
  15. 1
      .github/workflows/test-ruby.yml
  16. 5
      .haml-lint.yml
  17. 2
      .prettierignore
  18. 4
      .rubocop.yml
  19. 149
      .rubocop_todo.yml
  20. 7
      Gemfile
  21. 60
      Gemfile.lock
  22. 10
      app/controllers/admin/dashboard_controller.rb
  23. 42
      app/controllers/admin/domain_blocks_controller.rb
  24. 2
      app/controllers/admin/webhooks_controller.rb
  25. 2
      app/controllers/api/v1/accounts_controller.rb
  26. 17
      app/controllers/api/v1/conversations_controller.rb
  27. 2
      app/controllers/api/v1/lists_controller.rb
  28. 8
      app/controllers/backups_controller.rb
  29. 4
      app/controllers/settings/imports_controller.rb
  30. 2
      app/controllers/settings/preferences/base_controller.rb
  31. 6
      app/helpers/application_helper.rb
  32. 2
      app/helpers/react_component_helper.rb
  33. 4
      app/helpers/settings_helper.rb
  34. 38
      app/javascript/flavours/glitch/actions/importer/normalizer.js
  35. 4
      app/javascript/flavours/glitch/actions/lists.js
  36. 2
      app/javascript/flavours/glitch/actions/notifications.js
  37. 3
      app/javascript/flavours/glitch/actions/statuses.js
  38. 11
      app/javascript/flavours/glitch/actions/streaming.js
  39. 91
      app/javascript/flavours/glitch/components/admin/ImpactReport.jsx
  40. 11
      app/javascript/flavours/glitch/components/domain.tsx
  41. 4
      app/javascript/flavours/glitch/components/dropdown_menu.jsx
  42. 10
      app/javascript/flavours/glitch/components/load_gap.tsx
  43. 28
      app/javascript/flavours/glitch/components/load_more.jsx
  44. 24
      app/javascript/flavours/glitch/components/load_more.tsx
  45. 15
      app/javascript/flavours/glitch/components/media_attachments.jsx
  46. 12
      app/javascript/flavours/glitch/components/media_gallery.jsx
  47. 20
      app/javascript/flavours/glitch/components/poll.jsx
  48. 14
      app/javascript/flavours/glitch/components/relative_timestamp.tsx
  49. 2
      app/javascript/flavours/glitch/components/scrollable_list.jsx
  50. 36
      app/javascript/flavours/glitch/components/status.jsx
  51. 12
      app/javascript/flavours/glitch/components/status_action_bar.jsx
  52. 16
      app/javascript/flavours/glitch/components/status_content.jsx
  53. 12
      app/javascript/flavours/glitch/containers/admin_component.jsx
  54. 19
      app/javascript/flavours/glitch/containers/compose_container.jsx
  55. 15
      app/javascript/flavours/glitch/containers/mastodon.jsx
  56. 13
      app/javascript/flavours/glitch/containers/media_container.jsx
  57. 4
      app/javascript/flavours/glitch/containers/status_container.js
  58. 8
      app/javascript/flavours/glitch/features/account/components/header.jsx
  59. 2
      app/javascript/flavours/glitch/features/account_gallery/index.jsx
  60. 2
      app/javascript/flavours/glitch/features/account_timeline/components/header.jsx
  61. 2
      app/javascript/flavours/glitch/features/account_timeline/components/moved_note.jsx
  62. 2
      app/javascript/flavours/glitch/features/account_timeline/containers/header_container.jsx
  63. 2
      app/javascript/flavours/glitch/features/compose/components/search_results.jsx
  64. 2
      app/javascript/flavours/glitch/features/directory/index.jsx
  65. 2
      app/javascript/flavours/glitch/features/explore/index.jsx
  66. 4
      app/javascript/flavours/glitch/features/explore/results.jsx
  67. 18
      app/javascript/flavours/glitch/features/list_timeline/index.jsx
  68. 2
      app/javascript/flavours/glitch/features/local_settings/page/index.jsx
  69. 10
      app/javascript/flavours/glitch/features/status/components/action_bar.jsx
  70. 21
      app/javascript/flavours/glitch/features/status/components/detailed_status.jsx
  71. 4
      app/javascript/flavours/glitch/features/status/index.jsx
  72. 12
      app/javascript/flavours/glitch/features/ui/components/audio_modal.jsx
  73. 7
      app/javascript/flavours/glitch/features/ui/components/media_modal.jsx
  74. 12
      app/javascript/flavours/glitch/features/ui/components/video_modal.jsx
  75. 1114
      app/javascript/flavours/glitch/locales/defaultMessages.json
  76. 2
      app/javascript/flavours/glitch/locales/en.json
  77. 22
      app/javascript/flavours/glitch/locales/global_locale.ts
  78. 5
      app/javascript/flavours/glitch/locales/index.ts
  79. 57
      app/javascript/flavours/glitch/locales/intl_provider.tsx
  80. 37
      app/javascript/flavours/glitch/locales/load_locale.ts
  81. 2
      app/javascript/flavours/glitch/locales/whitelist_af.json
  82. 2
      app/javascript/flavours/glitch/locales/whitelist_an.json
  83. 2
      app/javascript/flavours/glitch/locales/whitelist_ar.json
  84. 2
      app/javascript/flavours/glitch/locales/whitelist_ast.json
  85. 2
      app/javascript/flavours/glitch/locales/whitelist_be.json
  86. 2
      app/javascript/flavours/glitch/locales/whitelist_bg.json
  87. 2
      app/javascript/flavours/glitch/locales/whitelist_bn.json
  88. 2
      app/javascript/flavours/glitch/locales/whitelist_br.json
  89. 2
      app/javascript/flavours/glitch/locales/whitelist_bs.json
  90. 2
      app/javascript/flavours/glitch/locales/whitelist_ca.json
  91. 2
      app/javascript/flavours/glitch/locales/whitelist_ckb.json
  92. 2
      app/javascript/flavours/glitch/locales/whitelist_co.json
  93. 2
      app/javascript/flavours/glitch/locales/whitelist_cs.json
  94. 2
      app/javascript/flavours/glitch/locales/whitelist_cy.json
  95. 2
      app/javascript/flavours/glitch/locales/whitelist_da.json
  96. 2
      app/javascript/flavours/glitch/locales/whitelist_de.json
  97. 2
      app/javascript/flavours/glitch/locales/whitelist_el.json
  98. 2
      app/javascript/flavours/glitch/locales/whitelist_en-GB.json
  99. 2
      app/javascript/flavours/glitch/locales/whitelist_en.json
  100. 2
      app/javascript/flavours/glitch/locales/whitelist_eo.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -81,6 +81,15 @@ module.exports = {
{ property: 'substring', message: 'Use .slice instead of .substring.' }, { property: 'substring', message: 'Use .slice instead of .substring.' },
{ property: 'substr', message: 'Use .slice instead of .substr.' }, { 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-self-assign': 'off',
'no-unused-expressions': 'error', 'no-unused-expressions': 'error',
'no-unused-vars': 'off', 'no-unused-vars': 'off',
@ -293,6 +302,7 @@ module.exports = {
'.*rc.js', '.*rc.js',
'ide-helper.js', 'ide-helper.js',
'config/webpack/**/*', 'config/webpack/**/*',
'config/formatjs-formatter.js',
], ],
env: { env: {

@ -0,0 +1,114 @@
{
$schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: [
'config:base',
':dependencyDashboard',
':labels(dependencies)',
':maintainLockFilesMonthly', // update non-direct dependencies monthly
':prConcurrentLimit10', // only 10 open PRs at the same time
],
stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it
// packageRules order is important, they are applied from top to bottom and are merged,
// so for example grouping rules needs to be at the bottom
packageRules: [
{
// Ignore major version bumps for these node packages
matchManagers: ['npm'],
matchPackageNames: [
'@rails/ujs', // Needs to match the major Rails version
'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes
// Requires Webpacker upgrade or replacement
'@types/webpack',
'babel-loader',
'compression-webpack-plugin',
'css-loader',
'imports-loader',
'mini-css-extract-plugin',
'postcss-loader',
'sass-loader',
'terser-webpack-plugin',
'webpack',
'webpack-assets-manifest',
'webpack-bundle-analyzer',
'webpack-dev-server',
'webpack-cli',
// react-router: Requires manual upgrade
'history',
'react-router-dom',
],
matchUpdateTypes: ['major'],
enabled: false,
},
{
// Ignore major version bumps for these Ruby packages
matchManagers: ['bundler'],
matchPackageNames: [
'sprockets', // Requires manual upgrade https://github.com/rails/sprockets/blob/master/UPGRADING.md#guide-to-upgrading-from-sprockets-3x-to-4x
'strong_migrations', // Requires manual upgrade
'sidekiq', // Requires manual upgrade
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
'redis', // Requires manual upgrade and sync with Sidekiq version
'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
// Needs major Rails version bump
'rack',
'rails',
'rails-i18n',
],
matchUpdateTypes: ['major'],
enabled: false,
},
{
// Update Github Actions and Docker images weekly
matchManagers: ['github-actions', 'dockerfile', 'docker-compose'],
extends: ['schedule:weekly'],
},
{
// Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version
matchManagers: ['dockerfile'],
matchPackageNames: ['moritzheiber/ruby-jemalloc'],
matchUpdateTypes: ['minor', 'major'],
enabled: false,
},
{
// Ignore major bump for the node image, this needs to be synced with .nvmrc
matchManagers: ['dockerfile'],
matchPackageNames: ['node'],
matchUpdateTypes: ['major'],
enabled: false,
},
{
// Ignore major postgres bumps in the docker-compose file, as those break dev environments
matchManagers: ['docker-compose'],
matchPackageNames: ['postgres'],
matchUpdateTypes: ['major'],
enabled: false,
},
{
// Update devDependencies every week, with one grouped PR
matchDepTypes: 'devDependencies',
matchUpdateTypes: ['patch', 'minor'],
excludePackageNames: [
'typescript', // Typescript has many changes in minor versions, needs to be checked every time
],
groupName: 'devDependencies (non-major)',
extends: ['schedule:weekly'],
},
{
// Update @types/* packages every week, with one grouped PR
matchPackagePrefixes: '@types/',
matchUpdateTypes: ['patch', 'minor'],
groupName: 'DefinitelyTyped types (non-major)',
extends: ['schedule:weekly'],
addLabels: ['typescript'],
},
// Add labels depending on package manager
{ matchManagers: ['npm', 'nvm'], addLabels: ['javascript'] },
{ matchManagers: ['bundler', 'ruby-version'], addLabels: ['ruby'] },
{ matchManagers: ['docker-compose', 'dockerfile'], addLabels: ['docker'] },
{ matchManagers: ['github-actions'], addLabels: ['github_actions'] },
],
}

@ -41,8 +41,7 @@ jobs:
- name: Check for missing strings in English JSON - name: Check for missing strings in English JSON
run: | run: |
yarn build:development yarn i18n:extract --throws
yarn manage:translations en
git diff --exit-code git diff --exit-code
- name: Check locale file normalization - name: Check locale file normalization

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'
@ -48,4 +49,4 @@ jobs:
- run: echo "::add-matcher::.github/stylelint-matcher.json" - run: echo "::add-matcher::.github/stylelint-matcher.json"
- name: Stylelint - name: Stylelint
run: yarn test:lint:sass run: yarn lint:sass

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- '.github/workflows/haml-lint-problem-matcher.json' - '.github/workflows/haml-lint-problem-matcher.json'
- '.github/workflows/lint-haml.yml' - '.github/workflows/lint-haml.yml'

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'
@ -48,7 +49,7 @@ jobs:
run: yarn --frozen-lockfile run: yarn --frozen-lockfile
- name: ESLint - name: ESLint
run: yarn test:lint:js --max-warnings 0 run: yarn lint:js --max-warnings 0
- name: Typecheck - name: Typecheck
run: yarn test:typecheck run: yarn typecheck

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'
@ -40,4 +41,4 @@ jobs:
run: yarn --frozen-lockfile run: yarn --frozen-lockfile
- name: Prettier - name: Prettier
run: yarn prettier --check "**/*.json" run: yarn lint:json

@ -3,8 +3,10 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- '.github/workflows/lint-md.yml' - '.github/workflows/lint-md.yml'
- '.nvmrc'
- '.prettier*' - '.prettier*'
- '**/*.md' - '**/*.md'
- '!AUTHORS.md' - '!AUTHORS.md'
@ -14,6 +16,7 @@ on:
pull_request: pull_request:
paths: paths:
- '.github/workflows/lint-md.yml' - '.github/workflows/lint-md.yml'
- '.nvmrc'
- '.prettier*' - '.prettier*'
- '**/*.md' - '**/*.md'
- '!AUTHORS.md' - '!AUTHORS.md'
@ -32,9 +35,10 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
cache: yarn cache: yarn
node-version-file: '.nvmrc'
- name: Install all yarn packages - name: Install all yarn packages
run: yarn --frozen-lockfile run: yarn --frozen-lockfile
- name: Prettier - name: Prettier
run: yarn prettier --check "**/*.md" run: yarn lint:md

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'Gemfile*' - 'Gemfile*'
- '.rubocop*.yml' - '.rubocop*.yml'

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'
@ -42,4 +43,4 @@ jobs:
run: yarn --frozen-lockfile run: yarn --frozen-lockfile
- name: Prettier - name: Prettier
run: yarn prettier --check "**/*.{yml,yaml}" run: yarn lint:yml

@ -4,10 +4,12 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
- 'l10n_main' - 'l10n_main'
pull_request_target: pull_request_target:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
- 'l10n_main' - 'l10n_main'
types: [synchronize] types: [synchronize]

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
paths: paths:
- 'package.json' - 'package.json'
- 'yarn.lock' - 'yarn.lock'
@ -44,4 +45,4 @@ jobs:
run: yarn --frozen-lockfile run: yarn --frozen-lockfile
- name: Jest testing - name: Jest testing
run: yarn test:jest --reporters github-actions summary run: yarn jest --reporters github-actions summary

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
pull_request: pull_request:
jobs: jobs:

@ -3,6 +3,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
pull_request: pull_request:
jobs: jobs:

@ -4,6 +4,7 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' - 'dependabot/**'
- 'renovate/**'
pull_request: pull_request:
env: env:

@ -4,6 +4,11 @@ exclude:
- 'vendor/**/*' - 'vendor/**/*'
- lib/templates/haml/scaffold/_form.html.haml - lib/templates/haml/scaffold/_form.html.haml
require:
- ./lib/linter/haml_middle_dot.rb
linters: linters:
AltText: AltText:
enabled: true enabled: true
MiddleDot:
enabled: true

@ -61,7 +61,7 @@ docker-compose.override.yml
/app/javascript/mastodon/features/emoji/emoji_map.json /app/javascript/mastodon/features/emoji/emoji_map.json
# Ignore locale files # Ignore locale files
/app/javascript/mastodon/locales /app/javascript/mastodon/locales/*.json
/config/locales /config/locales
# Ignore vendored CSS reset # Ignore vendored CSS reset

@ -11,6 +11,7 @@ require:
- rubocop-rspec - rubocop-rspec
- rubocop-performance - rubocop-performance
- rubocop-capybara - rubocop-capybara
- ./lib/linter/rubocop_middle_dot
AllCops: AllCops:
TargetRubyVersion: 3.0 # Set to minimum supported version of CI TargetRubyVersion: 3.0 # Set to minimum supported version of CI
@ -205,3 +206,6 @@ Style/TrailingCommaInArrayLiteral:
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral # https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: 'comma' EnforcedStyleForMultiline: 'comma'
Style/MiddleDot:
Enabled: true

@ -239,79 +239,6 @@ RSpec/AnyInstance:
- 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/activitypub/delivery_worker_spec.rb'
- 'spec/workers/web/push_notification_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SkipBlocks, EnforcedStyle.
# SupportedStyles: described_class, explicit
RSpec/DescribedClass:
Exclude:
- 'spec/controllers/concerns/cache_concern_spec.rb'
- 'spec/controllers/concerns/challengable_concern_spec.rb'
- 'spec/lib/entity_cache_spec.rb'
- 'spec/lib/extractor_spec.rb'
- 'spec/lib/feed_manager_spec.rb'
- 'spec/lib/hash_object_spec.rb'
- 'spec/lib/ostatus/tag_manager_spec.rb'
- 'spec/lib/request_spec.rb'
- 'spec/lib/tag_manager_spec.rb'
- 'spec/lib/webfinger_resource_spec.rb'
- 'spec/mailers/notification_mailer_spec.rb'
- 'spec/mailers/user_mailer_spec.rb'
- 'spec/models/account_conversation_spec.rb'
- 'spec/models/account_domain_block_spec.rb'
- 'spec/models/account_migration_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/block_spec.rb'
- 'spec/models/domain_block_spec.rb'
- 'spec/models/email_domain_block_spec.rb'
- 'spec/models/export_spec.rb'
- 'spec/models/favourite_spec.rb'
- 'spec/models/follow_spec.rb'
- 'spec/models/identity_spec.rb'
- 'spec/models/import_spec.rb'
- 'spec/models/media_attachment_spec.rb'
- 'spec/models/notification_spec.rb'
- 'spec/models/relationship_filter_spec.rb'
- 'spec/models/report_filter_spec.rb'
- 'spec/models/session_activation_spec.rb'
- 'spec/models/setting_spec.rb'
- 'spec/models/site_upload_spec.rb'
- 'spec/models/status_pin_spec.rb'
- 'spec/models/status_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/policies/account_moderation_note_policy_spec.rb'
- 'spec/presenters/account_relationships_presenter_spec.rb'
- 'spec/presenters/status_relationships_presenter_spec.rb'
- 'spec/serializers/activitypub/note_serializer_spec.rb'
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
- 'spec/serializers/rest/account_serializer_spec.rb'
- 'spec/services/activitypub/fetch_remote_account_service_spec.rb'
- 'spec/services/activitypub/fetch_remote_actor_service_spec.rb'
- 'spec/services/activitypub/fetch_remote_key_service_spec.rb'
- 'spec/services/after_block_domain_from_account_service_spec.rb'
- 'spec/services/authorize_follow_service_spec.rb'
- 'spec/services/batched_remove_status_service_spec.rb'
- 'spec/services/block_domain_service_spec.rb'
- 'spec/services/block_service_spec.rb'
- 'spec/services/bootstrap_timeline_service_spec.rb'
- 'spec/services/clear_domain_media_service_spec.rb'
- 'spec/services/favourite_service_spec.rb'
- 'spec/services/follow_service_spec.rb'
- 'spec/services/import_service_spec.rb'
- 'spec/services/post_status_service_spec.rb'
- 'spec/services/precompute_feed_service_spec.rb'
- 'spec/services/process_mentions_service_spec.rb'
- 'spec/services/purge_domain_service_spec.rb'
- 'spec/services/reblog_service_spec.rb'
- 'spec/services/reject_follow_service_spec.rb'
- 'spec/services/remove_from_followers_service_spec.rb'
- 'spec/services/remove_status_service_spec.rb'
- 'spec/services/unallow_domain_service_spec.rb'
- 'spec/services/unblock_service_spec.rb'
- 'spec/services/unfollow_service_spec.rb'
- 'spec/services/unmute_service_spec.rb'
- 'spec/services/update_account_service_spec.rb'
- 'spec/validators/note_length_validator_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/EmptyExampleGroup: RSpec/EmptyExampleGroup:
Exclude: Exclude:
@ -468,30 +395,6 @@ RSpec/MessageSpies:
- 'spec/spec_helper.rb' - 'spec/spec_helper.rb'
- 'spec/validators/status_length_validator_spec.rb' - 'spec/validators/status_length_validator_spec.rb'
RSpec/MissingExampleGroupArgument:
Exclude:
- 'spec/controllers/accounts_controller_spec.rb'
- 'spec/controllers/activitypub/collections_controller_spec.rb'
- 'spec/controllers/admin/statuses_controller_spec.rb'
- 'spec/controllers/admin/users/roles_controller_spec.rb'
- 'spec/controllers/api/v1/accounts_controller_spec.rb'
- 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
- 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
- 'spec/controllers/api/v1/statuses_controller_spec.rb'
- 'spec/controllers/auth/registrations_controller_spec.rb'
- 'spec/features/log_in_spec.rb'
- 'spec/lib/activitypub/activity/undo_spec.rb'
- 'spec/lib/status_reach_finder_spec.rb'
- 'spec/models/account_spec.rb'
- 'spec/models/email_domain_block_spec.rb'
- 'spec/models/trends/statuses_spec.rb'
- 'spec/models/trends/tags_spec.rb'
- 'spec/models/user_role_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/services/fetch_link_card_service_spec.rb'
- 'spec/services/notify_service_spec.rb'
- 'spec/services/process_mentions_service_spec.rb'
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 19 Max: 19
@ -1336,11 +1239,6 @@ Style/GlobalStdStream:
- 'config/environments/development.rb' - 'config/environments/development.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
# Configuration parameters: AllowedVariables.
Style/GlobalVars:
Exclude:
- 'config/initializers/statsd.rb'
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:
@ -1474,7 +1372,6 @@ Style/RedundantConstantBase:
Exclude: Exclude:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb' - 'config/initializers/sidekiq.rb'
- 'config/initializers/statsd.rb'
- 'config/locales/sr-Latn.rb' - 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb' - 'config/locales/sr.rb'
@ -1488,52 +1385,6 @@ Style/RedundantFetchBlock:
- 'config/initializers/paperclip.rb' - 'config/initializers/paperclip.rb'
- 'config/puma.rb' - 'config/puma.rb'
# This cop supports safe autocorrection (--autocorrect).
Style/RedundantRegexpCharacterClass:
Exclude:
- 'app/lib/link_details_extractor.rb'
- 'app/lib/tag_manager.rb'
- 'app/models/domain_allow.rb'
- 'app/models/domain_block.rb'
- 'app/services/fetch_oembed_service.rb'
- 'config/initializers/rack_attack.rb'
- 'lib/tasks/emojis.rake'
- 'lib/tasks/mastodon.rake'
# This cop supports safe autocorrection (--autocorrect).
Style/RedundantRegexpEscape:
Exclude:
- 'app/lib/webfinger_resource.rb'
- 'app/models/account.rb'
- 'app/models/tag.rb'
- 'app/services/fetch_link_card_service.rb'
- 'config/initializers/twitter_regex.rb'
- 'lib/paperclip/color_extractor.rb'
- 'lib/tasks/mastodon.rake'
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Exclude:
- 'app/lib/link_details_extractor.rb'
- 'app/lib/plain_text_formatter.rb'
- 'app/lib/tag_manager.rb'
- 'app/lib/text_formatter.rb'
- 'app/models/account.rb'
- 'app/models/domain_allow.rb'
- 'app/models/domain_block.rb'
- 'app/models/site_upload.rb'
- 'app/models/tag.rb'
- 'app/services/backup_service.rb'
- 'app/services/fetch_oembed_service.rb'
- 'app/services/search_service.rb'
- 'config/initializers/rack_attack.rb'
- 'config/initializers/twitter_regex.rb'
- 'config/routes.rb'
- 'lib/mastodon/premailer_webpack_strategy.rb'
- 'lib/tasks/mastodon.rake'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
# AllowedMethods: present?, blank?, presence, try, try! # AllowedMethods: present?, blank?, presence, try, try!

@ -5,7 +5,7 @@ ruby '>= 3.0.0'
gem 'pkg-config', '~> 1.5' gem 'pkg-config', '~> 1.5'
gem 'puma', '~> 6.2' gem 'puma', '~> 6.3'
gem 'rails', '~> 6.1.7' gem 'rails', '~> 6.1.7'
gem 'sprockets', '~> 3.7.2' gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2' gem 'thor', '~> 1.2'
@ -17,10 +17,10 @@ gem 'makara', '~> 0.5'
gem 'pghero' gem 'pghero'
gem 'dotenv-rails', '~> 2.8' gem 'dotenv-rails', '~> 2.8'
gem 'aws-sdk-s3', '~> 1.122', require: false gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'fog-core', '<= 2.4.0' gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b' gem 'kt-paperclip', '~> 7.2'
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'

@ -7,18 +7,6 @@ GIT
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) 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 GIT
remote: https://github.com/mastodon/rails-settings-cached.git remote: https://github.com/mastodon/rails-settings-cached.git
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
@ -109,17 +97,17 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.761.0) aws-partitions (1.772.0)
aws-sdk-core (3.172.0) aws-sdk-core (3.174.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.64.0) aws-sdk-kms (1.65.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.122.0) aws-sdk-s3 (1.123.0)
aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-core (~> 3, >= 3.174.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4) aws-sigv4 (~> 1.4)
aws-sigv4 (1.5.2) aws-sigv4 (1.5.2)
@ -380,6 +368,12 @@ GEM
activerecord activerecord
kaminari-core (= 1.2.2) kaminari-core (= 1.2.2)
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) launchy (2.5.2)
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.8.1)
@ -442,11 +436,6 @@ GEM
nokogiri (1.15.2) nokogiri (1.15.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) 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) oj (3.14.3)
omniauth (1.9.2) omniauth (1.9.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
@ -501,7 +490,7 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
private_address_check (0.5.0) private_address_check (0.5.0)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (6.2.2) puma (6.3.0)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -544,8 +533,9 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (6.0.0) rails-i18n (6.0.0)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7) railties (>= 6.0.0, < 7)
@ -588,7 +578,7 @@ GEM
rspec-mocks (3.12.5) rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.2) rspec-rails (6.0.3)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@ -648,7 +638,7 @@ GEM
redis (>= 4.5.0, < 5) redis (>= 4.5.0, < 5)
sidekiq-bulk (0.2.0) sidekiq-bulk (0.2.0)
sidekiq sidekiq
sidekiq-scheduler (5.0.2) sidekiq-scheduler (5.0.3)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 6, < 8)
tilt (>= 1.4.0) tilt (>= 1.4.0)
@ -681,7 +671,6 @@ GEM
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.25) stackprof (0.2.25)
statsd-ruby (1.5.0)
stoplight (3.0.1) stoplight (3.0.1)
redlock (~> 1.0) redlock (~> 1.0)
strong_migrations (0.8.0) strong_migrations (0.8.0)
@ -770,7 +759,7 @@ DEPENDENCIES
active_model_serializers (~> 0.10) active_model_serializers (~> 0.10)
addressable (~> 2.8) addressable (~> 2.8)
annotate (~> 3.2) annotate (~> 3.2)
aws-sdk-s3 (~> 1.122) aws-sdk-s3 (~> 1.123)
better_errors (~> 2.9) better_errors (~> 2.9)
binding_of_caller (~> 1.0) binding_of_caller (~> 1.0)
blurhash (~> 0.1) blurhash (~> 0.1)
@ -818,7 +807,7 @@ DEPENDENCIES
json-ld-preloaded (~> 3.2) json-ld-preloaded (~> 3.2)
json-schema (~> 4.0) json-schema (~> 4.0)
kaminari (~> 1.2) kaminari (~> 1.2)
kt-paperclip (~> 7.1)! kt-paperclip (~> 7.2)
letter_opener (~> 1.8) letter_opener (~> 1.8)
letter_opener_web (~> 2.0) letter_opener_web (~> 2.0)
link_header (~> 0.0) link_header (~> 0.0)
@ -830,7 +819,6 @@ DEPENDENCIES
net-http (~> 0.3.2) net-http (~> 0.3.2)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa (~> 0.2)
oj (~> 3.14) oj (~> 3.14)
omniauth (~> 1.9) omniauth (~> 1.9)
omniauth-cas (~> 2.0) omniauth-cas (~> 2.0)
@ -846,7 +834,7 @@ DEPENDENCIES
premailer-rails premailer-rails
private_address_check (~> 0.5) private_address_check (~> 0.5)
public_suffix (~> 5.0) public_suffix (~> 5.0)
puma (~> 6.2) puma (~> 6.3)
pundit (~> 2.3) pundit (~> 2.3)
rack (~> 2.2.7) rack (~> 2.2.7)
rack-attack (~> 6.6) rack-attack (~> 6.6)
@ -894,3 +882,9 @@ DEPENDENCIES
webpacker (~> 5.4) webpacker (~> 5.4)
webpush! webpush!
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION
ruby 3.2.2p53
BUNDLED WITH
2.4.13

@ -14,15 +14,5 @@ module Admin
@pending_tags_count = Tag.pending_review.count @pending_tags_count = Tag.pending_review.count
@pending_appeals_count = Appeal.pending.count @pending_appeals_count = Appeal.pending.count
end end
private
def redis_info
@redis_info ||= if redis.is_a?(Redis::Namespace)
redis.redis.info
else
redis.info
end
end
end end
end end

@ -31,31 +31,41 @@ module Admin
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
# Disallow accidentally downgrading a domain block
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
@domain_block.save @domain_block.save
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
@domain_block.errors.delete(:domain) @domain_block.errors.delete(:domain)
render :new return render :new
end
# Allow transparently upgrading a domain block
if existing_domain_block.present?
@domain_block = existing_domain_block
@domain_block.assign_attributes(resource_params)
end
# Require explicit confirmation when suspending
return render :confirm_suspension if requires_confirmation?
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else else
if existing_domain_block.present? render :new
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :new
end
end end
end end
def update def update
authorize :domain_block, :update? authorize :domain_block, :update?
if @domain_block.update(update_params) @domain_block.assign_attributes(update_params)
# Require explicit confirmation when suspending
return render :confirm_suspension if requires_confirmation?
if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?) DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
log_action :update, @domain_block log_action :update, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
@ -92,5 +102,9 @@ module Admin
def action_from_button def action_from_button
'save' if params[:save] 'save' if params[:save]
end end
def requires_confirmation?
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm]
end
end end
end end

@ -71,7 +71,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:webhook).permit(:url, events: []) params.require(:webhook).permit(:url, :template, events: [])
end end
end end
end end

@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def account_params def account_params
params.permit(:username, :email, :password, :agreement, :locale, :reason) params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
end end
def check_enabled_registrations def check_enabled_registrations

@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
def index def index
@conversations = paginated_conversations @conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
end end
def read def read
@ -32,7 +32,20 @@ class Api::V1::ConversationsController < Api::BaseController
def paginated_conversations def paginated_conversations
AccountConversation.where(account: current_account) AccountConversation.where(account: current_account)
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) .includes(
account: :account_stat,
last_status: [
:media_attachments,
:preview_cards,
:status_stat,
:tags,
{
active_mentions: [account: :account_stat],
account: :account_stat,
},
]
)
.to_a_paginated_by_id(limit_param(LIMIT), **params_slice(:max_id, :since_id, :min_id))
end end
def insert_pagination_headers def insert_pagination_headers

@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController
end end
def list_params def list_params
params.permit(:title, :replies_policy) params.permit(:title, :replies_policy, :exclusive)
end end
end end

@ -11,15 +11,15 @@ class BackupsController < ApplicationController
def download def download
case Paperclip::Attachment.default_options[:storage] case Paperclip::Attachment.default_options[:storage]
when :s3 when :s3
redirect_to @backup.dump.expiring_url(10) redirect_to @backup.dump.expiring_url(10), allow_other_host: true
when :fog when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? 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 else
redirect_to full_asset_url(@backup.dump.url) redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end end
when :filesystem when :filesystem
redirect_to full_asset_url(@backup.dump.url) redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
end end
end end

@ -12,6 +12,7 @@ class Settings::ImportsController < Settings::BaseController
muting: 'muted_accounts_failures.csv', muting: 'muted_accounts_failures.csv',
domain_blocking: 'blocked_domains_failures.csv', domain_blocking: 'blocked_domains_failures.csv',
bookmarks: 'bookmarks_failures.csv', bookmarks: 'bookmarks_failures.csv',
lists: 'lists_failures.csv',
}.freeze }.freeze
TYPE_TO_HEADERS_MAP = { TYPE_TO_HEADERS_MAP = {
@ -20,6 +21,7 @@ class Settings::ImportsController < Settings::BaseController
muting: ['Account address', 'Hide notifications'], muting: ['Account address', 'Hide notifications'],
domain_blocking: false, domain_blocking: false,
bookmarks: false, bookmarks: false,
lists: false,
}.freeze }.freeze
def index def index
@ -49,6 +51,8 @@ class Settings::ImportsController < Settings::BaseController
csv << [row.data['domain']] csv << [row.data['domain']]
when :bookmarks when :bookmarks
csv << [row.data['uri']] csv << [row.data['uri']]
when :lists
csv << [row.data['list_name'], row.data['acct']]
end end
end end
end end

@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController
end end
def user_params def user_params
params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys) params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys)
end end
end end

@ -170,11 +170,11 @@ module ApplicationHelper
end end
def storage_host def storage_host
URI::HTTPS.build(host: storage_host_name).to_s "https://#{storage_host_var}"
end end
def storage_host? def storage_host?
storage_host_name.present? storage_host_var.present?
end end
def quote_wrap(text, line_width: 80, break_sequence: "\n") def quote_wrap(text, line_width: 80, break_sequence: "\n")
@ -235,7 +235,7 @@ module ApplicationHelper
private private
def storage_host_name def storage_host_var
ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
end end
end end

@ -11,7 +11,7 @@ module ReactComponentHelper
end end
def react_admin_component(name, props = {}) 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) div_tag_with_data(data)
end end

@ -5,10 +5,6 @@ module SettingsHelper
LanguagesHelper::SUPPORTED_LOCALES.keys LanguagesHelper::SUPPORTED_LOCALES.keys
end end
def hash_to_object(hash)
HashObject.new(hash)
end
def session_device_icon(session) def session_device_icon(session)
device = session.detection.device device = session.detection.device

@ -6,7 +6,7 @@ import { unescapeHTML } from 'flavours/glitch/utils/html';
const domParser = new DOMParser(); const domParser = new DOMParser();
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji; obj[`:${emoji.shortcode}:`] = emoji;
return obj; return obj;
}, {}); }, {});
@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) {
export function normalizeAccount(account) { export function normalizeAccount(account) {
account = { ...account }; account = { ...account };
const emojiMap = makeEmojiMap(account); const emojiMap = makeEmojiMap(account.emojis);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name; const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap); account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
@ -78,7 +78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
} else { } else {
const spoilerText = normalStatus.spoiler_text || ''; const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus); const emojiMap = makeEmojiMap(normalStatus.emojis);
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
@ -89,22 +89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) {
return normalStatus; return normalStatus;
} }
export function normalizeStatusTranslation(translation, status) {
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
spoiler_text: translation.spoiler_text,
};
return normalTranslation;
}
export function normalizePoll(poll) { export function normalizePoll(poll) {
const normalPoll = { ...poll }; const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(normalPoll); const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => ({ normalPoll.options = poll.options.map((option, index) => ({
...option, ...option,
voted: poll.own_votes && poll.own_votes.includes(index), voted: poll.own_votes && poll.own_votes.includes(index),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap), titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
})); }));
return normalPoll; return normalPoll;
} }
export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};
return normalTranslation;
}
export function normalizeAnnouncement(announcement) { export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement }; const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement); const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);

@ -151,10 +151,10 @@ export const createListFail = error => ({
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)); 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)); dispatch(updateListSuccess(data));
if (shouldReset) { if (shouldReset) {

@ -1,4 +1,4 @@
import IntlMessageFormat from 'intl-messageformat'; import { IntlMessageFormat } from 'intl-messageformat';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';

@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({
error, error,
}); });
export const undoStatusTranslation = id => ({ export const undoStatusTranslation = (id, pollId) => ({
type: STATUS_TRANSLATE_UNDO, type: STATUS_TRANSLATE_UNDO,
id, id,
pollId,
}); });

@ -1,6 +1,6 @@
// @ts-check // @ts-check
import { getLocale } from 'mastodon/locales'; import { getLocale } from 'flavours/glitch/locales';
import { connectStream } from '../stream'; import { connectStream } from '../stream';
@ -25,8 +25,6 @@ import {
fillListTimelineGaps, fillListTimelineGaps,
} from './timelines'; } from './timelines';
const { messages } = getLocale();
/** /**
* @param {number} max * @param {number} max
* @returns {number} * @returns {number}
@ -44,8 +42,10 @@ const randomUpTo = max =>
* @param {function(object): boolean} [options.accept] * @param {function(object): boolean} [options.accept]
* @returns {function(): void} * @returns {function(): void}
*/ */
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
connectStream(channelName, params, (dispatch, getState) => { const { messages } = getLocale();
return connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error // @ts-expect-error
@ -122,6 +122,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
}, },
}; };
}); });
};
/** /**
* @param {Function} dispatch * @param {Function} dispatch

@ -0,0 +1,91 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedNumber, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import api from 'flavours/glitch/api';
import { Skeleton } from 'flavours/glitch/components/skeleton';
export default class ImpactReport extends PureComponent {
static propTypes = {
domain: PropTypes.string.isRequired,
};
state = {
loading: true,
data: null,
};
componentDidMount () {
const { domain } = this.props;
const params = {
domain: domain,
include_subdomains: true,
};
api().post('/api/v1/admin/measures', {
keys: ['instance_accounts', 'instance_follows', 'instance_followers'],
start_at: null,
end_at: null,
instance_accounts: params,
instance_follows: params,
instance_followers: params,
}).then(res => {
this.setState({
loading: false,
data: res.data,
});
}).catch(err => {
console.error(err);
});
}
render () {
const { loading, data } = this.state;
return (
<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,8 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import * as React from 'react'; import * as React from 'react';
import type { InjectedIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { defineMessages, injectIntl } from 'react-intl';
import { IconButton } from './icon_button'; import { IconButton } from './icon_button';
@ -16,9 +15,11 @@ const messages = defineMessages({
interface Props { interface Props {
domain: string; domain: string;
onUnblockDomain: (domain: string) => void; onUnblockDomain: (domain: string) => void;
intl: InjectedIntl;
} }
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
const intl = useIntl();
const handleDomainUnblock = useCallback(() => { const handleDomainUnblock = useCallback(() => {
onUnblockDomain(domain); onUnblockDomain(domain);
}, [domain, onUnblockDomain]); }, [domain, onUnblockDomain]);
@ -42,5 +43,3 @@ const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
</div> </div>
); );
}; };
export const Domain = injectIntl(_Domain);

@ -121,10 +121,10 @@ class DropdownMenu extends PureComponent {
return <li key={`sep-${i}`} className='dropdown-menu__separator' />; return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
} }
const { text, href = '#', target = '_blank', method } = option; const { text, href = '#', target = '_blank', method, dangerous } = option;
return ( return (
<li className='dropdown-menu__item' key={`${text}-${i}`}> <li className={classNames('dropdown-menu__item', { 'dropdown-menu__item--dangerous': dangerous })} key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text} {text}
</a> </a>

@ -1,7 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import type { InjectedIntl } from 'react-intl'; import { useIntl, defineMessages } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
@ -13,10 +12,11 @@ interface Props {
disabled: boolean; disabled: boolean;
maxId: string; maxId: string;
onClick: (maxId: string) => void; onClick: (maxId: string) => void;
intl: InjectedIntl;
} }
const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => { export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
const intl = useIntl();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
onClick(maxId); onClick(maxId);
}, [maxId, onClick]); }, [maxId, onClick]);
@ -32,5 +32,3 @@ const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
</button> </button>
); );
}; };
export const LoadGap = injectIntl(_LoadGap);

@ -1,28 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
export default class LoadMore extends PureComponent {
static propTypes = {
onClick: PropTypes.func,
disabled: PropTypes.bool,
visible: PropTypes.bool,
};
static defaultProps = {
visible: true,
};
render() {
const { disabled, visible } = this.props;
return (
<button className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);
}
}

@ -0,0 +1,24 @@
import { FormattedMessage } from 'react-intl';
interface Props {
onClick: (event: React.MouseEvent) => void;
disabled?: boolean;
visible?: boolean;
}
export const LoadMore: React.FC<Props> = ({
onClick,
disabled,
visible = true,
}) => {
return (
<button
type='button'
className='load-more'
disabled={disabled || !visible}
style={{ visibility: visible ? 'visible' : 'hidden' }}
onClick={onClick}
>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);
};

@ -52,8 +52,9 @@ export default class MediaAttachments extends ImmutablePureComponent {
}; };
render () { render () {
const { status, lang, width, height, revealed } = this.props; const { status, width, height, revealed } = this.props;
const mediaAttachments = status.get('media_attachments'); const mediaAttachments = status.get('media_attachments');
const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;
if (mediaAttachments.size === 0) { if (mediaAttachments.size === 0) {
return null; return null;
@ -61,14 +62,15 @@ export default class MediaAttachments extends ImmutablePureComponent {
if (mediaAttachments.getIn([0, 'type']) === 'audio') { if (mediaAttachments.getIn([0, 'type']) === 'audio') {
const audio = mediaAttachments.get(0); const audio = mediaAttachments.get(0);
const description = audio.getIn(['translation', 'description']) || audio.get('description');
return ( return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => ( {Component => (
<Component <Component
src={audio.get('url')} src={audio.get('url')}
alt={audio.get('description')} alt={description}
lang={lang || status.get('language')} lang={language}
width={width} width={width}
height={height} height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
@ -82,6 +84,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
); );
} else if (mediaAttachments.getIn([0, 'type']) === 'video') { } else if (mediaAttachments.getIn([0, 'type']) === 'video') {
const video = mediaAttachments.get(0); const video = mediaAttachments.get(0);
const description = video.getIn(['translation', 'description']) || video.get('description');
return ( return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
@ -91,8 +94,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
frameRate={video.getIn(['meta', 'original', 'frame_rate'])} frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
blurhash={video.get('blurhash')} blurhash={video.get('blurhash')}
src={video.get('url')} src={video.get('url')}
alt={video.get('description')} alt={description}
lang={lang || status.get('language')} lang={language}
width={width} width={width}
height={height} height={height}
inline inline
@ -109,7 +112,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={mediaAttachments} media={mediaAttachments}
lang={lang || status.get('language')} lang={language}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
defaultWidth={width} defaultWidth={width}
revealed={revealed} revealed={revealed}

@ -124,10 +124,12 @@ class Item extends PureComponent {
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>); badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
} }
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}> <div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
<Blurhash <Blurhash
hash={attachment.get('blurhash')} hash={attachment.get('blurhash')}
className='media-gallery__preview' className='media-gallery__preview'
@ -166,8 +168,8 @@ class Item extends PureComponent {
src={previewUrl} src={previewUrl}
srcSet={srcSet} srcSet={srcSet}
sizes={sizes} sizes={sizes}
alt={attachment.get('description')} alt={description}
title={attachment.get('description')} title={description}
lang={lang} lang={lang}
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }} style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
onLoad={this.handleImageLoad} onLoad={this.handleImageLoad}
@ -183,8 +185,8 @@ class Item extends PureComponent {
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video <video
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`} className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
aria-label={attachment.get('description')} aria-label={description}
title={attachment.get('description')} title={description}
lang={lang} lang={lang}
role='application' role='application'
src={attachment.get('url')} src={attachment.get('url')}

@ -58,9 +58,9 @@ class Poll extends ImmutablePureComponent {
}; };
static getDerivedStateFromProps (props, state) { static getDerivedStateFromProps (props, state) {
const { poll, intl } = props; const { poll } = props;
const expires_at = poll.get('expires_at'); 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 }; return (expired === state.expired) ? null : { expired };
} }
@ -77,10 +77,10 @@ class Poll extends ImmutablePureComponent {
} }
_setupTimer () { _setupTimer () {
const { poll, intl } = this.props; const { poll } = this.props;
clearTimeout(this._timer); clearTimeout(this._timer);
if (!this.state.expired) { 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._timer = setTimeout(() => {
this.setState({ expired: true }); this.setState({ expired: true });
}, delay); }, delay);
@ -139,10 +139,12 @@ class Poll extends ImmutablePureComponent {
const active = !!this.state.selected[`${optionIndex}`]; const active = !!this.state.selected[`${optionIndex}`];
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex)); const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
let titleEmojified = option.get('title_emojified'); const title = option.getIn(['translation', 'title']) || option.get('title');
if (!titleEmojified) { let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
if (!titleHtml) {
const emojiMap = makeEmojiMap(poll); const emojiMap = makeEmojiMap(poll);
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
} }
return ( return (
@ -164,7 +166,7 @@ class Poll extends ImmutablePureComponent {
role={poll.get('multiple') ? 'checkbox' : 'radio'} role={poll.get('multiple') ? 'checkbox' : 'radio'}
onKeyPress={this.handleOptionKeyPress} onKeyPress={this.handleOptionKeyPress}
aria-checked={active} aria-checked={active}
aria-label={option.get('title')} aria-label={title}
lang={lang} lang={lang}
data-index={optionIndex} data-index={optionIndex}
/> />
@ -183,7 +185,7 @@ class Poll extends ImmutablePureComponent {
<span <span
className='poll__option__text translate' className='poll__option__text translate'
lang={lang} lang={lang}
dangerouslySetInnerHTML={{ __html: titleEmojified }} dangerouslySetInnerHTML={{ __html: titleHtml }}
/> />
{!!voted && <span className='poll__voted'> {!!voted && <span className='poll__voted'>

@ -1,6 +1,6 @@
import { Component } from 'react'; import { Component } from 'react';
import type { InjectedIntl } from 'react-intl'; import type { IntlShape } from 'react-intl';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({ const messages = defineMessages({
@ -103,7 +103,7 @@ const getUnitDelay = (units: string) => {
}; };
export const timeAgoString = ( export const timeAgoString = (
intl: InjectedIntl, intl: IntlShape,
date: Date, date: Date,
now: number, now: number,
year: number, year: number,
@ -155,7 +155,7 @@ export const timeAgoString = (
}; };
const timeRemainingString = ( const timeRemainingString = (
intl: InjectedIntl, intl: IntlShape,
date: Date, date: Date,
now: number, now: number,
timeGiven = true timeGiven = true
@ -190,7 +190,7 @@ const timeRemainingString = (
}; };
interface Props { interface Props {
intl: InjectedIntl; intl: IntlShape;
timestamp: string; timestamp: string;
year: number; year: number;
futureDate?: boolean; futureDate?: boolean;
@ -201,7 +201,7 @@ interface States {
} }
class RelativeTimestamp extends Component<Props, States> { class RelativeTimestamp extends Component<Props, States> {
state = { state = {
now: this.props.intl.now(), now: Date.now(),
}; };
static defaultProps = { static defaultProps = {
@ -223,7 +223,7 @@ class RelativeTimestamp extends Component<Props, States> {
UNSAFE_componentWillReceiveProps(nextProps: Props) { UNSAFE_componentWillReceiveProps(nextProps: Props) {
if (this.props.timestamp !== nextProps.timestamp) { if (this.props.timestamp !== nextProps.timestamp) {
this.setState({ now: this.props.intl.now() }); this.setState({ now: Date.now() });
} }
} }
@ -253,7 +253,7 @@ class RelativeTimestamp extends Component<Props, States> {
: Math.max(updateInterval, unitRemainder); : Math.max(updateInterval, unitRemainder);
this._timer = window.setTimeout(() => { this._timer = window.setTimeout(() => {
this.setState({ now: this.props.intl.now() }); this.setState({ now: Date.now() });
}, delay); }, delay);
} }

@ -15,7 +15,7 @@ import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/inters
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
import LoadMore from './load_more'; import { LoadMore } from './load_more';
import LoadPending from './load_pending'; import LoadPending from './load_pending';
import LoadingIndicator from './loading_indicator'; import LoadingIndicator from './loading_indicator';

@ -26,12 +26,18 @@ import StatusHeader from './status_header';
import StatusIcons from './status_icons'; import StatusIcons from './status_icons';
import StatusPrepend from './status_prepend'; import StatusPrepend from './status_prepend';
const domParser = new DOMParser();
export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => { export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
const displayName = status.getIn(['account', 'display_name']); const displayName = status.getIn(['account', 'display_name']);
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
const values = [ const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName, displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && !expanded ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length), spoilerText && !expanded ? spoilerText : contentText,
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']), status.getIn(['account', 'acct']),
]; ];
@ -391,12 +397,14 @@ class Status extends ImmutablePureComponent {
handleOpenVideo = (options) => { handleOpenVideo = (options) => {
const { status } = this.props; const { status } = this.props;
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options); const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
}; };
handleOpenMedia = (media, index) => { handleOpenMedia = (media, index) => {
const { status } = this.props; const { status } = this.props;
this.props.onOpenMedia(status.get('id'), media, index, status.get('language')); const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenMedia(status.get('id'), media, index, lang);
}; };
handleHotkeyOpenMedia = e => { handleHotkeyOpenMedia = e => {
@ -406,10 +414,11 @@ class Status extends ImmutablePureComponent {
e.preventDefault(); e.preventDefault();
if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').size > 0) {
const lang = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); onOpenVideo(statusId, status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
} else { } else {
onOpenMedia(statusId, status.get('media_attachments'), 0); onOpenMedia(statusId, status.get('media_attachments'), 0, lang);
} }
} }
}; };
@ -625,6 +634,8 @@ class Status extends ImmutablePureComponent {
media.push(<PictureInPicturePlaceholder />); media.push(<PictureInPicturePlaceholder />);
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
} else if (attachments.size > 0) { } else if (attachments.size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');
if (muted || attachments.some(item => item.get('type') === 'unknown')) { if (muted || attachments.some(item => item.get('type') === 'unknown')) {
media.push( media.push(
<AttachmentList <AttachmentList
@ -634,14 +645,15 @@ class Status extends ImmutablePureComponent {
); );
} else if (attachments.getIn([0, 'type']) === 'audio') { } else if (attachments.getIn([0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push( media.push(
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => ( {Component => (
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={description}
lang={status.get('language')} lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
@ -662,6 +674,7 @@ class Status extends ImmutablePureComponent {
mediaIcons.push('music'); mediaIcons.push('music');
} else if (attachments.getIn([0, 'type']) === 'video') { } else if (attachments.getIn([0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push( media.push(
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} > <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
@ -670,8 +683,8 @@ class Status extends ImmutablePureComponent {
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={description}
lang={status.get('language')} lang={language}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
@ -691,7 +704,7 @@ class Status extends ImmutablePureComponent {
{Component => ( {Component => (
<Component <Component
media={attachments} media={attachments}
lang={status.get('language')} lang={language}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])} fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
@ -724,7 +737,8 @@ class Status extends ImmutablePureComponent {
} }
if (status.get('poll')) { if (status.get('poll')) {
contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />); const language = status.getIn(['translation', 'language']) || status.get('language');
contentMedia.push(<PollContainer pollId={status.get('poll')} lang={language} />);
contentMediaIcons.push('tasks'); contentMediaIcons.push('tasks');
} }

@ -243,21 +243,21 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe) { if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
if (!this.props.onFilter) { if (!this.props.onFilter) {
menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick }); menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
menu.push(null); menu.push(null);
} }
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null); menu.push(null);

@ -327,11 +327,11 @@ class StatusContent extends PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const contentLocale = intl.locale.replace(/[_-].*/, ''); const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale); const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') }; const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') }; const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const lang = status.get('translation') ? intl.locale : status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', { const classNames = classnames('status__content', {
'status__content--with-action': parseClick && !disabled, 'status__content--with-action': parseClick && !disabled,
'status__content--with-spoiler': status.get('spoiler_text').length > 0, 'status__content--with-spoiler': status.get('spoiler_text').length > 0,
@ -396,7 +396,7 @@ class StatusContent extends PureComponent {
<p <p
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} 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' onClick={this.handleSpoilerClick} aria-expanded={!hidden}> <button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
{toggleText} {toggleText}
@ -414,7 +414,7 @@ class StatusContent extends PureComponent {
className='status__content__text translate' className='status__content__text translate'
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={language}
/> />
{!hidden && translateButton} {!hidden && translateButton}
{media} {media}
@ -439,7 +439,7 @@ class StatusContent extends PureComponent {
tabIndex={0} tabIndex={0}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={language}
/> />
{translateButton} {translateButton}
{media} {media}
@ -460,7 +460,7 @@ class StatusContent extends PureComponent {
tabIndex={0} tabIndex={0}
onMouseEnter={this.handleMouseEnter} onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave} onMouseLeave={this.handleMouseLeave}
lang={lang} lang={language}
/> />
{translateButton} {translateButton}
{media} {media}

@ -1,25 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider } from 'flavours/glitch/locales';
import { getLocale } from 'mastodon/locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
export default class AdminComponent extends PureComponent { export default class AdminComponent extends PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
}; };
render () { render () {
const { locale, children } = this.props; const { children } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages}> <IntlProvider>
{children} {children}
</IntlProvider> </IntlProvider>
); );

@ -1,38 +1,25 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider, addLocaleData } from 'react-intl';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
import { hydrateStore } from 'flavours/glitch/actions/store'; import { hydrateStore } from 'flavours/glitch/actions/store';
import Compose from 'flavours/glitch/features/standalone/compose'; import Compose from 'flavours/glitch/features/standalone/compose';
import initialState from 'flavours/glitch/initial_state'; import initialState from 'flavours/glitch/initial_state';
import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store'; import { store } from 'flavours/glitch/store';
import { getLocale } from 'mastodon/locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
if (initialState) { if (initialState) {
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }
store.dispatch(fetchCustomEmojis()); store.dispatch(fetchCustomEmojis());
export default class TimelineContainer extends PureComponent { export default class ComposeContainer extends PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
render () { render () {
const { locale } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages}> <IntlProvider>
<Provider store={store}> <Provider store={store}>
<Compose /> <Compose />
</Provider> </Provider>

@ -1,8 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { IntlProvider, addLocaleData } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { BrowserRouter, Route } from 'react-router-dom'; import { BrowserRouter, Route } from 'react-router-dom';
@ -17,11 +15,8 @@ import { connectUserStream } from 'flavours/glitch/actions/streaming';
import ErrorBoundary from 'flavours/glitch/components/error_boundary'; import ErrorBoundary from 'flavours/glitch/components/error_boundary';
import UI from 'flavours/glitch/features/ui'; import UI from 'flavours/glitch/features/ui';
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state'; import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
import { IntlProvider } from 'flavours/glitch/locales';
import { store } from 'flavours/glitch/store'; import { store } from 'flavours/glitch/store';
import { getLocale } from 'locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`; const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
@ -45,10 +40,6 @@ const createIdentityContext = state => ({
export default class Mastodon extends PureComponent { export default class Mastodon extends PureComponent {
static propTypes = {
locale: PropTypes.string.isRequired,
};
static childContextTypes = { static childContextTypes = {
identity: PropTypes.shape({ identity: PropTypes.shape({
signedIn: PropTypes.bool.isRequired, signedIn: PropTypes.bool.isRequired,
@ -84,10 +75,8 @@ export default class Mastodon extends PureComponent {
} }
render () { render () {
const { locale } = this.props;
return ( return (
<IntlProvider locale={locale} messages={messages}> <IntlProvider>
<ReduxProvider store={store}> <ReduxProvider store={store}>
<ErrorBoundary> <ErrorBoundary>
<BrowserRouter> <BrowserRouter>

@ -2,8 +2,6 @@ import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { IntlProvider, addLocaleData } from 'react-intl';
import { fromJS } from 'immutable'; import { fromJS } from 'immutable';
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
@ -14,19 +12,14 @@ import Audio from 'flavours/glitch/features/audio';
import Card from 'flavours/glitch/features/status/components/card'; import Card from 'flavours/glitch/features/status/components/card';
import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
import { IntlProvider } from 'flavours/glitch/locales';
import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar'; import { getScrollbarWidth } from 'flavours/glitch/utils/scrollbar';
import { getLocale } from 'mastodon/locales';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio }; const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
export default class MediaContainer extends PureComponent { export default class MediaContainer extends PureComponent {
static propTypes = { static propTypes = {
locale: PropTypes.string.isRequired,
components: PropTypes.object.isRequired, components: PropTypes.object.isRequired,
}; };
@ -75,7 +68,7 @@ export default class MediaContainer extends PureComponent {
}; };
render () { render () {
const { locale, components } = this.props; const { components } = this.props;
let handleOpenVideo; let handleOpenVideo;
@ -85,7 +78,7 @@ export default class MediaContainer extends PureComponent {
} }
return ( return (
<IntlProvider locale={locale} messages={messages}> <IntlProvider>
<> <>
{[].map.call(components, (component, i) => { {[].map.call(components, (component, i) => {
const componentName = component.getAttribute('data-component'); const componentName = component.getAttribute('data-component');

@ -47,7 +47,7 @@ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' }, editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
@ -218,7 +218,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
onTranslate (status) { onTranslate (status) {
if (status.get('translation')) { if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'))); dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else { } else {
dispatch(translateStatus(status.get('id'))); dispatch(translateStatus(status.get('id')));
} }

@ -272,16 +272,16 @@ class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
} }
if (account.getIn(['relationship', 'blocking'])) { if (account.getIn(['relationship', 'blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
} else { } else {
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
} }
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
} }
if (signedIn && isRemote) { if (signedIn && isRemote) {
@ -290,7 +290,7 @@ class Header extends ImmutablePureComponent {
if (account.getIn(['relationship', 'domain_blocking'])) { if (account.getIn(['relationship', 'domain_blocking'])) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
} else { } else {
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
} }
} }

@ -9,7 +9,7 @@ import { connect } from 'react-redux';
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts'; import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines'; import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
import LoadMore from 'flavours/glitch/components/load_more'; import { LoadMore } from 'flavours/glitch/components/load_more';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header'; import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';

@ -153,7 +153,7 @@ export default class Header extends ImmutablePureComponent {
{!(hideTabs || hidden) && ( {!(hideTabs || hidden) && (
<div className='account__section-headline'> <div className='account__section-headline'>
<NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink> <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts with replies' /></NavLink> <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
<NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
</div> </div>
)} )}

@ -38,7 +38,7 @@ export default class MovedNote extends ImmutablePureComponent {
<div className='account__moved-note'> <div className='account__moved-note'>
<div className='account__moved-note__message'> <div className='account__moved-note__message'>
<div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div> <div className='account__moved-note__icon-wrapper'><Icon id='suitcase' className='account__moved-note__icon' fixedWidth /></div>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} /> <FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={displayNameHtml} /></bdi> }} />
</div> </div>
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'> <a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>

@ -148,7 +148,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal({ dispatch(openModal({
modalType: 'CONFIRM', modalType: 'CONFIRM',
modalProps: { modalProps: {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
confirm: intl.formatMessage(messages.blockDomainConfirm), confirm: intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => dispatch(blockDomain(domain)), onConfirm: () => dispatch(blockDomain(domain)),
}, },

@ -7,7 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import LoadMore from 'flavours/glitch/components/load_more'; import { LoadMore } from 'flavours/glitch/components/load_more';
import AccountContainer from 'flavours/glitch/containers/account_container'; import AccountContainer from 'flavours/glitch/containers/account_container';
import StatusContainer from 'flavours/glitch/containers/status_container'; import StatusContainer from 'flavours/glitch/containers/status_container';
import { searchEnabled } from 'flavours/glitch/initial_state'; import { searchEnabled } from 'flavours/glitch/initial_state';

@ -13,7 +13,7 @@ import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavour
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory'; import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; import ColumnHeader from 'flavours/glitch/components/column_header';
import LoadMore from 'flavours/glitch/components/load_more'; import { LoadMore } from 'flavours/glitch/components/load_more';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import { RadioButton } from 'flavours/glitch/components/radio_button'; import { RadioButton } from 'flavours/glitch/components/radio_button';
import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ScrollContainer from 'flavours/glitch/containers/scroll_container';

@ -69,7 +69,7 @@ class Explore extends PureComponent {
<Search /> <Search />
</div> </div>
<div className='scrollable scrollable--flex'> <div className='scrollable scrollable--flex' data-nosnippet>
{isSearching ? ( {isSearching ? (
<SearchResults /> <SearchResults />
) : ( ) : (

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import { expandSearch } from 'flavours/glitch/actions/search'; import { expandSearch } from 'flavours/glitch/actions/search';
import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag';
import LoadMore from 'flavours/glitch/components/load_more'; import { LoadMore } from 'flavours/glitch/components/load_more';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator'; import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
import Account from 'flavours/glitch/containers/account_container'; import Account from 'flavours/glitch/containers/account_container';
import Status from 'flavours/glitch/containers/status_container'; import Status from 'flavours/glitch/containers/status_container';
@ -113,7 +113,7 @@ class Results extends PureComponent {
<button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button> <button onClick={this.handleSelectAll} className={type === 'all' && 'active'}><FormattedMessage id='search_results.all' defaultMessage='All' /></button>
<button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='People' /></button> <button onClick={this.handleSelectAccounts} className={type === 'accounts' && 'active'}><FormattedMessage id='search_results.accounts' defaultMessage='People' /></button>
<button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button> <button onClick={this.handleSelectHashtags} className={type === 'hashtags' && 'active'}><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></button>
<button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></button> <button onClick={this.handleSelectStatuses} className={type === 'statuses' && 'active'}><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></button>
</div> </div>
<div className='explore__search-results'> <div className='explore__search-results'>

@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Toggle from 'react-toggle';
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists'; import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -145,7 +147,13 @@ class ListTimeline extends PureComponent {
handleRepliesPolicyChange = ({ target }) => { handleRepliesPolicyChange = ({ target }) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const { id } = this.props.params; 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 () { render () {
@ -154,6 +162,7 @@ class ListTimeline extends PureComponent {
const pinned = !!columnId; const pinned = !!columnId;
const title = list ? list.get('title') : id; const title = list ? list.get('title') : id;
const replies_policy = list ? list.get('replies_policy') : undefined; const replies_policy = list ? list.get('replies_policy') : undefined;
const isExclusive = list ? list.get('exclusive') : undefined;
if (typeof list === 'undefined') { if (typeof list === 'undefined') {
return ( return (
@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
</button> </button>
</div> </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 && ( { replies_policy !== undefined && (
<div role='group' aria-labelledby={`list-${id}-replies-policy`}> <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
<span id={`list-${id}-replies-policy`} className='column-settings__section'> <span id={`list-${id}-replies-policy`} className='column-settings__section'>

@ -275,7 +275,7 @@ class LocalSettingsPage extends PureComponent {
), ),
({ intl, onChange, settings }) => ( ({ intl, onChange, settings }) => (
<div className='glitch local-settings__page content_warnings'> <div className='glitch local-settings__page content_warnings'>
<h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content warnings' /></h1> <h1><FormattedMessage id='settings.content_warnings' defaultMessage='Content Warnings' /></h1>
<LocalSettingsPageItem <LocalSettingsPageItem
settings={settings} settings={settings}
item={['content_warnings', 'shared_state']} item={['content_warnings', 'shared_state']}

@ -176,15 +176,15 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else { } else {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null); menu.push(null);
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {

@ -158,6 +158,8 @@ class DetailedStatus extends ImmutablePureComponent {
outerStyle.height = `${this.state.height}px`; outerStyle.height = `${this.state.height}px`;
} }
const language = status.getIn(['translation', 'language']) || status.get('language');
if (pictureInPicture.get('inUse')) { if (pictureInPicture.get('inUse')) {
media.push(<PictureInPicturePlaceholder />); media.push(<PictureInPicturePlaceholder />);
mediaIcons.push('video-camera'); mediaIcons.push('video-camera');
@ -166,12 +168,13 @@ class DetailedStatus extends ImmutablePureComponent {
media.push(<AttachmentList media={status.get('media_attachments')} />); media.push(<AttachmentList media={status.get('media_attachments')} />);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push( media.push(
<Audio <Audio
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={description}
lang={status.get('language')} lang={language}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
@ -187,14 +190,16 @@ class DetailedStatus extends ImmutablePureComponent {
mediaIcons.push('music'); mediaIcons.push('music');
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
media.push( media.push(
<Video <Video
preview={attachment.get('preview_url')} preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={attachment.get('description')} alt={description}
lang={status.get('language')} lang={language}
inline inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
@ -213,7 +218,7 @@ class DetailedStatus extends ImmutablePureComponent {
standalone standalone
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
media={status.get('media_attachments')} media={status.get('media_attachments')}
lang={status.get('language')} lang={language}
letterbox={settings.getIn(['media', 'letterbox'])} letterbox={settings.getIn(['media', 'letterbox'])}
fullwidth={settings.getIn(['media', 'fullwidth'])} fullwidth={settings.getIn(['media', 'fullwidth'])}
hidden={!expanded} hidden={!expanded}
@ -251,7 +256,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else if (this.context.router) { } else if (this.context.router) {
reblogLink = ( reblogLink = (
<> <>
· {' · '}
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'> <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
<Icon id={reblogIcon} /> <Icon id={reblogIcon} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
@ -263,7 +268,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else { } else {
reblogLink = ( reblogLink = (
<> <>
· {' · '}
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id={reblogIcon} /> <Icon id={reblogIcon} />
<span className='detailed-status__reblogs'> <span className='detailed-status__reblogs'>
@ -297,7 +302,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (status.get('edited_at')) { if (status.get('edited_at')) {
edited = ( edited = (
<> <>
· {' · '}
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /> <EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
</> </>
); );

@ -66,7 +66,7 @@ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' }, statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
@ -481,7 +481,7 @@ class Status extends ImmutablePureComponent {
const { dispatch } = this.props; const { dispatch } = this.props;
if (status.get('translation')) { if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id'))); dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else { } else {
dispatch(translateStatus(status.get('id'))); dispatch(translateStatus(status.get('id')));
} }

@ -8,7 +8,7 @@ import Audio from 'flavours/glitch/features/audio';
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer'; import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']), status: state.getIn(['statuses', statusId]),
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
}); });
@ -17,7 +17,7 @@ class AudioModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string.isRequired, statusId: PropTypes.string.isRequired,
language: PropTypes.string, status: ImmutablePropTypes.map.isRequired,
accountStaticAvatar: PropTypes.string.isRequired, accountStaticAvatar: PropTypes.string.isRequired,
options: PropTypes.shape({ options: PropTypes.shape({
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -31,15 +31,17 @@ class AudioModal extends ImmutablePureComponent {
}; };
render () { render () {
const { media, language, accountStaticAvatar, statusId, onClose } = this.props; const { media, status, accountStaticAvatar, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
const language = status.getIn(['translation', 'language']) || status.get('language');
const description = media.getIn(['translation', 'description']) || media.get('description');
return ( return (
<div className='modal-root__modal audio-modal'> <div className='modal-root__modal audio-modal'>
<div className='audio-modal__container'> <div className='audio-modal__container'>
<Audio <Audio
src={media.get('url')} src={media.get('url')}
alt={media.get('description')} alt={description}
lang={language} lang={language}
duration={media.getIn(['meta', 'original', 'duration'], 0)} duration={media.getIn(['meta', 'original', 'duration'], 0)}
height={150} height={150}
@ -52,7 +54,7 @@ class AudioModal extends ImmutablePureComponent {
</div> </div>
<div className='media-modal__overlay'> <div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
</div> </div>
</div> </div>
); );

@ -147,6 +147,7 @@ class MediaModal extends ImmutablePureComponent {
const content = media.map((image) => { const content = media.map((image) => {
const width = image.getIn(['meta', 'original', 'width']) || null; const width = image.getIn(['meta', 'original', 'width']) || null;
const height = image.getIn(['meta', 'original', 'height']) || null; const height = image.getIn(['meta', 'original', 'height']) || null;
const description = image.getIn(['translation', 'description']) || image.get('description');
if (image.get('type') === 'image') { if (image.get('type') === 'image') {
return ( return (
@ -155,7 +156,7 @@ class MediaModal extends ImmutablePureComponent {
src={image.get('url')} src={image.get('url')}
width={width} width={width}
height={height} height={height}
alt={image.get('description')} alt={description}
lang={lang} lang={lang}
key={image.get('url')} key={image.get('url')}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
@ -178,7 +179,7 @@ class MediaModal extends ImmutablePureComponent {
volume={volume || 1} volume={volume || 1}
onCloseVideo={onClose} onCloseVideo={onClose}
detailed detailed
alt={image.get('description')} alt={description}
lang={lang} lang={lang}
key={image.get('url')} key={image.get('url')}
/> />
@ -190,7 +191,7 @@ class MediaModal extends ImmutablePureComponent {
width={width} width={width}
height={height} height={height}
key={image.get('url')} key={image.get('url')}
alt={image.get('description')} alt={description}
lang={lang} lang={lang}
onClick={this.toggleNavigation} onClick={this.toggleNavigation}
/> />

@ -9,7 +9,7 @@ import Footer from 'flavours/glitch/features/picture_in_picture/components/foote
import Video from 'flavours/glitch/features/video'; import Video from 'flavours/glitch/features/video';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
language: state.getIn(['statuses', statusId, 'language']), status: state.getIn(['statuses', statusId]),
}); });
class VideoModal extends ImmutablePureComponent { class VideoModal extends ImmutablePureComponent {
@ -17,7 +17,7 @@ class VideoModal extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
statusId: PropTypes.string, statusId: PropTypes.string,
language: PropTypes.string, status: ImmutablePropTypes.map,
options: PropTypes.shape({ options: PropTypes.shape({
startTime: PropTypes.number, startTime: PropTypes.number,
autoPlay: PropTypes.bool, autoPlay: PropTypes.bool,
@ -38,8 +38,10 @@ class VideoModal extends ImmutablePureComponent {
} }
render () { render () {
const { media, statusId, language, onClose } = this.props; const { media, status, onClose } = this.props;
const options = this.props.options || {}; const options = this.props.options || {};
const language = status.getIn(['translation', 'language']) || status.get('language');
const description = media.getIn(['translation', 'description']) || media.get('description');
return ( return (
<div className='modal-root__modal video-modal'> <div className='modal-root__modal video-modal'>
@ -55,13 +57,13 @@ class VideoModal extends ImmutablePureComponent {
onCloseVideo={onClose} onCloseVideo={onClose}
autoFocus autoFocus
detailed detailed
alt={media.get('description')} alt={description}
lang={language} lang={language}
/> />
</div> </div>
<div className='media-modal__overlay'> <div className='media-modal__overlay'>
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} {status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
</div> </div>
</div> </div>
); );

File diff suppressed because it is too large Load Diff

@ -4,7 +4,9 @@
"account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.disclaimer_full": "Information below may reflect the user's profile incompletely.",
"account.follows": "Follows", "account.follows": "Follows",
"account.joined": "Joined {date}", "account.joined": "Joined {date}",
"account.mute_notifications": "Mute notifications from @{name}",
"account.suspended_disclaimer_full": "This user has been suspended by a moderator.", "account.suspended_disclaimer_full": "This user has been suspended by a moderator.",
"account.unmute_notifications": "Unmute notifications from @{name}",
"account.view_full_profile": "View full profile", "account.view_full_profile": "View full profile",
"account_note.cancel": "Cancel", "account_note.cancel": "Cancel",
"account_note.edit": "Edit", "account_note.edit": "Edit",

@ -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;
}

@ -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';

@ -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>
);
};

@ -0,0 +1,37 @@
import { Semaphore } from 'async-mutex';
import type { LocaleData } from './global_locale';
import { isLocaleLoaded, setLocale } from './global_locale';
const localeLoadingSemaphore = new Semaphore(1);
export async function loadLocale() {
const locale = document.querySelector<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 upstreamLocaleData = await import(
/* webpackMode: "lazy" */
/* webpackChunkName: "locales/vanilla/[request]" */
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */
`mastodon/locales/${locale}.json`
) as LocaleData['messages'];
const localeData = await import(
/* webpackMode: "lazy" */
/* webpackChunkName: "locales/glitch/[request]" */
/* webpackInclude: /\.json$/ */
/* webpackPreload: true */
`flavours/glitch/locales/${locale}.json`
) as LocaleData['messages'];
setLocale({ messages: { ...upstreamLocaleData, ...localeData }, locale });
});
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save