Merge pull request #2246 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
af97a6ac5a
10
.eslintrc.js
10
.eslintrc.js
|
@ -81,6 +81,15 @@ module.exports = {
|
|||
{ property: 'substring', message: 'Use .slice instead of .substring.' },
|
||||
{ property: 'substr', message: 'Use .slice instead of .substr.' },
|
||||
],
|
||||
'no-restricted-syntax': [
|
||||
'error',
|
||||
{
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
selector: 'Literal[value=/•/], JSXText[value=/•/]',
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
message: "Use '·' (middle dot) instead of '•' (bullet)",
|
||||
},
|
||||
],
|
||||
'no-self-assign': 'off',
|
||||
'no-unused-expressions': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
|
@ -293,6 +302,7 @@ module.exports = {
|
|||
'.*rc.js',
|
||||
'ide-helper.js',
|
||||
'config/webpack/**/*',
|
||||
'config/formatjs-formatter.js',
|
||||
],
|
||||
|
||||
env: {
|
||||
|
|
114
.github/renovate.json5
vendored
Normal file
114
.github/renovate.json5
vendored
Normal file
|
@ -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'] },
|
||||
],
|
||||
}
|
3
.github/workflows/check-i18n.yml
vendored
3
.github/workflows/check-i18n.yml
vendored
|
@ -41,8 +41,7 @@ jobs:
|
|||
|
||||
- name: Check for missing strings in English JSON
|
||||
run: |
|
||||
yarn build:development
|
||||
yarn manage:translations en
|
||||
yarn i18n:extract --throws
|
||||
git diff --exit-code
|
||||
|
||||
- name: Check locale file normalization
|
||||
|
|
3
.github/workflows/lint-css.yml
vendored
3
.github/workflows/lint-css.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
|
@ -48,4 +49,4 @@ jobs:
|
|||
- run: echo "::add-matcher::.github/stylelint-matcher.json"
|
||||
|
||||
- name: Stylelint
|
||||
run: yarn test:lint:sass
|
||||
run: yarn lint:sass
|
||||
|
|
1
.github/workflows/lint-haml.yml
vendored
1
.github/workflows/lint-haml.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- '.github/workflows/haml-lint-problem-matcher.json'
|
||||
- '.github/workflows/lint-haml.yml'
|
||||
|
|
5
.github/workflows/lint-js.yml
vendored
5
.github/workflows/lint-js.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
|
@ -48,7 +49,7 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: ESLint
|
||||
run: yarn test:lint:js --max-warnings 0
|
||||
run: yarn lint:js --max-warnings 0
|
||||
|
||||
- name: Typecheck
|
||||
run: yarn test:typecheck
|
||||
run: yarn typecheck
|
||||
|
|
3
.github/workflows/lint-json.yml
vendored
3
.github/workflows/lint-json.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
|
@ -40,4 +41,4 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Prettier
|
||||
run: yarn prettier --check "**/*.json"
|
||||
run: yarn lint:json
|
||||
|
|
6
.github/workflows/lint-md.yml
vendored
6
.github/workflows/lint-md.yml
vendored
|
@ -3,8 +3,10 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- '.github/workflows/lint-md.yml'
|
||||
- '.nvmrc'
|
||||
- '.prettier*'
|
||||
- '**/*.md'
|
||||
- '!AUTHORS.md'
|
||||
|
@ -14,6 +16,7 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/lint-md.yml'
|
||||
- '.nvmrc'
|
||||
- '.prettier*'
|
||||
- '**/*.md'
|
||||
- '!AUTHORS.md'
|
||||
|
@ -32,9 +35,10 @@ jobs:
|
|||
uses: actions/setup-node@v3
|
||||
with:
|
||||
cache: yarn
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install all yarn packages
|
||||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Prettier
|
||||
run: yarn prettier --check "**/*.md"
|
||||
run: yarn lint:md
|
||||
|
|
1
.github/workflows/lint-ruby.yml
vendored
1
.github/workflows/lint-ruby.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'Gemfile*'
|
||||
- '.rubocop*.yml'
|
||||
|
|
3
.github/workflows/lint-yml.yml
vendored
3
.github/workflows/lint-yml.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
|
@ -42,4 +43,4 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Prettier
|
||||
run: yarn prettier --check "**/*.{yml,yaml}"
|
||||
run: yarn lint:yml
|
||||
|
|
2
.github/workflows/rebase-needed.yml
vendored
2
.github/workflows/rebase-needed.yml
vendored
|
@ -4,10 +4,12 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
- 'l10n_main'
|
||||
pull_request_target:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
- 'l10n_main'
|
||||
types: [synchronize]
|
||||
|
||||
|
|
3
.github/workflows/test-js.yml
vendored
3
.github/workflows/test-js.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'yarn.lock'
|
||||
|
@ -44,4 +45,4 @@ jobs:
|
|||
run: yarn --frozen-lockfile
|
||||
|
||||
- name: Jest testing
|
||||
run: yarn test:jest --reporters github-actions summary
|
||||
run: yarn jest --reporters github-actions summary
|
||||
|
|
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
|
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
|
1
.github/workflows/test-ruby.yml
vendored
1
.github/workflows/test-ruby.yml
vendored
|
@ -4,6 +4,7 @@ on:
|
|||
push:
|
||||
branches-ignore:
|
||||
- 'dependabot/**'
|
||||
- 'renovate/**'
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
|
|
|
@ -4,6 +4,11 @@ exclude:
|
|||
- 'vendor/**/*'
|
||||
- lib/templates/haml/scaffold/_form.html.haml
|
||||
|
||||
require:
|
||||
- ./lib/linter/haml_middle_dot.rb
|
||||
|
||||
linters:
|
||||
AltText:
|
||||
enabled: true
|
||||
MiddleDot:
|
||||
enabled: true
|
||||
|
|
|
@ -61,7 +61,7 @@ docker-compose.override.yml
|
|||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||
|
||||
# Ignore locale files
|
||||
/app/javascript/mastodon/locales
|
||||
/app/javascript/mastodon/locales/*.json
|
||||
/config/locales
|
||||
|
||||
# Ignore vendored CSS reset
|
||||
|
|
|
@ -11,6 +11,7 @@ require:
|
|||
- rubocop-rspec
|
||||
- rubocop-performance
|
||||
- rubocop-capybara
|
||||
- ./lib/linter/rubocop_middle_dot
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 3.0 # Set to minimum supported version of CI
|
||||
|
@ -205,3 +206,6 @@ Style/TrailingCommaInArrayLiteral:
|
|||
# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
EnforcedStyleForMultiline: 'comma'
|
||||
|
||||
Style/MiddleDot:
|
||||
Enabled: true
|
||||
|
|
|
@ -239,79 +239,6 @@ RSpec/AnyInstance:
|
|||
- 'spec/workers/activitypub/delivery_worker_spec.rb'
|
||||
- 'spec/workers/web/push_notification_worker_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: SkipBlocks, EnforcedStyle.
|
||||
# SupportedStyles: described_class, explicit
|
||||
RSpec/DescribedClass:
|
||||
Exclude:
|
||||
- 'spec/controllers/concerns/cache_concern_spec.rb'
|
||||
- 'spec/controllers/concerns/challengable_concern_spec.rb'
|
||||
- 'spec/lib/entity_cache_spec.rb'
|
||||
- 'spec/lib/extractor_spec.rb'
|
||||
- 'spec/lib/feed_manager_spec.rb'
|
||||
- 'spec/lib/hash_object_spec.rb'
|
||||
- 'spec/lib/ostatus/tag_manager_spec.rb'
|
||||
- 'spec/lib/request_spec.rb'
|
||||
- 'spec/lib/tag_manager_spec.rb'
|
||||
- 'spec/lib/webfinger_resource_spec.rb'
|
||||
- 'spec/mailers/notification_mailer_spec.rb'
|
||||
- 'spec/mailers/user_mailer_spec.rb'
|
||||
- 'spec/models/account_conversation_spec.rb'
|
||||
- 'spec/models/account_domain_block_spec.rb'
|
||||
- 'spec/models/account_migration_spec.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/models/block_spec.rb'
|
||||
- 'spec/models/domain_block_spec.rb'
|
||||
- 'spec/models/email_domain_block_spec.rb'
|
||||
- 'spec/models/export_spec.rb'
|
||||
- 'spec/models/favourite_spec.rb'
|
||||
- 'spec/models/follow_spec.rb'
|
||||
- 'spec/models/identity_spec.rb'
|
||||
- 'spec/models/import_spec.rb'
|
||||
- 'spec/models/media_attachment_spec.rb'
|
||||
- 'spec/models/notification_spec.rb'
|
||||
- 'spec/models/relationship_filter_spec.rb'
|
||||
- 'spec/models/report_filter_spec.rb'
|
||||
- 'spec/models/session_activation_spec.rb'
|
||||
- 'spec/models/setting_spec.rb'
|
||||
- 'spec/models/site_upload_spec.rb'
|
||||
- 'spec/models/status_pin_spec.rb'
|
||||
- 'spec/models/status_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/policies/account_moderation_note_policy_spec.rb'
|
||||
- 'spec/presenters/account_relationships_presenter_spec.rb'
|
||||
- 'spec/presenters/status_relationships_presenter_spec.rb'
|
||||
- 'spec/serializers/activitypub/note_serializer_spec.rb'
|
||||
- 'spec/serializers/activitypub/update_poll_serializer_spec.rb'
|
||||
- 'spec/serializers/rest/account_serializer_spec.rb'
|
||||
- 'spec/services/activitypub/fetch_remote_account_service_spec.rb'
|
||||
- 'spec/services/activitypub/fetch_remote_actor_service_spec.rb'
|
||||
- 'spec/services/activitypub/fetch_remote_key_service_spec.rb'
|
||||
- 'spec/services/after_block_domain_from_account_service_spec.rb'
|
||||
- 'spec/services/authorize_follow_service_spec.rb'
|
||||
- 'spec/services/batched_remove_status_service_spec.rb'
|
||||
- 'spec/services/block_domain_service_spec.rb'
|
||||
- 'spec/services/block_service_spec.rb'
|
||||
- 'spec/services/bootstrap_timeline_service_spec.rb'
|
||||
- 'spec/services/clear_domain_media_service_spec.rb'
|
||||
- 'spec/services/favourite_service_spec.rb'
|
||||
- 'spec/services/follow_service_spec.rb'
|
||||
- 'spec/services/import_service_spec.rb'
|
||||
- 'spec/services/post_status_service_spec.rb'
|
||||
- 'spec/services/precompute_feed_service_spec.rb'
|
||||
- 'spec/services/process_mentions_service_spec.rb'
|
||||
- 'spec/services/purge_domain_service_spec.rb'
|
||||
- 'spec/services/reblog_service_spec.rb'
|
||||
- 'spec/services/reject_follow_service_spec.rb'
|
||||
- 'spec/services/remove_from_followers_service_spec.rb'
|
||||
- 'spec/services/remove_status_service_spec.rb'
|
||||
- 'spec/services/unallow_domain_service_spec.rb'
|
||||
- 'spec/services/unblock_service_spec.rb'
|
||||
- 'spec/services/unfollow_service_spec.rb'
|
||||
- 'spec/services/unmute_service_spec.rb'
|
||||
- 'spec/services/update_account_service_spec.rb'
|
||||
- 'spec/validators/note_length_validator_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
RSpec/EmptyExampleGroup:
|
||||
Exclude:
|
||||
|
@ -468,30 +395,6 @@ RSpec/MessageSpies:
|
|||
- 'spec/spec_helper.rb'
|
||||
- 'spec/validators/status_length_validator_spec.rb'
|
||||
|
||||
RSpec/MissingExampleGroupArgument:
|
||||
Exclude:
|
||||
- 'spec/controllers/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/activitypub/collections_controller_spec.rb'
|
||||
- 'spec/controllers/admin/statuses_controller_spec.rb'
|
||||
- 'spec/controllers/admin/users/roles_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/accounts_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/admin/account_actions_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/admin/domain_allows_controller_spec.rb'
|
||||
- 'spec/controllers/api/v1/statuses_controller_spec.rb'
|
||||
- 'spec/controllers/auth/registrations_controller_spec.rb'
|
||||
- 'spec/features/log_in_spec.rb'
|
||||
- 'spec/lib/activitypub/activity/undo_spec.rb'
|
||||
- 'spec/lib/status_reach_finder_spec.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/models/email_domain_block_spec.rb'
|
||||
- 'spec/models/trends/statuses_spec.rb'
|
||||
- 'spec/models/trends/tags_spec.rb'
|
||||
- 'spec/models/user_role_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
- 'spec/services/fetch_link_card_service_spec.rb'
|
||||
- 'spec/services/notify_service_spec.rb'
|
||||
- 'spec/services/process_mentions_service_spec.rb'
|
||||
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 19
|
||||
|
||||
|
@ -1336,11 +1239,6 @@ Style/GlobalStdStream:
|
|||
- 'config/environments/development.rb'
|
||||
- 'config/environments/production.rb'
|
||||
|
||||
# Configuration parameters: AllowedVariables.
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/statsd.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
|
||||
Style/GuardClause:
|
||||
|
@ -1474,7 +1372,6 @@ Style/RedundantConstantBase:
|
|||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/sidekiq.rb'
|
||||
- 'config/initializers/statsd.rb'
|
||||
- 'config/locales/sr-Latn.rb'
|
||||
- 'config/locales/sr.rb'
|
||||
|
||||
|
@ -1488,52 +1385,6 @@ Style/RedundantFetchBlock:
|
|||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/puma.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Style/RedundantRegexpCharacterClass:
|
||||
Exclude:
|
||||
- 'app/lib/link_details_extractor.rb'
|
||||
- 'app/lib/tag_manager.rb'
|
||||
- 'app/models/domain_allow.rb'
|
||||
- 'app/models/domain_block.rb'
|
||||
- 'app/services/fetch_oembed_service.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/tasks/emojis.rake'
|
||||
- 'lib/tasks/mastodon.rake'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Style/RedundantRegexpEscape:
|
||||
Exclude:
|
||||
- 'app/lib/webfinger_resource.rb'
|
||||
- 'app/models/account.rb'
|
||||
- 'app/models/tag.rb'
|
||||
- 'app/services/fetch_link_card_service.rb'
|
||||
- 'config/initializers/twitter_regex.rb'
|
||||
- 'lib/paperclip/color_extractor.rb'
|
||||
- 'lib/tasks/mastodon.rake'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
|
||||
# SupportedStyles: slashes, percent_r, mixed
|
||||
Style/RegexpLiteral:
|
||||
Exclude:
|
||||
- 'app/lib/link_details_extractor.rb'
|
||||
- 'app/lib/plain_text_formatter.rb'
|
||||
- 'app/lib/tag_manager.rb'
|
||||
- 'app/lib/text_formatter.rb'
|
||||
- 'app/models/account.rb'
|
||||
- 'app/models/domain_allow.rb'
|
||||
- 'app/models/domain_block.rb'
|
||||
- 'app/models/site_upload.rb'
|
||||
- 'app/models/tag.rb'
|
||||
- 'app/services/backup_service.rb'
|
||||
- 'app/services/fetch_oembed_service.rb'
|
||||
- 'app/services/search_service.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'config/initializers/twitter_regex.rb'
|
||||
- 'config/routes.rb'
|
||||
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
||||
- 'lib/tasks/mastodon.rake'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
|
||||
# AllowedMethods: present?, blank?, presence, try, try!
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -5,7 +5,7 @@ ruby '>= 3.0.0'
|
|||
|
||||
gem 'pkg-config', '~> 1.5'
|
||||
|
||||
gem 'puma', '~> 6.2'
|
||||
gem 'puma', '~> 6.3'
|
||||
gem 'rails', '~> 6.1.7'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 1.2'
|
||||
|
@ -17,10 +17,10 @@ gem 'makara', '~> 0.5'
|
|||
gem 'pghero'
|
||||
gem 'dotenv-rails', '~> 2.8'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.122', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
gem 'fog-core', '<= 2.4.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'kt-paperclip', '~> 7.1', github: 'kreeti/kt-paperclip', ref: '11abf222dc31bff71160a1d138b445214f434b2b'
|
||||
gem 'kt-paperclip', '~> 7.2'
|
||||
gem 'blurhash', '~> 0.1'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.10'
|
||||
|
@ -60,7 +60,6 @@ gem 'kaminari', '~> 1.2'
|
|||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'nsa', '~> 0.2'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
|
|
60
Gemfile.lock
60
Gemfile.lock
|
@ -7,18 +7,6 @@ GIT
|
|||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/kreeti/kt-paperclip.git
|
||||
revision: 11abf222dc31bff71160a1d138b445214f434b2b
|
||||
ref: 11abf222dc31bff71160a1d138b445214f434b2b
|
||||
specs:
|
||||
kt-paperclip (7.1.1)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
marcel (~> 1.0.1)
|
||||
mime-types
|
||||
terrapin (~> 0.6.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/mastodon/rails-settings-cached.git
|
||||
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
|
||||
|
@ -109,17 +97,17 @@ GEM
|
|||
attr_required (1.0.1)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.761.0)
|
||||
aws-sdk-core (3.172.0)
|
||||
aws-partitions (1.772.0)
|
||||
aws-sdk-core (3.174.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.64.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-kms (1.65.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.122.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sdk-s3 (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.5.2)
|
||||
|
@ -380,6 +368,12 @@ GEM
|
|||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
kt-paperclip (7.2.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
marcel (~> 1.0.1)
|
||||
mime-types
|
||||
terrapin (~> 0.6.0)
|
||||
launchy (2.5.2)
|
||||
addressable (~> 2.8)
|
||||
letter_opener (1.8.1)
|
||||
|
@ -442,11 +436,6 @@ GEM
|
|||
nokogiri (1.15.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.2.8)
|
||||
activesupport (>= 4.2, < 7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.14.3)
|
||||
omniauth (1.9.2)
|
||||
hashie (>= 3.4.6)
|
||||
|
@ -501,7 +490,7 @@ GEM
|
|||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.2.2)
|
||||
puma (6.3.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -544,8 +533,9 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.5.0)
|
||||
loofah (~> 2.19, >= 2.19.1)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
rails-i18n (6.0.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 7)
|
||||
|
@ -588,7 +578,7 @@ GEM
|
|||
rspec-mocks (3.12.5)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.0.2)
|
||||
rspec-rails (6.0.3)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
|
@ -648,7 +638,7 @@ GEM
|
|||
redis (>= 4.5.0, < 5)
|
||||
sidekiq-bulk (0.2.0)
|
||||
sidekiq
|
||||
sidekiq-scheduler (5.0.2)
|
||||
sidekiq-scheduler (5.0.3)
|
||||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0)
|
||||
|
@ -681,7 +671,6 @@ GEM
|
|||
net-scp (>= 1.1.2)
|
||||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.1)
|
||||
redlock (~> 1.0)
|
||||
strong_migrations (0.8.0)
|
||||
|
@ -770,7 +759,7 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10)
|
||||
addressable (~> 2.8)
|
||||
annotate (~> 3.2)
|
||||
aws-sdk-s3 (~> 1.122)
|
||||
aws-sdk-s3 (~> 1.123)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
blurhash (~> 0.1)
|
||||
|
@ -818,7 +807,7 @@ DEPENDENCIES
|
|||
json-ld-preloaded (~> 3.2)
|
||||
json-schema (~> 4.0)
|
||||
kaminari (~> 1.2)
|
||||
kt-paperclip (~> 7.1)!
|
||||
kt-paperclip (~> 7.2)
|
||||
letter_opener (~> 1.8)
|
||||
letter_opener_web (~> 2.0)
|
||||
link_header (~> 0.0)
|
||||
|
@ -830,7 +819,6 @@ DEPENDENCIES
|
|||
net-http (~> 0.3.2)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
nsa (~> 0.2)
|
||||
oj (~> 3.14)
|
||||
omniauth (~> 1.9)
|
||||
omniauth-cas (~> 2.0)
|
||||
|
@ -846,7 +834,7 @@ DEPENDENCIES
|
|||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
public_suffix (~> 5.0)
|
||||
puma (~> 6.2)
|
||||
puma (~> 6.3)
|
||||
pundit (~> 2.3)
|
||||
rack (~> 2.2.7)
|
||||
rack-attack (~> 6.6)
|
||||
|
@ -894,3 +882,9 @@ DEPENDENCIES
|
|||
webpacker (~> 5.4)
|
||||
webpush!
|
||||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.2p53
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.13
|
||||
|
|
|
@ -14,15 +14,5 @@ module Admin
|
|||
@pending_tags_count = Tag.pending_review.count
|
||||
@pending_appeals_count = Appeal.pending.count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def redis_info
|
||||
@redis_info ||= if redis.is_a?(Redis::Namespace)
|
||||
redis.redis.info
|
||||
else
|
||||
redis.info
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,31 +31,41 @@ module Admin
|
|||
@domain_block = DomainBlock.new(resource_params)
|
||||
existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil
|
||||
|
||||
# Disallow accidentally downgrading a domain block
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
|
||||
@domain_block.errors.delete(:domain)
|
||||
render :new
|
||||
else
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.update(resource_params)
|
||||
end
|
||||
return render :new
|
||||
end
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
# Allow transparently upgrading a domain block
|
||||
if existing_domain_block.present?
|
||||
@domain_block = existing_domain_block
|
||||
@domain_block.assign_attributes(resource_params)
|
||||
end
|
||||
|
||||
# Require explicit confirmation when suspending
|
||||
return render :confirm_suspension if requires_confirmation?
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id)
|
||||
log_action :create, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize :domain_block, :update?
|
||||
|
||||
if @domain_block.update(update_params)
|
||||
@domain_block.assign_attributes(update_params)
|
||||
|
||||
# Require explicit confirmation when suspending
|
||||
return render :confirm_suspension if requires_confirmation?
|
||||
|
||||
if @domain_block.save
|
||||
DomainBlockWorker.perform_async(@domain_block.id, @domain_block.severity_previously_changed?)
|
||||
log_action :update, @domain_block
|
||||
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
|
||||
|
@ -92,5 +102,9 @@ module Admin
|
|||
def action_from_button
|
||||
'save' if params[:save]
|
||||
end
|
||||
|
||||
def requires_confirmation?
|
||||
@domain_block.valid? && (@domain_block.new_record? || @domain_block.severity_changed?) && @domain_block.severity.to_s == 'suspend' && !params[:confirm]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,7 +71,7 @@ module Admin
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:webhook).permit(:url, events: [])
|
||||
params.require(:webhook).permit(:url, :template, events: [])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -90,7 +90,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
||||
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone)
|
||||
end
|
||||
|
||||
def check_enabled_registrations
|
||||
|
|
|
@ -11,7 +11,7 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
|
||||
def index
|
||||
@conversations = paginated_conversations
|
||||
render json: @conversations, each_serializer: REST::ConversationSerializer
|
||||
render json: @conversations, each_serializer: REST::ConversationSerializer, relationships: StatusRelationshipsPresenter.new(@conversations.map(&:last_status), current_user&.account_id)
|
||||
end
|
||||
|
||||
def read
|
||||
|
@ -32,7 +32,20 @@ class Api::V1::ConversationsController < Api::BaseController
|
|||
|
||||
def paginated_conversations
|
||||
AccountConversation.where(account: current_account)
|
||||
.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
.includes(
|
||||
account: :account_stat,
|
||||
last_status: [
|
||||
:media_attachments,
|
||||
:preview_cards,
|
||||
:status_stat,
|
||||
:tags,
|
||||
{
|
||||
active_mentions: [account: :account_stat],
|
||||
account: :account_stat,
|
||||
},
|
||||
]
|
||||
)
|
||||
.to_a_paginated_by_id(limit_param(LIMIT), **params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
|
|
@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController
|
|||
end
|
||||
|
||||
def list_params
|
||||
params.permit(:title, :replies_policy)
|
||||
params.permit(:title, :replies_policy, :exclusive)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,15 +11,15 @@ class BackupsController < ApplicationController
|
|||
def download
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
redirect_to @backup.dump.expiring_url(10)
|
||||
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
|
||||
when :fog
|
||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
|
||||
else
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||
end
|
||||
when :filesystem
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ class Settings::ImportsController < Settings::BaseController
|
|||
muting: 'muted_accounts_failures.csv',
|
||||
domain_blocking: 'blocked_domains_failures.csv',
|
||||
bookmarks: 'bookmarks_failures.csv',
|
||||
lists: 'lists_failures.csv',
|
||||
}.freeze
|
||||
|
||||
TYPE_TO_HEADERS_MAP = {
|
||||
|
@ -20,6 +21,7 @@ class Settings::ImportsController < Settings::BaseController
|
|||
muting: ['Account address', 'Hide notifications'],
|
||||
domain_blocking: false,
|
||||
bookmarks: false,
|
||||
lists: false,
|
||||
}.freeze
|
||||
|
||||
def index
|
||||
|
@ -49,6 +51,8 @@ class Settings::ImportsController < Settings::BaseController
|
|||
csv << [row.data['domain']]
|
||||
when :bookmarks
|
||||
csv << [row.data['uri']]
|
||||
when :lists
|
||||
csv << [row.data['list_name'], row.data['acct']]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController
|
|||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys)
|
||||
params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -170,11 +170,11 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def storage_host
|
||||
URI::HTTPS.build(host: storage_host_name).to_s
|
||||
"https://#{storage_host_var}"
|
||||
end
|
||||
|
||||
def storage_host?
|
||||
storage_host_name.present?
|
||||
storage_host_var.present?
|
||||
end
|
||||
|
||||
def quote_wrap(text, line_width: 80, break_sequence: "\n")
|
||||
|
@ -235,7 +235,7 @@ module ApplicationHelper
|
|||
|
||||
private
|
||||
|
||||
def storage_host_name
|
||||
def storage_host_var
|
||||
ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ module ReactComponentHelper
|
|||
end
|
||||
|
||||
def react_admin_component(name, props = {})
|
||||
data = { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) }
|
||||
data = { 'admin-component': name.to_s.camelcase, props: Oj.dump(props) }
|
||||
div_tag_with_data(data)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,10 +5,6 @@ module SettingsHelper
|
|||
LanguagesHelper::SUPPORTED_LOCALES.keys
|
||||
end
|
||||
|
||||
def hash_to_object(hash)
|
||||
HashObject.new(hash)
|
||||
end
|
||||
|
||||
def session_device_icon(session)
|
||||
device = session.detection.device
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import { unescapeHTML } from 'flavours/glitch/utils/html';
|
|||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
||||
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
|
||||
obj[`:${emoji.shortcode}:`] = emoji;
|
||||
return obj;
|
||||
}, {});
|
||||
|
@ -20,7 +20,7 @@ export function searchTextFromRawStatus (status) {
|
|||
export function normalizeAccount(account) {
|
||||
account = { ...account };
|
||||
|
||||
const emojiMap = makeEmojiMap(account);
|
||||
const emojiMap = makeEmojiMap(account.emojis);
|
||||
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;
|
||||
|
||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
|
||||
|
@ -78,7 +78,7 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||
} else {
|
||||
const spoilerText = normalStatus.spoiler_text || '';
|
||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
const emojiMap = makeEmojiMap(normalStatus);
|
||||
const emojiMap = makeEmojiMap(normalStatus.emojis);
|
||||
|
||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
|
||||
|
@ -89,22 +89,48 @@ export function normalizeStatus(status, normalOldStatus, settings) {
|
|||
return normalStatus;
|
||||
}
|
||||
|
||||
export function normalizeStatusTranslation(translation, status) {
|
||||
const emojiMap = makeEmojiMap(status.get('emojis').toJS());
|
||||
|
||||
const normalTranslation = {
|
||||
detected_source_language: translation.detected_source_language,
|
||||
language: translation.language,
|
||||
provider: translation.provider,
|
||||
contentHtml: emojify(translation.content, emojiMap),
|
||||
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
|
||||
spoiler_text: translation.spoiler_text,
|
||||
};
|
||||
|
||||
return normalTranslation;
|
||||
}
|
||||
|
||||
export function normalizePoll(poll) {
|
||||
const normalPoll = { ...poll };
|
||||
const emojiMap = makeEmojiMap(normalPoll);
|
||||
const emojiMap = makeEmojiMap(poll.emojis);
|
||||
|
||||
normalPoll.options = poll.options.map((option, index) => ({
|
||||
...option,
|
||||
voted: poll.own_votes && poll.own_votes.includes(index),
|
||||
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
|
||||
}));
|
||||
|
||||
return normalPoll;
|
||||
}
|
||||
|
||||
export function normalizePollOptionTranslation(translation, poll) {
|
||||
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());
|
||||
|
||||
const normalTranslation = {
|
||||
...translation,
|
||||
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
|
||||
};
|
||||
|
||||
return normalTranslation;
|
||||
}
|
||||
|
||||
export function normalizeAnnouncement(announcement) {
|
||||
const normalAnnouncement = { ...announcement };
|
||||
const emojiMap = makeEmojiMap(normalAnnouncement);
|
||||
const emojiMap = makeEmojiMap(normalAnnouncement.emojis);
|
||||
|
||||
normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);
|
||||
|
||||
|
|
|
@ -151,10 +151,10 @@ export const createListFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
|
||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
|
||||
api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import IntlMessageFormat from 'intl-messageformat';
|
||||
import { IntlMessageFormat } from 'intl-messageformat';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
|
|
@ -344,7 +344,8 @@ export const translateStatusFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const undoStatusTranslation = id => ({
|
||||
export const undoStatusTranslation = (id, pollId) => ({
|
||||
type: STATUS_TRANSLATE_UNDO,
|
||||
id,
|
||||
pollId,
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @ts-check
|
||||
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
import { getLocale } from 'flavours/glitch/locales';
|
||||
|
||||
import { connectStream } from '../stream';
|
||||
|
||||
|
@ -25,8 +25,6 @@ import {
|
|||
fillListTimelineGaps,
|
||||
} from './timelines';
|
||||
|
||||
const { messages } = getLocale();
|
||||
|
||||
/**
|
||||
* @param {number} max
|
||||
* @returns {number}
|
||||
|
@ -44,8 +42,10 @@ const randomUpTo = max =>
|
|||
* @param {function(object): boolean} [options.accept]
|
||||
* @returns {function(): void}
|
||||
*/
|
||||
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) =>
|
||||
connectStream(channelName, params, (dispatch, getState) => {
|
||||
export const connectTimelineStream = (timelineId, channelName, params = {}, options = {}) => {
|
||||
const { messages } = getLocale();
|
||||
|
||||
return connectStream(channelName, params, (dispatch, getState) => {
|
||||
const locale = getState().getIn(['meta', 'locale']);
|
||||
|
||||
// @ts-expect-error
|
||||
|
@ -122,6 +122,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Function} dispatch
|
||||
|
|
|
@ -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 * as React from 'react';
|
||||
|
||||
import type { InjectedIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
|
@ -16,9 +15,11 @@ const messages = defineMessages({
|
|||
interface Props {
|
||||
domain: string;
|
||||
onUnblockDomain: (domain: string) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
|
||||
|
||||
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDomainUnblock = useCallback(() => {
|
||||
onUnblockDomain(domain);
|
||||
}, [domain, onUnblockDomain]);
|
||||
|
@ -42,5 +43,3 @@ const _Domain: React.FC<Props> = ({ domain, onUnblockDomain, intl }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Domain = injectIntl(_Domain);
|
||||
|
|
|
@ -121,10 +121,10 @@ class DropdownMenu extends PureComponent {
|
|||
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 (
|
||||
<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}>
|
||||
{text}
|
||||
</a>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import type { InjectedIntl } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
@ -13,10 +12,11 @@ interface Props {
|
|||
disabled: boolean;
|
||||
maxId: string;
|
||||
onClick: (maxId: string) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
|
||||
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(maxId);
|
||||
}, [maxId, onClick]);
|
||||
|
@ -32,5 +32,3 @@ const _LoadGap: React.FC<Props> = ({ disabled, maxId, onClick, intl }) => {
|
|||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadGap = injectIntl(_LoadGap);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
24
app/javascript/flavours/glitch/components/load_more.tsx
Normal file
24
app/javascript/flavours/glitch/components/load_more.tsx
Normal file
|
@ -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 () {
|
||||
const { status, lang, width, height, revealed } = this.props;
|
||||
const { status, width, height, revealed } = this.props;
|
||||
const mediaAttachments = status.get('media_attachments');
|
||||
const language = status.getIn(['language', 'translation']) || status.get('language') || this.props.lang;
|
||||
|
||||
if (mediaAttachments.size === 0) {
|
||||
return null;
|
||||
|
@ -61,14 +62,15 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
|||
|
||||
if (mediaAttachments.getIn([0, 'type']) === 'audio') {
|
||||
const audio = mediaAttachments.get(0);
|
||||
const description = audio.getIn(['translation', 'description']) || audio.get('description');
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
lang={lang || status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
width={width}
|
||||
height={height}
|
||||
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
|
@ -82,6 +84,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
|||
);
|
||||
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
|
||||
const video = mediaAttachments.get(0);
|
||||
const description = video.getIn(['translation', 'description']) || video.get('description');
|
||||
|
||||
return (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
|
@ -91,8 +94,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
|||
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
lang={lang || status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
width={width}
|
||||
height={height}
|
||||
inline
|
||||
|
@ -109,7 +112,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
|
|||
{Component => (
|
||||
<Component
|
||||
media={mediaAttachments}
|
||||
lang={lang || status.get('language')}
|
||||
lang={language}
|
||||
sensitive={status.get('sensitive')}
|
||||
defaultWidth={width}
|
||||
revealed={revealed}
|
||||
|
|
|
@ -124,10 +124,12 @@ class Item extends PureComponent {
|
|||
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
|
||||
}
|
||||
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
if (attachment.get('type') === 'unknown') {
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
className='media-gallery__preview'
|
||||
|
@ -166,8 +168,8 @@ class Item extends PureComponent {
|
|||
src={previewUrl}
|
||||
srcSet={srcSet}
|
||||
sizes={sizes}
|
||||
alt={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
alt={description}
|
||||
title={description}
|
||||
lang={lang}
|
||||
style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
|
@ -183,8 +185,8 @@ class Item extends PureComponent {
|
|||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||
<video
|
||||
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
|
||||
aria-label={attachment.get('description')}
|
||||
title={attachment.get('description')}
|
||||
aria-label={description}
|
||||
title={description}
|
||||
lang={lang}
|
||||
role='application'
|
||||
src={attachment.get('url')}
|
||||
|
|
|
@ -58,9 +58,9 @@ class Poll extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
static getDerivedStateFromProps (props, state) {
|
||||
const { poll, intl } = props;
|
||||
const { poll } = props;
|
||||
const expires_at = poll.get('expires_at');
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < intl.now();
|
||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
|
||||
return (expired === state.expired) ? null : { expired };
|
||||
}
|
||||
|
||||
|
@ -77,10 +77,10 @@ class Poll extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
_setupTimer () {
|
||||
const { poll, intl } = this.props;
|
||||
const { poll } = this.props;
|
||||
clearTimeout(this._timer);
|
||||
if (!this.state.expired) {
|
||||
const delay = (new Date(poll.get('expires_at'))).getTime() - intl.now();
|
||||
const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
|
||||
this._timer = setTimeout(() => {
|
||||
this.setState({ expired: true });
|
||||
}, delay);
|
||||
|
@ -139,10 +139,12 @@ class Poll extends ImmutablePureComponent {
|
|||
const active = !!this.state.selected[`${optionIndex}`];
|
||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
||||
|
||||
let titleEmojified = option.get('title_emojified');
|
||||
if (!titleEmojified) {
|
||||
const title = option.getIn(['translation', 'title']) || option.get('title');
|
||||
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
|
||||
|
||||
if (!titleHtml) {
|
||||
const emojiMap = makeEmojiMap(poll);
|
||||
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
|
||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -164,7 +166,7 @@ class Poll extends ImmutablePureComponent {
|
|||
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||
onKeyPress={this.handleOptionKeyPress}
|
||||
aria-checked={active}
|
||||
aria-label={option.get('title')}
|
||||
aria-label={title}
|
||||
lang={lang}
|
||||
data-index={optionIndex}
|
||||
/>
|
||||
|
@ -183,7 +185,7 @@ class Poll extends ImmutablePureComponent {
|
|||
<span
|
||||
className='poll__option__text translate'
|
||||
lang={lang}
|
||||
dangerouslySetInnerHTML={{ __html: titleEmojified }}
|
||||
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
||||
/>
|
||||
|
||||
{!!voted && <span className='poll__voted'>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component } from 'react';
|
||||
|
||||
import type { InjectedIntl } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -103,7 +103,7 @@ const getUnitDelay = (units: string) => {
|
|||
};
|
||||
|
||||
export const timeAgoString = (
|
||||
intl: InjectedIntl,
|
||||
intl: IntlShape,
|
||||
date: Date,
|
||||
now: number,
|
||||
year: number,
|
||||
|
@ -155,7 +155,7 @@ export const timeAgoString = (
|
|||
};
|
||||
|
||||
const timeRemainingString = (
|
||||
intl: InjectedIntl,
|
||||
intl: IntlShape,
|
||||
date: Date,
|
||||
now: number,
|
||||
timeGiven = true
|
||||
|
@ -190,7 +190,7 @@ const timeRemainingString = (
|
|||
};
|
||||
|
||||
interface Props {
|
||||
intl: InjectedIntl;
|
||||
intl: IntlShape;
|
||||
timestamp: string;
|
||||
year: number;
|
||||
futureDate?: boolean;
|
||||
|
@ -201,7 +201,7 @@ interface States {
|
|||
}
|
||||
class RelativeTimestamp extends Component<Props, States> {
|
||||
state = {
|
||||
now: this.props.intl.now(),
|
||||
now: Date.now(),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -223,7 +223,7 @@ class RelativeTimestamp extends Component<Props, States> {
|
|||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.timestamp !== nextProps.timestamp) {
|
||||
this.setState({ now: this.props.intl.now() });
|
||||
this.setState({ now: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,7 +253,7 @@ class RelativeTimestamp extends Component<Props, States> {
|
|||
: Math.max(updateInterval, unitRemainder);
|
||||
|
||||
this._timer = window.setTimeout(() => {
|
||||
this.setState({ now: this.props.intl.now() });
|
||||
this.setState({ now: Date.now() });
|
||||
}, delay);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import IntersectionObserverWrapper from 'flavours/glitch/features/ui/util/inters
|
|||
|
||||
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 LoadingIndicator from './loading_indicator';
|
||||
|
||||
|
|
|
@ -26,12 +26,18 @@ import StatusHeader from './status_header';
|
|||
import StatusIcons from './status_icons';
|
||||
import StatusPrepend from './status_prepend';
|
||||
|
||||
const domParser = new DOMParser();
|
||||
|
||||
export const textForScreenReader = (intl, status, rebloggedByText = false, expanded = false) => {
|
||||
const displayName = status.getIn(['account', 'display_name']);
|
||||
|
||||
const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
|
||||
const contentHtml = status.getIn(['translation', 'contentHtml']) || status.get('contentHtml');
|
||||
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;
|
||||
|
||||
const values = [
|
||||
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
|
||||
status.get('spoiler_text') && !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' }),
|
||||
status.getIn(['account', 'acct']),
|
||||
];
|
||||
|
@ -391,12 +397,14 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleOpenVideo = (options) => {
|
||||
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) => {
|
||||
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 => {
|
||||
|
@ -406,10 +414,11 @@ class Status extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
const lang = status.getIn(['translation', 'language']) || status.get('language');
|
||||
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 {
|
||||
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 />);
|
||||
mediaIcons.push('video-camera');
|
||||
} else if (attachments.size > 0) {
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
|
||||
if (muted || attachments.some(item => item.get('type') === 'unknown')) {
|
||||
media.push(
|
||||
<AttachmentList
|
||||
|
@ -634,14 +645,15 @@ class Status extends ImmutablePureComponent {
|
|||
);
|
||||
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media.push(
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
|
@ -662,6 +674,7 @@ class Status extends ImmutablePureComponent {
|
|||
mediaIcons.push('music');
|
||||
} else if (attachments.getIn([0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media.push(
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
|
@ -670,8 +683,8 @@ class Status extends ImmutablePureComponent {
|
|||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
|
@ -691,7 +704,7 @@ class Status extends ImmutablePureComponent {
|
|||
{Component => (
|
||||
<Component
|
||||
media={attachments}
|
||||
lang={status.get('language')}
|
||||
lang={language}
|
||||
sensitive={status.get('sensitive')}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
fullwidth={!rootId && settings.getIn(['media', 'fullwidth'])}
|
||||
|
@ -724,7 +737,8 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
|
|
|
@ -243,21 +243,21 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
if (writtenByMe) {
|
||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
|
||||
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({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
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, dangerous: true });
|
||||
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)) {
|
||||
menu.push(null);
|
||||
|
|
|
@ -327,11 +327,11 @@ class StatusContent extends PureComponent {
|
|||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
||||
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
|
||||
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
|
||||
|
||||
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
|
||||
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||
const lang = status.get('translation') ? intl.locale : status.get('language');
|
||||
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
|
||||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const classNames = classnames('status__content', {
|
||||
'status__content--with-action': parseClick && !disabled,
|
||||
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
|
||||
|
@ -396,7 +396,7 @@ class StatusContent extends PureComponent {
|
|||
<p
|
||||
style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} />
|
||||
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
|
||||
{' '}
|
||||
<button type='button' className='status__content__spoiler-link' onClick={this.handleSpoilerClick} aria-expanded={!hidden}>
|
||||
{toggleText}
|
||||
|
@ -414,7 +414,7 @@ class StatusContent extends PureComponent {
|
|||
className='status__content__text translate'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
lang={language}
|
||||
/>
|
||||
{!hidden && translateButton}
|
||||
{media}
|
||||
|
@ -439,7 +439,7 @@ class StatusContent extends PureComponent {
|
|||
tabIndex={0}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
lang={language}
|
||||
/>
|
||||
{translateButton}
|
||||
{media}
|
||||
|
@ -460,7 +460,7 @@ class StatusContent extends PureComponent {
|
|||
tabIndex={0}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
lang={lang}
|
||||
lang={language}
|
||||
/>
|
||||
{translateButton}
|
||||
{media}
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
|
||||
export default class AdminComponent extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { locale, children } = this.props;
|
||||
const { children } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
|
|
@ -1,38 +1,25 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis';
|
||||
import { hydrateStore } from 'flavours/glitch/actions/store';
|
||||
import Compose from 'flavours/glitch/features/standalone/compose';
|
||||
import initialState from 'flavours/glitch/initial_state';
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
import { store } from 'flavours/glitch/store';
|
||||
|
||||
import { getLocale } from 'mastodon/locales';
|
||||
|
||||
const { localeData, messages } = getLocale();
|
||||
addLocaleData(localeData);
|
||||
|
||||
if (initialState) {
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
}
|
||||
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
|
||||
export default class TimelineContainer extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
};
|
||||
export default class ComposeContainer extends PureComponent {
|
||||
|
||||
render () {
|
||||
const { locale } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<Provider store={store}>
|
||||
<Compose />
|
||||
</Provider>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { BrowserRouter, Route } from 'react-router-dom';
|
||||
|
||||
|
@ -17,11 +15,8 @@ import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
|||
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
||||
import UI from 'flavours/glitch/features/ui';
|
||||
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
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)`;
|
||||
|
||||
|
@ -45,10 +40,6 @@ const createIdentityContext = state => ({
|
|||
|
||||
export default class Mastodon extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static childContextTypes = {
|
||||
identity: PropTypes.shape({
|
||||
signedIn: PropTypes.bool.isRequired,
|
||||
|
@ -84,10 +75,8 @@ export default class Mastodon extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { locale } = this.props;
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<BrowserRouter>
|
||||
|
|
|
@ -2,8 +2,6 @@ import PropTypes from 'prop-types';
|
|||
import { PureComponent } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import { ImmutableHashtag as Hashtag } from '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 MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||
import Video from 'flavours/glitch/features/video';
|
||||
import { IntlProvider } from 'flavours/glitch/locales';
|
||||
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 };
|
||||
|
||||
export default class MediaContainer extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
components: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -75,7 +68,7 @@ export default class MediaContainer extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { locale, components } = this.props;
|
||||
const { components } = this.props;
|
||||
|
||||
let handleOpenVideo;
|
||||
|
||||
|
@ -85,7 +78,7 @@ export default class MediaContainer extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<IntlProvider>
|
||||
<>
|
||||
{[].map.call(components, (component, i) => {
|
||||
const componentName = component.getAttribute('data-component');
|
||||
|
|
|
@ -47,7 +47,7 @@ const messages = defineMessages({
|
|||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
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?' },
|
||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
||||
|
@ -218,7 +218,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
|
||||
onTranslate (status) {
|
||||
if (status.get('translation')) {
|
||||
dispatch(undoStatusTranslation(status.get('id')));
|
||||
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||
} else {
|
||||
dispatch(translateStatus(status.get('id')));
|
||||
}
|
||||
|
|
|
@ -272,16 +272,16 @@ class Header extends ImmutablePureComponent {
|
|||
if (account.getIn(['relationship', 'muting'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute, dangerous: true });
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock, dangerous: true });
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true });
|
||||
}
|
||||
|
||||
if (signedIn && isRemote) {
|
||||
|
@ -290,7 +290,7 @@ class Header extends ImmutablePureComponent {
|
|||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain });
|
||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { connect } from 'react-redux';
|
|||
import { lookupAccount, fetchAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
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 ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
import ProfileColumnHeader from 'flavours/glitch/features/account/components/profile_column_header';
|
||||
|
|
|
@ -153,7 +153,7 @@ export default class Header extends ImmutablePureComponent {
|
|||
{!(hideTabs || hidden) && (
|
||||
<div className='account__section-headline'>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class MovedNote extends ImmutablePureComponent {
|
|||
<div className='account__moved-note'>
|
||||
<div className='account__moved-note__message'>
|
||||
<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>
|
||||
|
||||
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||
|
|
|
@ -148,7 +148,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
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),
|
||||
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 { 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 StatusContainer from 'flavours/glitch/containers/status_container';
|
||||
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 Column from 'flavours/glitch/components/column';
|
||||
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 { RadioButton } from 'flavours/glitch/components/radio_button';
|
||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
|
|
|
@ -69,7 +69,7 @@ class Explore extends PureComponent {
|
|||
<Search />
|
||||
</div>
|
||||
|
||||
<div className='scrollable scrollable--flex'>
|
||||
<div className='scrollable scrollable--flex' data-nosnippet>
|
||||
{isSearching ? (
|
||||
<SearchResults />
|
||||
) : (
|
||||
|
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { expandSearch } from 'flavours/glitch/actions/search';
|
||||
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 Account from 'flavours/glitch/containers/account_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.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.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 className='explore__search-results'>
|
||||
|
|
|
@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||
import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
@ -145,7 +147,13 @@ class ListTimeline extends PureComponent {
|
|||
handleRepliesPolicyChange = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, target.value));
|
||||
dispatch(updateList(id, undefined, false, undefined, target.value));
|
||||
};
|
||||
|
||||
onExclusiveToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, target.checked, undefined));
|
||||
};
|
||||
|
||||
render () {
|
||||
|
@ -154,6 +162,7 @@ class ListTimeline extends PureComponent {
|
|||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
|
@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ replies_policy !== undefined && (
|
||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
||||
|
|
|
@ -275,7 +275,7 @@ class LocalSettingsPage extends PureComponent {
|
|||
),
|
||||
({ intl, onChange, settings }) => (
|
||||
<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
|
||||
settings={settings}
|
||||
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(null);
|
||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick });
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick });
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick });
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
|
||||
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, dangerous: true });
|
||||
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)) {
|
||||
menu.push(null);
|
||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
|
||||
|
|
|
@ -158,6 +158,8 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
outerStyle.height = `${this.state.height}px`;
|
||||
}
|
||||
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
|
||||
if (pictureInPicture.get('inUse')) {
|
||||
media.push(<PictureInPicturePlaceholder />);
|
||||
mediaIcons.push('video-camera');
|
||||
|
@ -166,12 +168,13 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media.push(
|
||||
<Audio
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
|
@ -187,14 +190,16 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
mediaIcons.push('music');
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||
|
||||
media.push(
|
||||
<Video
|
||||
preview={attachment.get('preview_url')}
|
||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||
blurhash={attachment.get('blurhash')}
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
lang={status.get('language')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
|
@ -213,7 +218,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
standalone
|
||||
sensitive={status.get('sensitive')}
|
||||
media={status.get('media_attachments')}
|
||||
lang={status.get('language')}
|
||||
lang={language}
|
||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
||||
hidden={!expanded}
|
||||
|
@ -251,7 +256,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
} else if (this.context.router) {
|
||||
reblogLink = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||
<Icon id={reblogIcon} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
|
@ -263,7 +268,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
} else {
|
||||
reblogLink = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||
<Icon id={reblogIcon} />
|
||||
<span className='detailed-status__reblogs'>
|
||||
|
@ -297,7 +302,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
if (status.get('edited_at')) {
|
||||
edited = (
|
||||
<>
|
||||
·
|
||||
{' · '}
|
||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ const messages = defineMessages({
|
|||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' },
|
||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
|
||||
revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
|
||||
hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
|
||||
statusTitleWithAttachments: { id: 'status.title.with_attachments', defaultMessage: '{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}' },
|
||||
|
@ -481,7 +481,7 @@ class Status extends ImmutablePureComponent {
|
|||
const { dispatch } = this.props;
|
||||
|
||||
if (status.get('translation')) {
|
||||
dispatch(undoStatusTranslation(status.get('id')));
|
||||
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
|
||||
} else {
|
||||
dispatch(translateStatus(status.get('id')));
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import Audio from 'flavours/glitch/features/audio';
|
|||
import Footer from 'flavours/glitch/features/picture_in_picture/components/footer';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
language: state.getIn(['statuses', statusId, 'language']),
|
||||
status: state.getIn(['statuses', statusId]),
|
||||
accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']),
|
||||
});
|
||||
|
||||
|
@ -17,7 +17,7 @@ class AudioModal extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
statusId: PropTypes.string.isRequired,
|
||||
language: PropTypes.string,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
accountStaticAvatar: PropTypes.string.isRequired,
|
||||
options: PropTypes.shape({
|
||||
autoPlay: PropTypes.bool,
|
||||
|
@ -31,15 +31,17 @@ class AudioModal extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { media, language, accountStaticAvatar, statusId, onClose } = this.props;
|
||||
const { media, status, accountStaticAvatar, onClose } = this.props;
|
||||
const options = this.props.options || {};
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const description = media.getIn(['translation', 'description']) || media.get('description');
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal audio-modal'>
|
||||
<div className='audio-modal__container'>
|
||||
<Audio
|
||||
src={media.get('url')}
|
||||
alt={media.get('description')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
duration={media.getIn(['meta', 'original', 'duration'], 0)}
|
||||
height={150}
|
||||
|
@ -52,7 +54,7 @@ class AudioModal extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -147,6 +147,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
const content = media.map((image) => {
|
||||
const width = image.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||
const description = image.getIn(['translation', 'description']) || image.get('description');
|
||||
|
||||
if (image.get('type') === 'image') {
|
||||
return (
|
||||
|
@ -155,7 +156,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
src={image.get('url')}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={image.get('description')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
onClick={this.toggleNavigation}
|
||||
|
@ -178,7 +179,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
volume={volume || 1}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={image.get('description')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
/>
|
||||
|
@ -190,7 +191,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
width={width}
|
||||
height={height}
|
||||
key={image.get('url')}
|
||||
alt={image.get('description')}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
onClick={this.toggleNavigation}
|
||||
/>
|
||||
|
|
|
@ -9,7 +9,7 @@ import Footer from 'flavours/glitch/features/picture_in_picture/components/foote
|
|||
import Video from 'flavours/glitch/features/video';
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
language: state.getIn(['statuses', statusId, 'language']),
|
||||
status: state.getIn(['statuses', statusId]),
|
||||
});
|
||||
|
||||
class VideoModal extends ImmutablePureComponent {
|
||||
|
@ -17,7 +17,7 @@ class VideoModal extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
statusId: PropTypes.string,
|
||||
language: PropTypes.string,
|
||||
status: ImmutablePropTypes.map,
|
||||
options: PropTypes.shape({
|
||||
startTime: PropTypes.number,
|
||||
autoPlay: PropTypes.bool,
|
||||
|
@ -38,8 +38,10 @@ class VideoModal extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { media, statusId, language, onClose } = this.props;
|
||||
const { media, status, onClose } = this.props;
|
||||
const options = this.props.options || {};
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
const description = media.getIn(['translation', 'description']) || media.get('description');
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal video-modal'>
|
||||
|
@ -55,13 +57,13 @@ class VideoModal extends ImmutablePureComponent {
|
|||
onCloseVideo={onClose}
|
||||
autoFocus
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
alt={description}
|
||||
lang={language}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='media-modal__overlay'>
|
||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
||||
{status && <Footer statusId={status.get('id')} withOpenButton onClose={onClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
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.follows": "Follows",
|
||||
"account.joined": "Joined {date}",
|
||||
"account.mute_notifications": "Mute notifications from @{name}",
|
||||
"account.suspended_disclaimer_full": "This user has been suspended by a moderator.",
|
||||
"account.unmute_notifications": "Unmute notifications from @{name}",
|
||||
"account.view_full_profile": "View full profile",
|
||||
"account_note.cancel": "Cancel",
|
||||
"account_note.edit": "Edit",
|
||||
|
|
22
app/javascript/flavours/glitch/locales/global_locale.ts
Normal file
22
app/javascript/flavours/glitch/locales/global_locale.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export interface LocaleData {
|
||||
locale: string;
|
||||
messages: Record<string, string>;
|
||||
}
|
||||
|
||||
let loadedLocale: LocaleData;
|
||||
|
||||
export function setLocale(locale: LocaleData) {
|
||||
loadedLocale = locale;
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
if (!loadedLocale && process.env.NODE_ENV === 'development') {
|
||||
throw new Error('getLocale() called before any locale has been set');
|
||||
}
|
||||
|
||||
return loadedLocale;
|
||||
}
|
||||
|
||||
export function isLocaleLoaded() {
|
||||
return !!loadedLocale;
|
||||
}
|
5
app/javascript/flavours/glitch/locales/index.ts
Normal file
5
app/javascript/flavours/glitch/locales/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type { LocaleData } from './global_locale';
|
||||
export { setLocale, getLocale, isLocaleLoaded } from './global_locale';
|
||||
export { loadLocale } from './load_locale';
|
||||
|
||||
export { IntlProvider } from './intl_provider';
|
57
app/javascript/flavours/glitch/locales/intl_provider.tsx
Normal file
57
app/javascript/flavours/glitch/locales/intl_provider.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
||||
|
||||
import { getLocale, isLocaleLoaded } from './global_locale';
|
||||
import { loadLocale } from './load_locale';
|
||||
|
||||
function onProviderError(error: unknown) {
|
||||
// Silent the error, like upstream does
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
|
||||
// This browser does not advertise Intl support for this locale, we only print a warning
|
||||
// As-per the spec, the browser should select the best matching locale
|
||||
if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
error instanceof Error &&
|
||||
error.message.match('MISSING_DATA')
|
||||
) {
|
||||
console.warn(error.message);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
export const IntlProvider: React.FC<
|
||||
Omit<React.ComponentProps<typeof BaseIntlProvider>, 'locale' | 'messages'>
|
||||
> = ({ children, ...props }) => {
|
||||
const [localeLoaded, setLocaleLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadLocaleData() {
|
||||
if (!isLocaleLoaded()) {
|
||||
await loadLocale();
|
||||
}
|
||||
|
||||
setLocaleLoaded(true);
|
||||
}
|
||||
void loadLocaleData();
|
||||
}, []);
|
||||
|
||||
if (!localeLoaded) return null;
|
||||
|
||||
const { locale, messages } = getLocale();
|
||||
|
||||
return (
|
||||
<BaseIntlProvider
|
||||
locale={locale}
|
||||
messages={messages}
|
||||
onError={onProviderError}
|
||||
textComponent='span'
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</BaseIntlProvider>
|
||||
);
|
||||
};
|
37
app/javascript/flavours/glitch/locales/load_locale.ts
Normal file
37
app/javascript/flavours/glitch/locales/load_locale.ts
Normal file
|
@ -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 });
|
||||
});
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
|
@ -1,2 +0,0 @@
|
|||
[
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue