diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index 0671df390f..0000000000 --- a/.bundler-audit.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -ignore: - # devise-two-factor advisory about brute-forcing TOTP - # We have rate-limits on authentication endpoints in place (including second - # factor verification) since Mastodon v3.2.0 - - CVE-2024-0227 diff --git a/.eslintrc.js b/.eslintrc.js index 8fe3a98b4a..66a0f12ba1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,10 +20,6 @@ module.exports = defineConfig({ es6: true, }, - globals: { - ATTACHMENT_HOST: false, - }, - parser: '@typescript-eslint/parser', plugins: [ @@ -79,7 +75,7 @@ module.exports = defineConfig({ ], }, ], - 'no-empty': 'off', + 'no-empty': ['error', { "allowEmptyCatch": true }], 'no-restricted-properties': [ 'error', { property: 'substring', message: 'Use .slice instead of .substring.' }, @@ -94,7 +90,6 @@ module.exports = defineConfig({ message: "Use '·' (middle dot) instead of '•' (bullet)", }, ], - 'no-self-assign': 'off', 'no-unused-expressions': 'error', 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': [ @@ -119,12 +114,10 @@ module.exports = defineConfig({ 'react/jsx-tag-spacing': 'error', 'react/jsx-uses-react': 'off', // not needed with new JSX transform 'react/jsx-wrap-multilines': 'error', - 'react/no-deprecated': 'off', 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/self-closing-comp': 'error', // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46 - 'jsx-a11y/accessible-emoji': 'warn', 'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/media-has-caption': 'off', @@ -139,23 +132,6 @@ module.exports = defineConfig({ // ], 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'off', // recommended rule is: - // 'jsx-a11y/no-noninteractive-element-interactions': [ - // 'error', - // { - // body: ['onError', 'onLoad'], - // iframe: ['onError', 'onLoad'], - // img: ['onError', 'onLoad'], - // }, - // ], - 'jsx-a11y/no-noninteractive-element-interactions': [ - 'warn', - { - handlers: [ - 'onClick', - ], - }, - ], - // recommended rule is: // 'jsx-a11y/no-noninteractive-tabindex': [ // 'error', // { @@ -165,7 +141,6 @@ module.exports = defineConfig({ // }, // ], 'jsx-a11y/no-noninteractive-tabindex': 'off', - 'jsx-a11y/no-onchange': 'off', // recommended is full 'error' 'jsx-a11y/no-static-element-interactions': [ 'warn', @@ -366,6 +341,9 @@ module.exports = defineConfig({ // Disable formatting rules that have been enabled in the base config 'indent': 'off', + // This is not needed as we use noImplicitReturns, which handles this in addition to understanding types + 'consistent-return': 'off', + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml index bbc31598c7..e3e2da0c78 100644 --- a/.github/workflows/bundler-audit.yml +++ b/.github/workflows/bundler-audit.yml @@ -6,14 +6,12 @@ on: paths: - 'Gemfile*' - '.ruby-version' - - '.bundler-audit.yml' - '.github/workflows/bundler-audit.yml' pull_request: paths: - 'Gemfile*' - '.ruby-version' - - '.bundler-audit.yml' - '.github/workflows/bundler-audit.yml' schedule: @@ -23,12 +21,17 @@ jobs: security: runs-on: ubuntu-latest + env: + BUNDLE_ONLY: development + steps: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true - name: Run bundler-audit - run: bundle exec bundler-audit + run: bundle exec bundler-audit check --update diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 25615b720d..ca4b0c80bf 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -26,12 +26,18 @@ on: jobs: lint: runs-on: ubuntu-latest + + env: + BUNDLE_ONLY: development + steps: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true - name: Run haml-lint run: | diff --git a/.github/workflows/lint-ruby.yml b/.github/workflows/lint-ruby.yml index 411b323486..b3a89c3caf 100644 --- a/.github/workflows/lint-ruby.yml +++ b/.github/workflows/lint-ruby.yml @@ -27,19 +27,24 @@ jobs: lint: runs-on: ubuntu-latest + env: + BUNDLE_ONLY: development + steps: - name: Clone repository uses: actions/checkout@v4 - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true - name: Set-up RuboCop Problem Matcher uses: r7kamura/rubocop-problem-matchers-action@v1 - name: Run rubocop - run: bundle exec rubocop + run: bin/rubocop - name: Run brakeman if: always() # Run both checks, even if the first failed - run: bundle exec brakeman + run: bin/brakeman diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml deleted file mode 100644 index 6698847315..0000000000 --- a/.github/workflows/test-migrations-two-step.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: Test two step migrations -on: - push: - branches-ignore: - - 'dependabot/**' - - 'renovate/**' - pull_request: - -jobs: - pre_job: - runs-on: ubuntu-latest - - outputs: - should_skip: ${{ steps.skip_check.outputs.should_skip }} - - steps: - - id: skip_check - uses: fkirc/skip-duplicate-actions@v5 - with: - paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-two-step.yml", "lib/tasks/tests.rake"]' - - test: - runs-on: ubuntu-latest - needs: pre_job - if: needs.pre_job.outputs.should_skip != 'true' - - strategy: - fail-fast: false - - matrix: - postgres: - - 14-alpine - - 15-alpine - - services: - postgres: - image: postgres:${{ matrix.postgres}} - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 6379:6379 - - env: - CONTINUOUS_INTEGRATION: true - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - DISABLE_SIMPLECOV: true - RAILS_ENV: test - BUNDLE_CLEAN: true - BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development production' - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 - - steps: - - uses: actions/checkout@v4 - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - - - name: Create database - run: './bin/rails db:create' - - - name: Run historical migrations with data population - run: './bin/rails tests:migrations:prepare_database' - env: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - - name: Run all remaining pre-deployment migrations - run: './bin/rails db:migrate' - env: - SKIP_POST_DEPLOYMENT_MIGRATIONS: true - - - name: Run all post-deployment migrations - run: './bin/rails db:migrate' - - - name: Check migration result - run: './bin/rails tests:migrations:check_database' diff --git a/.github/workflows/test-migrations-one-step.yml b/.github/workflows/test-migrations.yml similarity index 58% rename from .github/workflows/test-migrations-one-step.yml rename to .github/workflows/test-migrations.yml index 1ff5cc06b9..3eaf2c2d74 100644 --- a/.github/workflows/test-migrations-one-step.yml +++ b/.github/workflows/test-migrations.yml @@ -1,4 +1,5 @@ -name: Test one step migrations +name: Historical data migration test + on: push: branches-ignore: @@ -17,7 +18,7 @@ jobs: - id: skip_check uses: fkirc/skip-duplicate-actions@v5 with: - paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations-one-step.yml", "lib/tasks/tests.rake"]' + paths: '["Gemfile*", ".ruby-version", "**/*.rb", ".github/workflows/test-migrations.yml", "lib/tasks/tests.rake"]' test: runs-on: ubuntu-latest @@ -40,9 +41,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 5432:5432 @@ -50,14 +51,13 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 6379:6379 env: - CONTINUOUS_INTEGRATION: true DB_HOST: localhost DB_USER: postgres DB_PASS: postgres @@ -65,7 +65,7 @@ jobs: RAILS_ENV: test BUNDLE_CLEAN: true BUNDLE_FROZEN: true - BUNDLE_WITHOUT: 'development production' + BUNDLE_WITHOUT: 'development:production' BUNDLE_JOBS: 3 BUNDLE_RETRY: 3 @@ -75,14 +75,19 @@ jobs: - name: Set up Ruby environment uses: ./.github/actions/setup-ruby - - name: Create database - run: './bin/rails db:create' + - name: Test "one step migration" flow + run: | + bin/rails db:drop + bin/rails db:create + bin/rails tests:migrations:prepare_database + bin/rails db:migrate + bin/rails tests:migrations:check_database - - name: Run historical migrations with data population - run: './bin/rails tests:migrations:prepare_database' - - - name: Run all remaining migrations - run: './bin/rails db:migrate' - - - name: Check migration result - run: './bin/rails tests:migrations:check_database' + - name: Test "two step migration" flow + run: | + bin/rails db:drop + bin/rails db:create + SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database + SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate + bin/rails db:migrate + bin/rails tests:migrations:check_database diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 5f2297381a..ef898968d0 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -28,11 +28,7 @@ jobs: env: RAILS_ENV: ${{ matrix.mode }} BUNDLE_WITH: ${{ matrix.mode }} - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder - OTP_SECRET: precompile_placeholder - SECRET_KEY_BASE: precompile_placeholder + SECRET_KEY_BASE_DUMMY: 1 steps: - uses: actions/checkout@v4 @@ -77,9 +73,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 5432:5432 @@ -87,9 +83,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 6379:6379 @@ -136,15 +132,17 @@ jobs: additional-system-dependencies: ffmpeg libpam-dev - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' + run: | + bin/rails db:setup + bin/flatware fan bin/rails db:test:prepare - - run: bin/rspec + - run: bin/flatware rspec -r ./spec/flatware_helper.rb - name: Upload coverage reports to Codecov if: matrix.ruby-version == '.ruby-version' uses: codecov/codecov-action@v4 with: - files: coverage/lcov/mastodon.lcov + files: coverage/lcov/*.lcov env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -163,9 +161,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 5432:5432 @@ -173,9 +171,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 6379:6379 @@ -250,9 +248,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 5432:5432 @@ -260,9 +258,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 6379:6379 @@ -289,9 +287,13 @@ jobs: - uses: actions/download-artifact@v4 with: - path: './public' + path: './' name: ${{ github.sha }} + - name: Expand archived asset artifacts + run: | + tar xvzf artifacts.tar.gz + - name: Set up Ruby environment uses: ./.github/actions/setup-ruby with: @@ -335,9 +337,9 @@ jobs: POSTGRES_USER: postgres options: >- --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 5432:5432 @@ -345,9 +347,9 @@ jobs: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" - --health-interval 10s - --health-timeout 5s - --health-retries 5 + --health-interval 10ms + --health-timeout 3s + --health-retries 50 ports: - 6379:6379 @@ -358,9 +360,9 @@ jobs: xpack.security.enabled: false options: >- --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 10s - --health-timeout 5s - --health-retries 10 + --health-interval 2s + --health-timeout 3s + --health-retries 50 ports: - 9200:9200 @@ -372,9 +374,9 @@ jobs: DISABLE_SECURITY_PLUGIN: true options: >- --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 10s - --health-timeout 5s - --health-retries 10 + --health-interval 2s + --health-timeout 3s + --health-retries 50 ports: - 9200:9200 @@ -409,7 +411,7 @@ jobs: - uses: actions/download-artifact@v4 with: - path: './public' + path: './' name: ${{ github.sha }} - name: Set up Ruby environment diff --git a/.nvmrc b/.nvmrc index c61a3d77e7..cecb936289 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20.14 +20.15 diff --git a/.rubocop.yml b/.rubocop.yml index cbc0afd281..965f56f3e7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,27 @@ -# Can be removed once all rules are addressed or moved to this file as documented overrides -inherit_from: .rubocop_todo.yml +--- +AllCops: + CacheRootDirectory: tmp + DisplayStyleGuide: true + Exclude: + - Vagrantfile + - config/initializers/json_ld* + - lib/mastodon/migration_helpers.rb + ExtraDetails: true + NewCops: enable + TargetRubyVersion: 3.1 # Oldest supported ruby version + +inherit_from: + - .rubocop/layout.yml + - .rubocop/metrics.yml + - .rubocop/naming.yml + - .rubocop/rails.yml + - .rubocop/rspec_rails.yml + - .rubocop/rspec.yml + - .rubocop/style.yml + - .rubocop/custom.yml + - .rubocop_todo.yml + - .rubocop/strict.yml -# Used for merging with exclude lists with .rubocop_todo.yml inherit_mode: merge: - Exclude @@ -12,229 +32,3 @@ require: - rubocop-rspec_rails - rubocop-performance - rubocop-capybara - - ./lib/linter/rubocop_middle_dot - -AllCops: - TargetRubyVersion: 3.1 # Set to minimum supported version of CI - DisplayCopNames: true - DisplayStyleGuide: true - ExtraDetails: true - UseCache: true - CacheRootDirectory: tmp - NewCops: enable # Opt-in to newly added rules - Exclude: - - db/schema.rb - - 'bin/*' - - 'node_modules/**/*' - - 'Vagrantfile' - - 'vendor/**/*' - - 'config/initializers/json_ld*' # Generated files - - 'lib/mastodon/migration_helpers.rb' # Vendored from GitLab - - 'lib/templates/**/*' - -# Reason: Prefer Hashes without extreme indentation -# https://docs.rubocop.org/rubocop/cops_layout.html#layoutfirsthashelementindentation -Layout/FirstHashElementIndentation: - EnforcedStyle: consistent - -# Reason: Currently disabled in .rubocop_todo.yml -# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength -Layout/LineLength: - Max: 300 # Default of 120 causes a duplicate entry in generated todo file - -## Disable most Metrics/*Length cops -# Reason: those are often triggered and force significant refactors when this happend -# but the team feel they are not really improving the code quality. - -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsblocklength -Metrics/BlockLength: - Enabled: false - -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsclasslength -Metrics/ClassLength: - Enabled: false - -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmethodlength -Metrics/MethodLength: - Enabled: false - -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsmodulelength -Metrics/ModuleLength: - Enabled: false - -## End Disable Metrics/*Length cops - -# Reason: Currently disabled in .rubocop_todo.yml -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsabcsize -Metrics/AbcSize: - Exclude: - - 'lib/mastodon/cli/*.rb' - -# Reason: Currently disabled in .rubocop_todo.yml -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricscyclomaticcomplexity -Metrics/CyclomaticComplexity: - Exclude: - - lib/mastodon/cli/*.rb - -# Reason: -# https://docs.rubocop.org/rubocop/cops_metrics.html#metricsparameterlists -Metrics/ParameterLists: - CountKeywordArgs: false - -# Reason: Prefer seeing a variable name -# https://docs.rubocop.org/rubocop/cops_naming.html#namingblockforwarding -Naming/BlockForwarding: - EnforcedStyle: explicit - -# Reason: Prevailing style is argument file paths -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsfilepath -Rails/FilePath: - EnforcedStyle: arguments - -# Reason: Prevailing style uses numeric status codes, matches RSpec/Rails/HttpStatus -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railshttpstatus -Rails/HttpStatus: - EnforcedStyle: numeric - -# Reason: Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railslexicallyscopedactionfilter -Rails/LexicallyScopedActionFilter: - Exclude: - - 'app/controllers/auth/*' - -# Reason: These tasks are doing local work which do not need full env loaded -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsrakeenvironment -Rails/RakeEnvironment: - Exclude: - - 'lib/tasks/auto_annotate_models.rake' - - 'lib/tasks/emojis.rake' - - 'lib/tasks/mastodon.rake' - - 'lib/tasks/repo.rake' - - 'lib/tasks/statistics.rake' - -# Reason: There are appropriate times to use these features -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsskipsmodelvalidations -Rails/SkipsModelValidations: - Enabled: false - -# Reason: We want to preserve the ability to migrate from arbitrary old versions, -# and cannot guarantee that every installation has run every migration as they upgrade. -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsunusedignoredcolumns -Rails/UnusedIgnoredColumns: - Enabled: false - -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop-rails/cops_rails.html#railsnegateinclude -Rails/NegateInclude: - Enabled: false - -# Reason: Enforce default limit, but allow some elements to span lines -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecexamplelength -RSpec/ExampleLength: - CountAsOne: ['array', 'heredoc', 'method_call'] - -# Reason: Deprecated cop, will be removed in 3.0, replaced by SpecFilePathFormat -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecfilepath -RSpec/FilePath: - Enabled: false - -# Reason: -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject -RSpec/NamedSubject: - EnforcedStyle: named_only - -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnottonot -RSpec/NotToNot: - EnforcedStyle: to_not - -# Reason: Match overrides from Rspec/FilePath rule above -# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecspecfilepathformat -RSpec/SpecFilePathFormat: - CustomTransform: - ActivityPub: activitypub - DeepL: deepl - FetchOEmbedService: fetch_oembed_service - OEmbedController: oembed_controller - OStatus: ostatus - -# Reason: Prevailing style uses numeric status codes, matches Rails/HttpStatus -# https://docs.rubocop.org/rubocop-rspec/cops_rspec_rails.html#rspecrailshttpstatus -RSpecRails/HttpStatus: - EnforcedStyle: numeric - -# Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#styleclassandmodulechildren -Style/ClassAndModuleChildren: - Enabled: false - -# Reason: Classes mostly self-document with their names -# https://docs.rubocop.org/rubocop/cops_style.html#styledocumentation -Style/Documentation: - Enabled: false - -# Reason: Route redirects are not token-formatted and must be skipped -# https://docs.rubocop.org/rubocop/cops_style.html#styleformatstringtoken -Style/FormatStringToken: - inherit_mode: - merge: - - AllowedMethods # The rubocop-rails config adds `redirect` - AllowedMethods: - - redirect_with_vary - -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop/cops_style.html#stylehashaslastarrayitem -Style/HashAsLastArrayItem: - Enabled: false - -# Reason: Enforce modern Ruby style -# https://docs.rubocop.org/rubocop/cops_style.html#stylehashsyntax -Style/HashSyntax: - EnforcedStyle: ruby19_no_mixed_keys - EnforcedShorthandSyntax: either - -# Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#stylenumericliterals -Style/NumericLiterals: - AllowedPatterns: - - \d{4}_\d{2}_\d{2}_\d{6} # For DB migration date version number readability - -# Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#stylepercentliteraldelimiters -Style/PercentLiteralDelimiters: - PreferredDelimiters: - '%i': '()' - '%w': '()' - -# Reason: Prefer less indentation in conditional assignments -# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantbegin -Style/RedundantBegin: - Enabled: false - -# Reason: Prevailing style choice -# https://docs.rubocop.org/rubocop/cops_style.html#styleredundantfetchblock -Style/RedundantFetchBlock: - Enabled: false - -# Reason: Overridden to reduce implicit StandardError rescues -# https://docs.rubocop.org/rubocop/cops_style.html#stylerescuestandarderror -Style/RescueStandardError: - EnforcedStyle: implicit - -# Reason: Originally disabled for CodeClimate, and no config consensus has been found -# https://docs.rubocop.org/rubocop/cops_style.html#stylesymbolarray -Style/SymbolArray: - Enabled: false - -# Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainarrayliteral -Style/TrailingCommaInArrayLiteral: - EnforcedStyleForMultiline: 'comma' - -# Reason: -# https://docs.rubocop.org/rubocop/cops_style.html#styletrailingcommainhashliteral -Style/TrailingCommaInHashLiteral: - EnforcedStyleForMultiline: 'comma' - -Style/MiddleDot: - Enabled: true diff --git a/.rubocop/custom.yml b/.rubocop/custom.yml new file mode 100644 index 0000000000..63035837f8 --- /dev/null +++ b/.rubocop/custom.yml @@ -0,0 +1,6 @@ +--- +require: + - ../lib/linter/rubocop_middle_dot + +Style/MiddleDot: + Enabled: true diff --git a/.rubocop/layout.yml b/.rubocop/layout.yml new file mode 100644 index 0000000000..487879ca2c --- /dev/null +++ b/.rubocop/layout.yml @@ -0,0 +1,6 @@ +--- +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Layout/LineLength: + Max: 300 # Default of 120 causes a duplicate entry in generated todo file diff --git a/.rubocop/metrics.yml b/.rubocop/metrics.yml new file mode 100644 index 0000000000..89532af42a --- /dev/null +++ b/.rubocop/metrics.yml @@ -0,0 +1,23 @@ +--- +Metrics/AbcSize: + Exclude: + - lib/mastodon/cli/*.rb + +Metrics/BlockLength: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Exclude: + - lib/mastodon/cli/*.rb + +Metrics/MethodLength: + Enabled: false + +Metrics/ModuleLength: + Enabled: false + +Metrics/ParameterLists: + CountKeywordArgs: false diff --git a/.rubocop/naming.yml b/.rubocop/naming.yml new file mode 100644 index 0000000000..da6ad4ac57 --- /dev/null +++ b/.rubocop/naming.yml @@ -0,0 +1,3 @@ +--- +Naming/BlockForwarding: + EnforcedStyle: explicit diff --git a/.rubocop/rails.yml b/.rubocop/rails.yml new file mode 100644 index 0000000000..ae31c1f266 --- /dev/null +++ b/.rubocop/rails.yml @@ -0,0 +1,23 @@ +--- +Rails/BulkChangeTable: + Enabled: false # Conflicts with strong_migrations features + +Rails/FilePath: + EnforcedStyle: arguments + +Rails/HttpStatus: + EnforcedStyle: numeric + +Rails/NegateInclude: + Enabled: false + +Rails/RakeEnvironment: + Exclude: # Tasks are doing local work which do not need full env loaded + - lib/tasks/auto_annotate_models.rake + - lib/tasks/emojis.rake + - lib/tasks/mastodon.rake + - lib/tasks/repo.rake + - lib/tasks/statistics.rake + +Rails/SkipsModelValidations: + Enabled: false diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml new file mode 100644 index 0000000000..d2d2f8325d --- /dev/null +++ b/.rubocop/rspec.yml @@ -0,0 +1,27 @@ +--- +RSpec/ExampleLength: + CountAsOne: ['array', 'heredoc', 'method_call'] + Max: 20 # Override default of 5 + +RSpec/MultipleExpectations: + Max: 10 # Overrides default of 1 + +RSpec/MultipleMemoizedHelpers: + Max: 20 # Overrides default of 5 + +RSpec/NamedSubject: + EnforcedStyle: named_only + +RSpec/NestedGroups: + Max: 10 # Overrides default of 3 + +RSpec/NotToNot: + EnforcedStyle: to_not + +RSpec/SpecFilePathFormat: + CustomTransform: + ActivityPub: activitypub + DeepL: deepl + FetchOEmbedService: fetch_oembed_service + OEmbedController: oembed_controller + OStatus: ostatus diff --git a/.rubocop/rspec_rails.yml b/.rubocop/rspec_rails.yml new file mode 100644 index 0000000000..993a5689ad --- /dev/null +++ b/.rubocop/rspec_rails.yml @@ -0,0 +1,3 @@ +--- +RSpecRails/HttpStatus: + EnforcedStyle: numeric diff --git a/.rubocop/strict.yml b/.rubocop/strict.yml new file mode 100644 index 0000000000..2222c6d8b9 --- /dev/null +++ b/.rubocop/strict.yml @@ -0,0 +1,19 @@ +Lint/Debugger: # Remove any `binding.pry` + Enabled: true + Exclude: [] + +RSpec/Focus: # Require full spec run on CI + Enabled: true + Exclude: [] + +Rails/Output: # Remove any `puts` debugging + Enabled: true + Exclude: [] + +Rails/FindEach: # Using `each` could impact performance, use `find_each` + Enabled: true + Exclude: [] + +Rails/UniqBeforePluck: # Require `uniq.pluck` and not `pluck.uniq` + Enabled: true + Exclude: [] diff --git a/.rubocop/style.yml b/.rubocop/style.yml new file mode 100644 index 0000000000..03e35a70ac --- /dev/null +++ b/.rubocop/style.yml @@ -0,0 +1,47 @@ +--- +Style/ClassAndModuleChildren: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/FormatStringToken: + AllowedMethods: + - redirect_with_vary # Route redirects are not token-formatted + inherit_mode: + merge: + - AllowedMethods + +Style/HashAsLastArrayItem: + Enabled: false + +Style/HashSyntax: + EnforcedShorthandSyntax: either + EnforcedStyle: ruby19_no_mixed_keys + +Style/NumericLiterals: + AllowedPatterns: + - \d{4}_\d{2}_\d{2}_\d{6} + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%i': () + '%w': () + +Style/RedundantBegin: + Enabled: false + +Style/RedundantFetchBlock: + Enabled: false + +Style/RescueStandardError: + EnforcedStyle: implicit + +Style/SymbolArray: + Enabled: false + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9bc6b8c258..4a86051c42 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by -# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` -# using RuboCop version 1.63.5. +# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` +# using RuboCop version 1.64.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -27,33 +27,10 @@ Metrics/CyclomaticComplexity: Metrics/PerceivedComplexity: Max: 27 -# Configuration parameters: CountAsOne. -RSpec/ExampleLength: - Max: 18 - -RSpec/MultipleExpectations: - Max: 7 - -# Configuration parameters: AllowSubject. -RSpec/MultipleMemoizedHelpers: - Max: 17 - -# Configuration parameters: AllowedGroups. -RSpec/NestedGroups: - Max: 6 - Rails/OutputSafety: Exclude: - 'config/initializers/simple_form.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowedMethods, AllowedPatterns. -# AllowedMethods: ==, equal?, eql? -Style/ClassEqualityComparison: - Exclude: - - 'app/helpers/jsonld_helper.rb' - - 'app/serializers/activitypub/outbox_serializer.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowedVars. Style/FetchEnvVar: @@ -70,7 +47,6 @@ Style/FetchEnvVar: - 'config/initializers/vapid.rb' - 'lib/mastodon/redis_config.rb' - 'lib/tasks/repo.rake' - - 'spec/system/profile_spec.rb' # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. @@ -81,40 +57,10 @@ Style/FormatStringToken: - 'config/initializers/devise.rb' - 'lib/paperclip/color_extractor.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/GlobalStdStream: - Exclude: - - 'config/environments/development.rb' - - 'config/environments/production.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. Style/GuardClause: - Exclude: - - 'app/lib/activitypub/activity/block.rb' - - 'app/lib/request.rb' - - 'app/lib/request_pool.rb' - - 'app/lib/webfinger.rb' - - 'app/lib/webfinger_resource.rb' - - 'app/models/concerns/account/counters.rb' - - 'app/models/concerns/user/ldap_authenticable.rb' - - 'app/models/tag.rb' - - 'app/models/user.rb' - - 'app/services/fan_out_on_write_service.rb' - - 'app/services/post_status_service.rb' - - 'app/services/process_hashtags_service.rb' - - 'app/workers/move_worker.rb' - - 'app/workers/redownload_avatar_worker.rb' - - 'app/workers/redownload_header_worker.rb' - - 'app/workers/redownload_media_worker.rb' - - 'app/workers/remote_account_refresh_worker.rb' - - 'config/initializers/devise.rb' - - 'lib/devise/strategies/two_factor_ldap_authenticatable.rb' - - 'lib/devise/strategies/two_factor_pam_authenticatable.rb' - - 'lib/mastodon/cli/accounts.rb' - - 'lib/mastodon/cli/maintenance.rb' - - 'lib/mastodon/cli/media.rb' - - 'lib/tasks/repo.rake' + Enabled: false # This cop supports unsafe autocorrection (--autocorrect-all). Style/HashTransformValues: @@ -136,16 +82,10 @@ Style/MutableConstant: - 'app/services/delete_account_service.rb' - 'lib/mastodon/migration_warning.rb' -# This cop supports safe autocorrection (--autocorrect). -Style/NilLambda: - Exclude: - - 'config/initializers/paperclip.rb' - # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: Exclude: - - 'app/helpers/admin/account_moderation_notes_helper.rb' - 'app/helpers/jsonld_helper.rb' - 'app/lib/admin/system_check/message.rb' - 'app/lib/request.rb' @@ -169,13 +109,6 @@ Style/RedundantConstantBase: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: - Exclude: - - 'app/models/concerns/account/finder_concern.rb' - # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: WordRegex. # SupportedStyles: percent, brackets diff --git a/.ruby-version b/.ruby-version index 4772543317..619b537668 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.2 +3.3.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b24d6f15..7c3d96ba4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to this project will be documented in this file. +## [4.2.10] - 2024-07-04 + +### Security + +- Fix incorrect permission checking on multiple API endpoints ([GHSA-58x8-3qxw-6hm7](https://github.com/mastodon/mastodon/security/advisories/GHSA-58x8-3qxw-6hm7)) +- Fix incorrect authorship checking when processing some activities (CVE-2024-37903, [GHSA-xjvf-fm67-4qc3](https://github.com/mastodon/mastodon/security/advisories/GHSA-xjvf-fm67-4qc3)) +- Fix ongoing streaming sessions not being invalidated when application tokens get revoked ([GHSA-vp5r-5pgw-jwqx](https://github.com/mastodon/mastodon/security/advisories/GHSA-vp5r-5pgw-jwqx)) +- Update dependencies + +### Added + +- Add yarn version specification to avoid confusion with Yarn 3 and Yarn 4 + +### Changed + +- Change preview cards generation to skip unusually long URLs ([oneiros](https://github.com/mastodon/mastodon/pull/30854)) +- Change search modifiers to be case-insensitive ([Gargron](https://github.com/mastodon/mastodon/pull/30865)) +- Change `STATSD_ADDR` handling to emit a warning rather than crashing if the address is unreachable ([timothyjrogers](https://github.com/mastodon/mastodon/pull/30691)) +- Change PWA start URL from `/home` to `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27377)) + +### Removed + +- Removed dependency on `posix-spawn` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18559)) + +### Fixed + +- Fix scheduled statuses scheduled in less than 5 minutes being immediately published ([danielmbrasil](https://github.com/mastodon/mastodon/pull/30584)) +- Fix encoding detection for link cards ([oneiros](https://github.com/mastodon/mastodon/pull/30780)) +- Fix `/admin/accounts/:account_id/statuses/:id` for edited posts with media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30819)) +- Fix duplicate `@context` attribute in user archive export ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/30653)) + ## [4.2.9] - 2024-05-30 ### Security diff --git a/Dockerfile b/Dockerfile index cb5b872059..7f7eca06da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.7 +# syntax=docker/dockerfile:1.8 # This file is designed for production server deployment, not local development work # For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker @@ -12,22 +12,22 @@ ARG BUILDPLATFORM=${BUILDPLATFORM} # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.3.2" +ARG RUBY_VERSION="3.3.3" # # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="20" # Debian image to use for base image, change with [--build-arg DEBIAN_VERSION="bookworm"] ARG DEBIAN_VERSION="bookworm" # Node image to use for base image based on combined variables (ex: 20-bookworm-slim) -FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim as node +FROM docker.io/node:${NODE_MAJOR_VERSION}-${DEBIAN_VERSION}-slim AS node # Ruby image to use for base image based on combined variables (ex: 3.3.x-slim-bookworm) -FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby +FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} AS ruby # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA -# Example: v4.2.0-nightly.2023.11.09+something -# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] +# Example: v4.3.0-nightly.2023.11.09+pr-123456 +# Overwrite existence of 'alpha.X' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] ARG MASTODON_VERSION_PRERELEASE="" -# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-12345"] +# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-123456"] ARG MASTODON_VERSION_METADATA="" # Allow Ruby on Rails to serve static files @@ -48,8 +48,6 @@ ENV \ # Apply Mastodon version information MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \ MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}" \ -# Enable libvips - MASTODON_USE_LIBVIPS=true \ # Apply Mastodon static files and YJIT options RAILS_SERVE_STATIC_FILES=${RAILS_SERVE_STATIC_FILES} \ RUBY_YJIT_ENABLE=${RUBY_YJIT_ENABLE} \ @@ -67,7 +65,9 @@ ENV \ DEBIAN_FRONTEND="noninteractive" \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ # Optimize jemalloc 5.x performance - MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" + MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ +# Enable libvips, should not be changed + MASTODON_USE_LIBVIPS=true # Set default shell used for running commands SHELL ["/bin/bash", "-o", "pipefail", "-o", "errexit", "-c"] @@ -100,11 +100,8 @@ RUN \ apt-get dist-upgrade -yq; \ # Install jemalloc, curl and other necessary components apt-get install -y --no-install-recommends \ - ca-certificates \ curl \ - ffmpeg \ file \ - libvips42 \ libjemalloc2 \ patchelf \ procps \ @@ -120,7 +117,7 @@ RUN \ ; # Create temporary build layer from base image -FROM ruby as build +FROM ruby AS build # Copy Node package configuration files into working directory COPY package.json yarn.lock .yarnrc.yml /opt/mastodon/ @@ -138,18 +135,47 @@ RUN \ --mount=type=cache,id=apt-lib-${TARGETPLATFORM},target=/var/lib/apt,sharing=locked \ # Install build tools and bundler dependencies from APT apt-get install -y --no-install-recommends \ - g++ \ - gcc \ + autoconf \ + automake \ + build-essential \ + cmake \ git \ libgdbm-dev \ + libglib2.0-dev \ libgmp-dev \ libicu-dev \ libidn-dev \ libpq-dev \ libssl-dev \ - make \ + libtool \ + meson \ + nasm \ + pkg-config \ shared-mime-info \ - zlib1g-dev \ + xz-utils \ + # libvips components + libcgif-dev \ + libexif-dev \ + libexpat1-dev \ + libgirepository1.0-dev \ + libheif-dev \ + libimagequant-dev \ + libjpeg62-turbo-dev \ + liblcms2-dev \ + liborc-dev \ + libspng-dev \ + libtiff-dev \ + libwebp-dev \ + # ffmpeg components + libdav1d-dev \ + liblzma-dev \ + libmp3lame-dev \ + libopus-dev \ + libsnappy-dev \ + libvorbis-dev \ + libvpx-dev \ + libx264-dev \ + libx265-dev \ ; RUN \ @@ -158,8 +184,70 @@ RUN \ corepack enable; \ corepack prepare --activate; +# Create temporary libvips specific build layer from build layer +FROM build AS libvips + +# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] +# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips +ARG VIPS_VERSION=8.15.2 +# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] +ARG VIPS_URL=https://github.com/libvips/libvips/releases/download + +WORKDIR /usr/local/libvips/src + +RUN \ + curl -sSL -o vips-${VIPS_VERSION}.tar.xz ${VIPS_URL}/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.xz; \ + tar xf vips-${VIPS_VERSION}.tar.xz; \ + cd vips-${VIPS_VERSION}; \ + meson setup build --prefix /usr/local/libvips --libdir=lib -Ddeprecated=false -Dintrospection=disabled -Dmodules=disabled -Dexamples=false; \ + cd build; \ + ninja; \ + ninja install; + +# Create temporary ffmpeg specific build layer from build layer +FROM build AS ffmpeg + +# ffmpeg version to compile, change with [--build-arg FFMPEG_VERSION="7.0.x"] +# renovate: datasource=repology depName=ffmpeg packageName=openpkg_current/ffmpeg +ARG FFMPEG_VERSION=7.0.1 +# ffmpeg download URL, change with [--build-arg FFMPEG_URL="https://ffmpeg.org/releases"] +ARG FFMPEG_URL=https://ffmpeg.org/releases + +WORKDIR /usr/local/ffmpeg/src + +RUN \ + curl -sSL -o ffmpeg-${FFMPEG_VERSION}.tar.xz ${FFMPEG_URL}/ffmpeg-${FFMPEG_VERSION}.tar.xz; \ + tar xf ffmpeg-${FFMPEG_VERSION}.tar.xz; \ + cd ffmpeg-${FFMPEG_VERSION}; \ + ./configure \ + --prefix=/usr/local/ffmpeg \ + --toolchain=hardened \ + --disable-debug \ + --disable-devices \ + --disable-doc \ + --disable-ffplay \ + --disable-network \ + --disable-static \ + --enable-ffmpeg \ + --enable-ffprobe \ + --enable-gpl \ + --enable-libdav1d \ + --enable-libmp3lame \ + --enable-libopus \ + --enable-libsnappy \ + --enable-libvorbis \ + --enable-libvpx \ + --enable-libwebp \ + --enable-libx264 \ + --enable-libx265 \ + --enable-shared \ + --enable-version3 \ + ; \ + make -j$(nproc); \ + make install; + # Create temporary bundler specific build layer from build layer -FROM build as bundler +FROM build AS bundler ARG TARGETPLATFORM @@ -181,7 +269,7 @@ RUN \ bundle install -j"$(nproc)"; # Create temporary node specific build layer from build layer -FROM build as yarn +FROM build AS yarn ARG TARGETPLATFORM @@ -198,7 +286,7 @@ RUN \ yarn workspaces focus --production @mastodon/mastodon; # Create temporary assets build layer from build layer -FROM build as precompiler +FROM build AS precompiler # Copy Mastodon sources into precompiler layer COPY . /opt/mastodon/ @@ -207,22 +295,22 @@ COPY . /opt/mastodon/ COPY --from=yarn /opt/mastodon /opt/mastodon/ COPY --from=bundler /opt/mastodon /opt/mastodon/ COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ +# Copy libvips components to layer for precompiler +COPY --from=libvips /usr/local/libvips/bin /usr/local/bin +COPY --from=libvips /usr/local/libvips/lib /usr/local/lib ARG TARGETPLATFORM RUN \ + ldconfig; \ # Use Ruby on Rails to create Mastodon assets - ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \ - ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \ - ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \ - OTP_SECRET=precompile_placeholder \ - SECRET_KEY_BASE=precompile_placeholder \ + SECRET_KEY_BASE_DUMMY=1 \ bundle exec rails assets:precompile; \ # Cleanup temporary files rm -fr /opt/mastodon/tmp; # Prep final Mastodon Ruby layer -FROM ruby as mastodon +FROM ruby AS mastodon ARG TARGETPLATFORM @@ -236,12 +324,41 @@ RUN \ --mount=type=cache,id=yarn-cache-${TARGETPLATFORM},target=/usr/local/share/.cache/yarn,sharing=locked \ # Apt update install non-dev versions of necessary components apt-get install -y --no-install-recommends \ - libssl3 \ - libpq5 \ + libexpat1 \ + libglib2.0-0 \ libicu72 \ libidn12 \ + libpq5 \ libreadline8 \ + libssl3 \ libyaml-0-2 \ + # libvips components + libcgif0 \ + libexif12 \ + libheif1 \ + libimagequant0 \ + libjpeg62-turbo \ + liblcms2-2 \ + liborc-0.4-0 \ + libspng0 \ + libtiff6 \ + libwebp7 \ + libwebpdemux2 \ + libwebpmux3 \ + # ffmpeg components + libdav1d6 \ + libmp3lame0 \ + libopencore-amrnb0 \ + libopencore-amrwb0 \ + libopus0 \ + libsnappy1v5 \ + libtheora0 \ + libvorbis0a \ + libvorbisenc2 \ + libvorbisfile3 \ + libvpx7 \ + libx264-164 \ + libx265-199 \ ; # Copy Mastodon sources into final layer @@ -252,9 +369,22 @@ COPY --from=precompiler /opt/mastodon/public/packs /opt/mastodon/public/packs COPY --from=precompiler /opt/mastodon/public/assets /opt/mastodon/public/assets # Copy bundler components to layer COPY --from=bundler /usr/local/bundle/ /usr/local/bundle/ +# Copy libvips components to layer +COPY --from=libvips /usr/local/libvips/bin /usr/local/bin +COPY --from=libvips /usr/local/libvips/lib /usr/local/lib +# Copy ffpmeg components to layer +COPY --from=ffmpeg /usr/local/ffmpeg/bin /usr/local/bin +COPY --from=ffmpeg /usr/local/ffmpeg/lib /usr/local/lib RUN \ -# Precompile bootsnap code for faster Rails startup + ldconfig; \ +# Smoketest media processors + vips -v; \ + ffmpeg -version; \ + ffprobe -version; + +RUN \ + # Precompile bootsnap code for faster Rails startup bundle exec bootsnap precompile --gemfile app/ lib/; RUN \ diff --git a/Gemfile b/Gemfile index be02a65626..ef52d50cac 100644 --- a/Gemfile +++ b/Gemfile @@ -9,9 +9,6 @@ gem 'rack', '~> 2.2.7' gem 'rails', '~> 7.1.1' gem 'thor', '~> 1.2' -# For why irb is in the Gemfile, see: https://ruby.social/@st0012/111444685161478182 -gem 'irb', '~> 1.8' - gem 'dotenv' gem 'haml-rails', '~>2.0' gem 'pg', '~> 1.5' @@ -28,7 +25,7 @@ gem 'ruby-vips', '~> 2.2', require: false gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' gem 'bootsnap', '~> 1.18.0', require: false -gem 'browser' +gem 'browser', '< 6' # https://github.com/fnando/browser/issues/543 gem 'charlock_holmes', '~> 0.7.7' gem 'chewy', '~> 7.3' gem 'devise', '~> 4.9' @@ -61,6 +58,7 @@ gem 'httplog', '~> 1.7.0' gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' +gem 'irb', '~> 1.8' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' @@ -71,7 +69,7 @@ gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' gem 'premailer-rails' -gem 'public_suffix', '~> 5.0' +gem 'public_suffix', '~> 6.0' gem 'pundit', '~> 2.3' gem 'rack-attack', '~> 6.6' gem 'rack-cors', '~> 2.0', require: 'rack/cors' @@ -102,12 +100,10 @@ gem 'json-ld' gem 'json-ld-preloaded', '~> 3.2' gem 'rdf-normalize', '~> 0.5' -gem 'private_address_check', '~> 0.5' - gem 'opentelemetry-api', '~> 1.2.5' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.27.0', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false gem 'opentelemetry-instrumentation-active_job', '~> 0.7.1', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.20.1', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.21.2', require: false @@ -118,13 +114,16 @@ group :opentelemetry do gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.27.1', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false - gem 'opentelemetry-instrumentation-rails', '~> 0.30.0', require: false + gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false end group :test do + # Enable usage of all available CPUs/cores during spec runs + gem 'flatware-rspec' + # Adds RSpec Error/Warning annotations to GitHub PRs on the Files tab gem 'rspec-github', '~> 2.4', require: false @@ -171,6 +170,7 @@ group :development do gem 'rubocop-performance', require: false gem 'rubocop-rails', require: false gem 'rubocop-rspec', require: false + gem 'rubocop-rspec_rails', require: false # Annotates modules with schema gem 'annotate', '~> 3.2' diff --git a/Gemfile.lock b/Gemfile.lock index 4d16fea47c..eb6720e454 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -89,8 +89,8 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) annotate (3.2.0) @@ -100,19 +100,19 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.940.0) - aws-sdk-core (3.197.0) + aws-partitions (1.950.0) + aws-sdk-core (3.201.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.83.0) - aws-sdk-core (~> 3, >= 3.197.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.152.0) - aws-sdk-core (~> 3, >= 3.197.0) + aws-sdk-kms (1.88.0) + aws-sdk-core (~> 3, >= 3.201.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.156.0) + aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) + aws-sigv4 (~> 1.5) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) azure-storage-blob (2.0.3) @@ -143,7 +143,7 @@ GEM brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) redis (>= 1.0, < 6) - builder (3.2.4) + builder (3.3.0) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) thor (~> 1.0) @@ -159,7 +159,7 @@ GEM case_transform (0.2) activesupport cbor (0.5.9.8) - charlock_holmes (0.7.7) + charlock_holmes (0.7.8) chewy (7.6.0) activesupport (>= 5.2) elasticsearch (>= 7.14.0, < 8) @@ -195,7 +195,7 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (5.0.0) + devise-two-factor (5.1.0) activesupport (~> 7.0) devise (~> 4.0) railties (~> 7.0) @@ -208,7 +208,7 @@ GEM activerecord (>= 4.2, < 8) docile (1.4.0) domain_name (0.6.20240107) - doorkeeper (5.6.9) + doorkeeper (5.7.1) railties (>= 5) dotenv (3.1.2) drb (2.2.1) @@ -226,7 +226,7 @@ GEM htmlentities (~> 4.3.3) launchy (~> 2.1) mail (~> 2.7) - erubi (1.12.0) + erubi (1.13.0) et-orbi (1.2.11) tzinfo excon (0.110.0) @@ -264,6 +264,11 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake + flatware (2.3.2) + thor (< 2.0) + flatware-rspec (2.3.2) + flatware (= 2.3.2) + rspec (>= 3.6) fog-core (2.4.0) builder excon (~> 0.71) @@ -272,7 +277,7 @@ GEM fog-json (1.2.0) fog-core multi_json (~> 1.10) - fog-openstack (1.1.1) + fog-openstack (1.1.3) fog-core (~> 2.1) fog-json (>= 1.0) formatador (1.1.0) @@ -341,7 +346,7 @@ GEM activesupport (>= 3.0) nokogiri (>= 1.6) io-console (0.7.2) - irb (1.13.1) + irb (1.13.2) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) @@ -398,6 +403,7 @@ GEM llhttp-ffi (0.5.0) ffi-compiler (~> 1.0) rake (~> 13.0) + logger (1.6.0) lograge (0.14.0) actionpack (>= 4) activesupport (>= 4) @@ -419,13 +425,13 @@ GEM addressable (~> 2.5) azure-storage-blob (~> 2.0.1) hashie (~> 5.0) - memory_profiler (1.0.1) + memory_profiler (1.0.2) mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2024.0507) + mime-types-data (3.2024.0604) mini_mime (1.1.5) mini_portile2 (2.8.7) - minitest (5.23.1) + minitest (5.24.1) msgpack (1.7.2) multi_json (1.15.0) multipart-post (2.4.0) @@ -445,7 +451,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.3) - nokogiri (1.16.5) + nokogiri (1.16.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) nsa (0.3.0) @@ -489,8 +495,8 @@ GEM opentelemetry-api (1.2.5) opentelemetry-common (0.20.1) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.27.0) - google-protobuf (~> 3.14) + opentelemetry-exporter-otlp (0.28.0) + google-protobuf (>= 3.18) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) opentelemetry-common (~> 0.20) @@ -498,6 +504,10 @@ GEM opentelemetry-semantic_conventions opentelemetry-helpers-sql-obfuscation (0.1.0) opentelemetry-common (~> 0.20) + opentelemetry-instrumentation-action_mailer (0.1.0) + opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-active_support (~> 0.1) + opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-action_pack (0.9.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) @@ -506,7 +516,7 @@ GEM opentelemetry-api (~> 1.0) opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_job (0.7.1) + opentelemetry-instrumentation-active_job (0.7.2) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-active_model_serializers (0.20.1) @@ -515,7 +525,7 @@ GEM opentelemetry-instrumentation-active_record (0.7.2) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-active_support (0.5.1) + opentelemetry-instrumentation-active_support (0.6.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (0.22.3) @@ -524,48 +534,42 @@ GEM opentelemetry-instrumentation-concurrent_ruby (0.21.3) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-excon (0.22.1) + opentelemetry-instrumentation-excon (0.22.3) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-faraday (0.24.2) + opentelemetry-instrumentation-faraday (0.24.5) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-http (0.23.3) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-http_client (0.22.4) + opentelemetry-instrumentation-http_client (0.22.6) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-net_http (0.22.4) + opentelemetry-instrumentation-net_http (0.22.6) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-pg (0.27.3) opentelemetry-api (~> 1.0) opentelemetry-helpers-sql-obfuscation opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rack (0.24.3) + opentelemetry-instrumentation-rack (0.24.5) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-rails (0.30.1) + opentelemetry-instrumentation-rails (0.31.0) opentelemetry-api (~> 1.0) + opentelemetry-instrumentation-action_mailer (~> 0.1.0) opentelemetry-instrumentation-action_pack (~> 0.9.0) opentelemetry-instrumentation-action_view (~> 0.7.0) opentelemetry-instrumentation-active_job (~> 0.7.0) opentelemetry-instrumentation-active_record (~> 0.7.0) - opentelemetry-instrumentation-active_support (~> 0.5.0) + opentelemetry-instrumentation-active_support (~> 0.6.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-redis (0.25.4) + opentelemetry-instrumentation-redis (0.25.6) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) - opentelemetry-instrumentation-sidekiq (0.25.3) + opentelemetry-instrumentation-sidekiq (0.25.6) opentelemetry-api (~> 1.0) - opentelemetry-common (~> 0.20.0) opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-registry (0.3.1) opentelemetry-api (~> 1.1) @@ -579,7 +583,7 @@ GEM orm_adapter (0.5.0) ox (2.14.18) parallel (1.25.1) - parser (3.3.2.0) + parser (3.3.3.0) ast (~> 2.4.1) racc parslet (2.0.0) @@ -596,7 +600,6 @@ GEM actionmailer (>= 3) net-smtp premailer (~> 1.7, >= 1.7.9) - private_address_check (0.5.0) propshaft (0.9.0) actionpack (>= 7.0.0) activesupport (>= 7.0.0) @@ -604,7 +607,7 @@ GEM railties (>= 7.0.0) psych (5.1.2) stringio - public_suffix (5.0.5) + public_suffix (6.0.0) puma (6.4.2) nio4r (~> 2.0) pundit (2.3.2) @@ -677,7 +680,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.7.0) rdf (~> 3.3) - rdoc (6.6.3.1) + rdoc (6.7.0) psych (>= 4.0.0) redcarpet (3.6.0) redis (4.8.1) @@ -686,15 +689,15 @@ GEM redlock (1.3.2) redis (>= 3.0.0, < 6.0) regexp_parser (2.9.2) - reline (0.5.8) + reline (0.5.9) io-console (~> 0.5) request_store (1.6.0) rack (>= 1.4) responders (3.1.1) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.1) + strscan rotp (6.3.0) rouge (4.2.1) rpam2 (4.0.2) @@ -702,9 +705,13 @@ GEM chunky_png (~> 1.0) rqrcode_core (~> 1.0) rqrcode_core (1.2.0) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-github (2.4.0) @@ -712,7 +719,7 @@ GEM rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (6.1.2) + rspec-rails (6.1.3) actionpack (>= 6.1) activesupport (>= 6.1) railties (>= 6.1) @@ -741,23 +748,19 @@ GEM parser (>= 3.3.1.0) rubocop-capybara (2.21.0) rubocop (~> 1.41) - rubocop-factory_bot (2.25.1) - rubocop (~> 1.41) - rubocop-performance (1.21.0) + rubocop-performance (1.21.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rails (2.25.0) + rubocop-rails (2.25.1) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.31.0) - rubocop (~> 1.40) - rubocop-capybara (~> 2.17) - rubocop-factory_bot (~> 2.22) - rubocop-rspec_rails (~> 2.28) - rubocop-rspec_rails (2.28.3) - rubocop (~> 1.40) + rubocop-rspec (3.0.2) + rubocop (~> 1.61) + rubocop-rspec_rails (2.30.0) + rubocop (~> 1.61) + rubocop-rspec (~> 3, >= 3.0.1) ruby-prof (1.7.0) ruby-progressbar (1.13.0) ruby-saml (1.16.0) @@ -771,14 +774,15 @@ GEM fugit (~> 1.1, >= 1.1.6) safety_net_attestation (0.4.0) jwt (~> 2.0) - sanitize (6.1.0) + sanitize (6.1.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) scenic (1.8.0) activerecord (>= 4.0.0) railties (>= 4.0.0) - selenium-webdriver (4.21.1) + selenium-webdriver (4.22.0) base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -815,7 +819,7 @@ GEM statsd-ruby (1.5.0) stoplight (4.1.0) redlock (~> 1.0) - stringio (3.1.0) + stringio (3.1.1) strong_migrations (1.8.0) activerecord (>= 5.2) strscan (3.1.0) @@ -829,7 +833,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (1.0.1) climate_control - test-prof (1.3.3) + test-prof (1.3.3.1) thor (1.3.1) tilt (2.3.0) timeout (0.4.1) @@ -897,7 +901,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.15) + zeitwerk (2.6.16) PLATFORMS ruby @@ -912,7 +916,7 @@ DEPENDENCIES blurhash (~> 0.1) bootsnap (~> 1.18.0) brakeman (~> 6.0) - browser + browser (< 6) bundler-audit (~> 0.9) capybara (~> 3.39) charlock_holmes (~> 0.7.7) @@ -937,6 +941,7 @@ DEPENDENCIES faker (~> 3.2) fast_blank (~> 1.0) fastimage + flatware-rspec fog-core (<= 2.4.0) fog-openstack (~> 1.0) fuubar (~> 2.5) @@ -978,7 +983,7 @@ DEPENDENCIES omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) opentelemetry-api (~> 1.2.5) - opentelemetry-exporter-otlp (~> 0.27.0) + opentelemetry-exporter-otlp (~> 0.28.0) opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) @@ -989,7 +994,7 @@ DEPENDENCIES opentelemetry-instrumentation-net_http (~> 0.22.4) opentelemetry-instrumentation-pg (~> 0.27.1) opentelemetry-instrumentation-rack (~> 0.24.1) - opentelemetry-instrumentation-rails (~> 0.30.0) + opentelemetry-instrumentation-rails (~> 0.31.0) opentelemetry-instrumentation-redis (~> 0.25.3) opentelemetry-instrumentation-sidekiq (~> 0.25.2) opentelemetry-sdk (~> 1.4) @@ -998,9 +1003,8 @@ DEPENDENCIES pg (~> 1.5) pghero premailer-rails - private_address_check (~> 0.5) propshaft - public_suffix (~> 5.0) + public_suffix (~> 6.0) puma (~> 6.3) pundit (~> 2.3) rack (~> 2.2.7) @@ -1023,6 +1027,7 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + rubocop-rspec_rails ruby-prof ruby-progressbar (~> 1.13) ruby-vips (~> 2.2) diff --git a/README.md b/README.md index efe51707e4..6f61b1f9cf 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre ### Tech stack - **Ruby on Rails** powers the REST API and other web pages -- **React.js** and Redux are used for the dynamic parts of the interface +- **React.js** and **Redux** are used for the dynamic parts of the interface - **Node.js** powers the streaming API ### Requirements diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 325b33df80..16a8cb9eea 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -4,6 +4,18 @@ module Admin class DomainBlocksController < BaseController before_action :set_domain_block, only: [:destroy, :edit, :update] + PERMITTED_PARAMS = %i( + domain + obfuscate + private_comment + public_comment + reject_media + reject_reports + severity + ).freeze + + PERMITTED_UPDATE_PARAMS = PERMITTED_PARAMS.without(:domain).freeze + def batch authorize :domain_block, :create? @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) @@ -88,11 +100,17 @@ module Admin end def update_params - params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) + params + .require(:domain_block) + .slice(*PERMITTED_UPDATE_PARAMS) + .permit(*PERMITTED_UPDATE_PARAMS) end def resource_params - params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) + params + .require(:domain_block) + .slice(*PERMITTED_PARAMS) + .permit(*PERMITTED_PARAMS) end def form_domain_block_batch_params diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index 67d987d0e3..283383acb4 100644 --- a/app/controllers/api/v1/admin/tags_controller.rb +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -13,6 +13,13 @@ class Api::V1::Admin::TagsController < Api::BaseController LIMIT = 100 + PERMITTED_PARAMS = %i( + display_name + listable + trendable + usable + ).freeze + def index authorize :tag, :index? render json: @tags, each_serializer: REST::Admin::TagSerializer @@ -40,7 +47,9 @@ class Api::V1::Admin::TagsController < Api::BaseController end def tag_params - params.permit(:display_name, :trendable, :usable, :listable) + params + .slice(*PERMITTED_PARAMS) + .permit(*PERMITTED_PARAMS) end def next_path diff --git a/app/controllers/api/v1/scheduled_statuses_controller.rb b/app/controllers/api/v1/scheduled_statuses_controller.rb index 45ee586518..c62305d711 100644 --- a/app/controllers/api/v1/scheduled_statuses_controller.rb +++ b/app/controllers/api/v1/scheduled_statuses_controller.rb @@ -6,6 +6,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, except: [:update, :destroy] before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:update, :destroy] + before_action :require_user! before_action :set_statuses, only: :index before_action :set_status, except: :index diff --git a/app/controllers/api/v1/statuses/translations_controller.rb b/app/controllers/api/v1/statuses/translations_controller.rb index 7d406b0a36..8cf495f78a 100644 --- a/app/controllers/api/v1/statuses/translations_controller.rb +++ b/app/controllers/api/v1/statuses/translations_controller.rb @@ -2,6 +2,7 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseController before_action -> { doorkeeper_authorize! :read, :'read:statuses' } + before_action :require_user! before_action :set_translation rescue_from TranslationService::NotConfiguredError, with: :not_found diff --git a/app/controllers/api/v1/timelines/base_controller.rb b/app/controllers/api/v1/timelines/base_controller.rb index e79eba79ee..1dba4a5bb2 100644 --- a/app/controllers/api/v1/timelines/base_controller.rb +++ b/app/controllers/api/v1/timelines/base_controller.rb @@ -3,8 +3,14 @@ class Api::V1::Timelines::BaseController < Api::BaseController after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + before_action :require_user!, if: :require_auth? + private + def require_auth? + !Setting.timeline_preview + end + def pagination_collection @statuses end diff --git a/app/controllers/api/v1/timelines/link_controller.rb b/app/controllers/api/v1/timelines/link_controller.rb index af962c430f..37ed084f06 100644 --- a/app/controllers/api/v1/timelines/link_controller.rb +++ b/app/controllers/api/v1/timelines/link_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? + before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action :set_preview_card before_action :set_statuses @@ -17,10 +17,6 @@ class Api::V1::Timelines::LinkController < Api::V1::Timelines::BaseController private - def require_auth? - !Setting.timeline_preview - end - def set_preview_card @preview_card = PreviewCard.joins(:trend).merge(PreviewCardTrend.allowed).find_by!(url: params[:url]) end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 5a6b4d0e98..cd5445617b 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController - before_action :require_user!, only: [:show], if: :require_auth? + before_action -> { authorize_if_got_token! :read, :'read:statuses' } PERMITTED_PARAMS = %i(local remote limit only_media allow_local_only).freeze @@ -13,10 +13,6 @@ class Api::V1::Timelines::PublicController < Api::V1::Timelines::BaseController private - def require_auth? - !Setting.timeline_preview - end - def load_statuses preloaded_public_statuses_page end diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 3bf8f374e1..2b097aab0f 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Timelines::TagController < Api::V1::Timelines::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth? + before_action -> { authorize_if_got_token! :read, :'read:statuses' } before_action :load_tag PERMITTED_PARAMS = %i(local limit only_media).freeze diff --git a/app/controllers/api/v2_alpha/notifications_controller.rb b/app/controllers/api/v2_alpha/notifications_controller.rb index 19d3ac9018..edba23ab4a 100644 --- a/app/controllers/api/v2_alpha/notifications_controller.rb +++ b/app/controllers/api/v2_alpha/notifications_controller.rb @@ -15,7 +15,7 @@ class Api::V2Alpha::NotificationsController < Api::BaseController @relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id) end - render json: @notifications.map { |notification| NotificationGroup.from_notification(notification) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata + render json: @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata end def show diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index f858c0ad93..e5a2ac0270 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -25,6 +25,14 @@ class Auth::RegistrationsController < Devise::RegistrationsController super(&:build_invite_request) end + def edit # rubocop:disable Lint/UselessMethodDefinition + super + end + + def create # rubocop:disable Lint/UselessMethodDefinition + super + end + def update super do |resource| resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password? diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 8440df6b7e..7bb22453ca 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -17,6 +17,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio def destroy Web::PushSubscription.unsubscribe_for(params[:id], current_resource_owner) + Doorkeeper::Application.find_by(id: params[:id])&.close_streaming_sessions(current_resource_owner) super end diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb index 3b9d580499..2a3d954a35 100644 --- a/app/helpers/admin/account_moderation_notes_helper.rb +++ b/app/helpers/admin/account_moderation_notes_helper.rb @@ -4,27 +4,42 @@ module Admin::AccountModerationNotesHelper def admin_account_link_to(account, path: nil) return if account.nil? - link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do - safe_join([ - image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), - content_tag(:span, account.acct, class: 'username'), - ], ' ') - end + link_to( + labeled_account_avatar(account), + path || admin_account_path(account.id), + class: class_names('name-tag', suspended: suspended_account?(account)), + title: account.acct + ) end def admin_account_inline_link_to(account) return if account.nil? - link_to admin_account_path(account.id), class: name_tag_classes(account, true), title: account.acct do - content_tag(:span, account.acct, class: 'username') - end + link_to( + account_inline_text(account), + admin_account_path(account.id), + class: class_names('inline-name-tag', suspended: suspended_account?(account)), + title: account.acct + ) end private - def name_tag_classes(account, inline = false) - classes = [inline ? 'inline-name-tag' : 'name-tag'] - classes << 'suspended' if account.suspended? || (account.local? && account.user.nil?) - classes.join(' ') + def labeled_account_avatar(account) + safe_join( + [ + image_tag(account.avatar.url, width: 15, height: 15, alt: '', class: 'avatar'), + account_inline_text(account), + ], + ' ' + ) + end + + def account_inline_text(account) + content_tag(:span, account.acct, class: 'username') + end + + def suspended_account?(account) + account.suspended? || (account.local? && account.user.nil?) end end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 4018ef6b1c..e8d5634126 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -15,15 +15,15 @@ module Admin::ActionLogsHelper link_to log.human_identifier, admin_roles_path(log.target_id) when 'Report' link_to "##{log.human_identifier.presence || log.target_id}", admin_report_path(log.target_id) - when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain' - link_to log.human_identifier, "https://#{log.human_identifier.presence}" + when 'Instance', 'DomainBlock', 'DomainAllow', 'UnavailableDomain' + log.human_identifier.present? ? link_to(log.human_identifier, admin_instance_path(log.human_identifier)) : I18n.t('admin.action_logs.unavailable_instance') when 'Status' link_to log.human_identifier, log.permalink when 'AccountWarning' link_to log.human_identifier, disputes_strike_path(log.target_id) when 'Announcement' link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id) - when 'IpBlock', 'Instance', 'CustomEmoji' + when 'IpBlock', 'EmailDomainBlock', 'CustomEmoji' log.human_identifier when 'CanonicalEmailBlock' content_tag(:samp, (log.human_identifier.presence || '')[0...7], title: log.human_identifier) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b9802f2332..2369cff7e6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -257,6 +257,10 @@ module ApplicationHelper instance_presenter.app_icon&.file&.url(size) end + def use_mask_icon? + instance_presenter.app_icon.blank? + end + # glitch-soc addition to handle the multiple flavors def preload_locale_pack supported_locales = Themes.instance.flavour(current_flavour)['locales'] diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb index b0f2077db0..932a3420db 100644 --- a/app/helpers/jsonld_helper.rb +++ b/app/helpers/jsonld_helper.rb @@ -141,7 +141,7 @@ module JsonLdHelper def safe_for_forwarding?(original, compacted) original.without('@context', 'signature').all? do |key, value| compacted_value = compacted[key] - return false unless value.class == compacted_value.class + return false unless value.instance_of?(compacted_value.class) if value.is_a?(Hash) safe_for_forwarding?(value, compacted_value) diff --git a/app/javascript/flavours/glitch/actions/directory.js b/app/javascript/flavours/glitch/actions/directory.js deleted file mode 100644 index 7a0748029d..0000000000 --- a/app/javascript/flavours/glitch/actions/directory.js +++ /dev/null @@ -1,62 +0,0 @@ -import api from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; -export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; -export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; - -export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; -export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; -export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; - -export const fetchDirectory = params => (dispatch) => { - dispatch(fetchDirectoryRequest()); - - api().get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchDirectorySuccess(data)); - dispatch(fetchRelationships(data.map(x => x.id))); - }).catch(error => dispatch(fetchDirectoryFail(error))); -}; - -export const fetchDirectoryRequest = () => ({ - type: DIRECTORY_FETCH_REQUEST, -}); - -export const fetchDirectorySuccess = accounts => ({ - type: DIRECTORY_FETCH_SUCCESS, - accounts, -}); - -export const fetchDirectoryFail = error => ({ - type: DIRECTORY_FETCH_FAIL, - error, -}); - -export const expandDirectory = params => (dispatch, getState) => { - dispatch(expandDirectoryRequest()); - - const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size; - - api().get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(expandDirectorySuccess(data)); - dispatch(fetchRelationships(data.map(x => x.id))); - }).catch(error => dispatch(expandDirectoryFail(error))); -}; - -export const expandDirectoryRequest = () => ({ - type: DIRECTORY_EXPAND_REQUEST, -}); - -export const expandDirectorySuccess = accounts => ({ - type: DIRECTORY_EXPAND_SUCCESS, - accounts, -}); - -export const expandDirectoryFail = error => ({ - type: DIRECTORY_EXPAND_FAIL, - error, -}); diff --git a/app/javascript/flavours/glitch/actions/directory.ts b/app/javascript/flavours/glitch/actions/directory.ts new file mode 100644 index 0000000000..3e0f1356b3 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/directory.ts @@ -0,0 +1,37 @@ +import type { List as ImmutableList } from 'immutable'; + +import { apiGetDirectory } from 'flavours/glitch/api/directory'; +import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const fetchDirectory = createDataLoadingThunk( + 'directory/fetch', + async (params: Parameters[0]) => + apiGetDirectory(params), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((x) => x.id))); + + return { accounts: data }; + }, +); + +export const expandDirectory = createDataLoadingThunk( + 'directory/expand', + async (params: Parameters[0], { getState }) => { + const loadedItems = getState().user_lists.getIn([ + 'directory', + 'items', + ]) as ImmutableList; + + return apiGetDirectory({ ...params, offset: loadedItems.size }, 20); + }, + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((x) => x.id))); + + return { accounts: data }; + }, +); diff --git a/app/javascript/flavours/glitch/actions/importer/index.js b/app/javascript/flavours/glitch/actions/importer/index.js index 5fbc9bb5bb..7341ba8550 100644 --- a/app/javascript/flavours/glitch/actions/importer/index.js +++ b/app/javascript/flavours/glitch/actions/importer/index.js @@ -68,13 +68,17 @@ export function importFetchedStatuses(statuses) { status.filtered.forEach(result => pushUnique(filters, result.filter)); } - if (status.reblog && status.reblog.id) { + if (status.reblog?.id) { processStatus(status.reblog); } - if (status.poll && status.poll.id) { + if (status.poll?.id) { pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } + + if (status.card) { + status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account)); + } } statuses.forEach(processStatus); diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index c2ad0f9908..5f10c8d889 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -36,6 +36,17 @@ export function normalizeStatus(status, normalOldStatus, settings) { normalStatus.poll = status.poll.id; } + if (status.card) { + normalStatus.card = { + ...status.card, + authors: status.card.authors.map(author => ({ + ...author, + accountId: author.account?.id, + account: undefined, + })), + }; + } + if (status.filtered) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } diff --git a/app/javascript/flavours/glitch/actions/notification_policies.ts b/app/javascript/flavours/glitch/actions/notification_policies.ts new file mode 100644 index 0000000000..76452de324 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/notification_policies.ts @@ -0,0 +1,16 @@ +import { + apiGetNotificationPolicy, + apiUpdateNotificationsPolicy, +} from 'flavours/glitch/api/notification_policies'; +import type { NotificationPolicy } from 'flavours/glitch/models/notification_policy'; +import { createDataLoadingThunk } from 'flavours/glitch/store/typed_functions'; + +export const fetchNotificationPolicy = createDataLoadingThunk( + 'notificationPolicy/fetch', + () => apiGetNotificationPolicy(), +); + +export const updateNotificationsPolicy = createDataLoadingThunk( + 'notificationPolicy/update', + (policy: Partial) => apiUpdateNotificationsPolicy(policy), +); diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index ecea16a467..b9edbb7b6c 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -57,10 +57,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST'; -export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS'; -export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL'; - export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST'; export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS'; export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL'; @@ -435,40 +431,6 @@ export function setBrowserPermission (value) { }; } -export const fetchNotificationPolicy = () => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().get('/api/v1/notifications/policy').then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - -export const fetchNotificationPolicyRequest = () => ({ - type: NOTIFICATION_POLICY_FETCH_REQUEST, -}); - -export const fetchNotificationPolicySuccess = policy => ({ - type: NOTIFICATION_POLICY_FETCH_SUCCESS, - policy, -}); - -export const fetchNotificationPolicyFail = error => ({ - type: NOTIFICATION_POLICY_FETCH_FAIL, - error, -}); - -export const updateNotificationsPolicy = params => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().put('/api/v1/notifications/policy', params).then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - export const fetchNotificationRequests = () => (dispatch, getState) => { const params = {}; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index d8341a5c16..a55240646f 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, onDisconnect() { - dispatch(disconnectTimeline(timelineId)); + dispatch(disconnectTimeline({ timeline: timelineId })); if (options.fallback) { // @ts-expect-error diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 1fa2c3cde4..eb5050f152 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -7,9 +7,11 @@ import { toServerSideType } from 'flavours/glitch/utils/filters'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; +import {timelineDelete} from './timelines_typed'; + +export { disconnectTimeline } from './timelines_typed'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; @@ -18,7 +20,6 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; @@ -73,16 +74,10 @@ export function updateTimeline(timeline, status, accept) { export function deleteFromTimelines(id) { return (dispatch, getState) => { const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON(); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); - dispatch({ - type: TIMELINE_DELETE, - id, - accountId, - references, - reblogOf, - }); + dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf })); }; } @@ -175,6 +170,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, @@ -237,12 +233,6 @@ export function connectTimeline(timeline) { }; } -export const disconnectTimeline = timeline => ({ - type: TIMELINE_DISCONNECT, - timeline, - usePendingItems: preferPendingItems, -}); - export const markAsPartial = timeline => ({ type: TIMELINE_MARK_AS_PARTIAL, timeline, diff --git a/app/javascript/flavours/glitch/actions/timelines_typed.ts b/app/javascript/flavours/glitch/actions/timelines_typed.ts new file mode 100644 index 0000000000..485b94ed52 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/timelines_typed.ts @@ -0,0 +1,20 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state'; + +export const disconnectTimeline = createAction( + 'timeline/disconnect', + ({ timeline }: { timeline: string }) => ({ + payload: { + timeline, + usePendingItems: preferPendingItems, + }, + }), +); + +export const timelineDelete = createAction<{ + statusId: string; + accountId: string; + references: string[]; + reblogOf: string | null; +}>('timelines/delete'); diff --git a/app/javascript/flavours/glitch/actions/trends.js b/app/javascript/flavours/glitch/actions/trends.js index 0b840b41ce..0bdf17a5d2 100644 --- a/app/javascript/flavours/glitch/actions/trends.js +++ b/app/javascript/flavours/glitch/actions/trends.js @@ -1,6 +1,6 @@ import api, { getLinks } from '../api'; -import { importFetchedStatuses } from './importer'; +import { importFetchedStatuses, importFetchedAccounts } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; @@ -49,8 +49,11 @@ export const fetchTrendingLinks = () => (dispatch) => { dispatch(fetchTrendingLinksRequest()); api() - .get('/api/v1/trends/links') - .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) + .get('/api/v1/trends/links', { params: { limit: 20 } }) + .then(({ data }) => { + dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account))); + dispatch(fetchTrendingLinksSuccess(data)); + }) .catch(err => dispatch(fetchTrendingLinksFail(err))); }; diff --git a/app/javascript/flavours/glitch/api.ts b/app/javascript/flavours/glitch/api.ts index e133125a29..24672290c7 100644 --- a/app/javascript/flavours/glitch/api.ts +++ b/app/javascript/flavours/glitch/api.ts @@ -59,16 +59,49 @@ export default function api(withAuthorization = true) { }); } +type RequestParamsOrData = Record; + export async function apiRequest( method: Method, url: string, - params?: Record, + args: { + params?: RequestParamsOrData; + data?: RequestParamsOrData; + } = {}, ) { const { data } = await api().request({ method, url: '/api/' + url, - data: params, + ...args, }); return data; } + +export async function apiRequestGet( + url: string, + params?: RequestParamsOrData, +) { + return apiRequest('GET', url, { params }); +} + +export async function apiRequestPost( + url: string, + data?: RequestParamsOrData, +) { + return apiRequest('POST', url, { data }); +} + +export async function apiRequestPut( + url: string, + data?: RequestParamsOrData, +) { + return apiRequest('PUT', url, { data }); +} + +export async function apiRequestDelete( + url: string, + params?: RequestParamsOrData, +) { + return apiRequest('DELETE', url, { params }); +} diff --git a/app/javascript/flavours/glitch/api/accounts.ts b/app/javascript/flavours/glitch/api/accounts.ts index 346b3bc38c..410f3d20e3 100644 --- a/app/javascript/flavours/glitch/api/accounts.ts +++ b/app/javascript/flavours/glitch/api/accounts.ts @@ -1,7 +1,7 @@ -import { apiRequest } from 'flavours/glitch/api'; +import { apiRequestPost } from 'flavours/glitch/api'; import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships'; export const apiSubmitAccountNote = (id: string, value: string) => - apiRequest('post', `v1/accounts/${id}/note`, { + apiRequestPost(`v1/accounts/${id}/note`, { comment: value, }); diff --git a/app/javascript/flavours/glitch/api/directory.ts b/app/javascript/flavours/glitch/api/directory.ts new file mode 100644 index 0000000000..72743a2584 --- /dev/null +++ b/app/javascript/flavours/glitch/api/directory.ts @@ -0,0 +1,15 @@ +import { apiRequestGet } from 'flavours/glitch/api'; +import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts'; + +export const apiGetDirectory = ( + params: { + order: string; + local: boolean; + offset?: number; + }, + limit = 20, +) => + apiRequestGet('v1/directory', { + ...params, + limit, + }); diff --git a/app/javascript/flavours/glitch/api/interactions.ts b/app/javascript/flavours/glitch/api/interactions.ts index eaa83b2136..172f97a256 100644 --- a/app/javascript/flavours/glitch/api/interactions.ts +++ b/app/javascript/flavours/glitch/api/interactions.ts @@ -1,10 +1,10 @@ -import { apiRequest } from 'flavours/glitch/api'; +import { apiRequestPost } from 'flavours/glitch/api'; import type { Status, StatusVisibility } from 'flavours/glitch/models/status'; export const apiReblog = (statusId: string, visibility: StatusVisibility) => - apiRequest<{ reblog: Status }>('post', `v1/statuses/${statusId}/reblog`, { + apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, { visibility, }); export const apiUnreblog = (statusId: string) => - apiRequest('post', `v1/statuses/${statusId}/unreblog`); + apiRequestPost(`v1/statuses/${statusId}/unreblog`); diff --git a/app/javascript/flavours/glitch/api/notification_policies.ts b/app/javascript/flavours/glitch/api/notification_policies.ts new file mode 100644 index 0000000000..e52ea64f41 --- /dev/null +++ b/app/javascript/flavours/glitch/api/notification_policies.ts @@ -0,0 +1,9 @@ +import { apiRequestGet, apiRequestPut } from 'flavours/glitch/api'; +import type { NotificationPolicyJSON } from 'flavours/glitch/api_types/notification_policies'; + +export const apiGetNotificationPolicy = () => + apiRequestGet('/v1/notifications/policy'); + +export const apiUpdateNotificationsPolicy = ( + policy: Partial, +) => apiRequestPut('/v1/notifications/policy', policy); diff --git a/app/javascript/flavours/glitch/api_types/notification_policies.ts b/app/javascript/flavours/glitch/api_types/notification_policies.ts new file mode 100644 index 0000000000..0f4a2d132e --- /dev/null +++ b/app/javascript/flavours/glitch/api_types/notification_policies.ts @@ -0,0 +1,12 @@ +// See app/serializers/rest/notification_policy_serializer.rb + +export interface NotificationPolicyJSON { + filter_not_following: boolean; + filter_not_followers: boolean; + filter_new_accounts: boolean; + filter_private_mentions: boolean; + summary: { + pending_requests_count: number; + pending_notifications_count: number; + }; +} diff --git a/app/javascript/flavours/glitch/api_types/statuses.ts b/app/javascript/flavours/glitch/api_types/statuses.ts index d63441873d..9de86e7fa6 100644 --- a/app/javascript/flavours/glitch/api_types/statuses.ts +++ b/app/javascript/flavours/glitch/api_types/statuses.ts @@ -30,6 +30,12 @@ export interface ApiMentionJSON { acct: string; } +export interface ApiPreviewCardAuthorJSON { + name: string; + url: string; + account?: ApiAccountJSON; +} + export interface ApiPreviewCardJSON { url: string; title: string; @@ -38,6 +44,7 @@ export interface ApiPreviewCardJSON { type: string; author_name: string; author_url: string; + author_account?: ApiAccountJSON; provider_name: string; provider_url: string; html: string; @@ -48,6 +55,7 @@ export interface ApiPreviewCardJSON { embed_url: string; blurhash: string; published_at: string; + authors: ApiPreviewCardAuthorJSON[]; } export interface ApiStatusJSON { diff --git a/app/javascript/flavours/glitch/components/account_bio.tsx b/app/javascript/flavours/glitch/components/account_bio.tsx new file mode 100644 index 0000000000..567a2374c2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/account_bio.tsx @@ -0,0 +1,20 @@ +import { useLinks } from 'flavours/glitch/hooks/useLinks'; + +export const AccountBio: React.FC<{ + note: string; + className: string; +}> = ({ note, className }) => { + const handleClick = useLinks(); + + if (note.length === 0 || note === '

') { + return null; + } + + return ( +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/account_fields.tsx b/app/javascript/flavours/glitch/components/account_fields.tsx new file mode 100644 index 0000000000..768eb1fa4b --- /dev/null +++ b/app/javascript/flavours/glitch/components/account_fields.tsx @@ -0,0 +1,42 @@ +import classNames from 'classnames'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; +import { useLinks } from 'flavours/glitch/hooks/useLinks'; +import type { Account } from 'flavours/glitch/models/account'; + +export const AccountFields: React.FC<{ + fields: Account['fields']; + limit: number; +}> = ({ fields, limit = -1 }) => { + const handleClick = useLinks(); + + if (fields.size === 0) { + return null; + } + + return ( +
+ {fields.take(limit).map((pair, i) => ( +
+
+ +
+ {pair.get('verified_at') && ( + + )} + +
+
+ ))} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/column_header.jsx b/app/javascript/flavours/glitch/components/column_header.jsx deleted file mode 100644 index 210ec396fa..0000000000 --- a/app/javascript/flavours/glitch/components/column_header.jsx +++ /dev/null @@ -1,233 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent, useCallback } from 'react'; - -import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl'; - -import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; - -import AddIcon from '@/material-icons/400-24px/add.svg?react'; -import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; -import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; -import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; -import { Icon } from 'flavours/glitch/components/icon'; -import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context'; -import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; -import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; - - -import { useAppHistory } from './router'; - -const messages = defineMessages({ - show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, - hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, - moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, - moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - back: { id: 'column_back_button.label', defaultMessage: 'Back' }, -}); - -const BackButton = ({ onlyIcon }) => { - const history = useAppHistory(); - const intl = useIntl(); - - const handleBackClick = useCallback(() => { - if (history.location?.state?.fromMastodon) { - history.goBack(); - } else { - history.push('/'); - } - }, [history]); - - return ( - - ); -}; - -BackButton.propTypes = { - onlyIcon: PropTypes.bool, -}; - -class ColumnHeader extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - intl: PropTypes.object.isRequired, - title: PropTypes.node, - icon: PropTypes.string, - iconComponent: PropTypes.func, - active: PropTypes.bool, - multiColumn: PropTypes.bool, - extraButton: PropTypes.node, - showBackButton: PropTypes.bool, - children: PropTypes.node, - pinned: PropTypes.bool, - placeholder: PropTypes.bool, - onPin: PropTypes.func, - onMove: PropTypes.func, - onClick: PropTypes.func, - appendContent: PropTypes.node, - collapseIssues: PropTypes.bool, - ...WithRouterPropTypes, - }; - - state = { - collapsed: true, - animating: false, - }; - - handleToggleClick = (e) => { - e.stopPropagation(); - this.setState({ collapsed: !this.state.collapsed, animating: true }); - }; - - handleTitleClick = () => { - this.props.onClick?.(); - }; - - handleMoveLeft = () => { - this.props.onMove(-1); - }; - - handleMoveRight = () => { - this.props.onMove(1); - }; - - handleTransitionEnd = () => { - this.setState({ animating: false }); - }; - - handlePin = () => { - if (!this.props.pinned) { - this.props.history.replace('/'); - } - - this.props.onPin(); - }; - - render () { - const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props; - const { collapsed, animating } = this.state; - - const wrapperClassName = classNames('column-header__wrapper', { - 'active': active, - }); - - const buttonClassName = classNames('column-header', { - 'active': active, - }); - - const collapsibleClassName = classNames('column-header__collapsible', { - 'collapsed': collapsed, - 'animating': animating, - }); - - const collapsibleButtonClassName = classNames('column-header__button', { - 'active': !collapsed, - }); - - let extraContent, pinButton, moveButtons, backButton, collapseButton; - - if (children) { - extraContent = ( -
- {children} -
- ); - } - - if (multiColumn && pinned) { - pinButton = ; - - moveButtons = ( -
- - -
- ); - } else if (multiColumn && this.props.onPin) { - pinButton = ; - } - - if (history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) { - backButton = ; - } - - const collapsedContent = [ - extraContent, - ]; - - if (multiColumn) { - collapsedContent.push( -
- {pinButton} - {moveButtons} -
- ); - } - - if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) { - collapseButton = ( - - ); - } - - const hasTitle = (icon || iconComponent) && title; - - const component = ( -
-

- {hasTitle && ( - <> - {backButton} - - - - )} - - {!hasTitle && backButton} - -
- {extraButton} - {collapseButton} -
-

- -
-
- {(!collapsed || animating) && collapsedContent} -
-
- - {appendContent} -
- ); - - if (placeholder) { - return component; - } else { - return ( - {component} - ); - } - } - -} - -export default injectIntl(withIdentity(withRouter(ColumnHeader))); diff --git a/app/javascript/flavours/glitch/components/column_header.tsx b/app/javascript/flavours/glitch/components/column_header.tsx new file mode 100644 index 0000000000..9bd1559904 --- /dev/null +++ b/app/javascript/flavours/glitch/components/column_header.tsx @@ -0,0 +1,301 @@ +import { useCallback, useState } from 'react'; + +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import AddIcon from '@/material-icons/400-24px/add.svg?react'; +import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; +import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; +import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; +import type { IconProp } from 'flavours/glitch/components/icon'; +import { Icon } from 'flavours/glitch/components/icon'; +import { ButtonInTabsBar } from 'flavours/glitch/features/ui/util/columns_context'; +import { useIdentity } from 'flavours/glitch/identity_context'; + +import { useAppHistory } from './router'; + +const messages = defineMessages({ + show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, + hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, + moveLeft: { + id: 'column_header.moveLeft_settings', + defaultMessage: 'Move column to the left', + }, + moveRight: { + id: 'column_header.moveRight_settings', + defaultMessage: 'Move column to the right', + }, + back: { id: 'column_back_button.label', defaultMessage: 'Back' }, +}); + +const BackButton: React.FC<{ + onlyIcon: boolean; +}> = ({ onlyIcon }) => { + const history = useAppHistory(); + const intl = useIntl(); + + const handleBackClick = useCallback(() => { + if (history.location.state?.fromMastodon) { + history.goBack(); + } else { + history.push('/'); + } + }, [history]); + + return ( + + ); +}; + +export interface Props { + title?: string; + icon?: string; + iconComponent?: IconProp; + active?: boolean; + children?: React.ReactNode; + pinned?: boolean; + multiColumn?: boolean; + extraButton?: React.ReactNode; + showBackButton?: boolean; + placeholder?: boolean; + appendContent?: React.ReactNode; + collapseIssues?: boolean; + onClick?: () => void; + onMove?: (arg0: number) => void; + onPin?: () => void; +} + +export const ColumnHeader: React.FC = ({ + title, + icon, + iconComponent, + active, + children, + pinned, + multiColumn, + extraButton, + showBackButton, + placeholder, + appendContent, + collapseIssues, + onClick, + onMove, + onPin, +}) => { + const intl = useIntl(); + const { signedIn } = useIdentity(); + const history = useAppHistory(); + const [collapsed, setCollapsed] = useState(true); + const [animating, setAnimating] = useState(false); + + const handleToggleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setCollapsed((value) => !value); + setAnimating(true); + }, + [setCollapsed, setAnimating], + ); + + const handleTitleClick = useCallback(() => { + onClick?.(); + }, [onClick]); + + const handleMoveLeft = useCallback(() => { + onMove?.(-1); + }, [onMove]); + + const handleMoveRight = useCallback(() => { + onMove?.(1); + }, [onMove]); + + const handleTransitionEnd = useCallback(() => { + setAnimating(false); + }, [setAnimating]); + + const handlePin = useCallback(() => { + if (!pinned) { + history.replace('/'); + } + + onPin?.(); + }, [history, pinned, onPin]); + + const wrapperClassName = classNames('column-header__wrapper', { + active, + }); + + const buttonClassName = classNames('column-header', { + active, + }); + + const collapsibleClassName = classNames('column-header__collapsible', { + collapsed, + animating, + }); + + const collapsibleButtonClassName = classNames('column-header__button', { + active: !collapsed, + }); + + let extraContent, pinButton, moveButtons, backButton, collapseButton; + + if (children) { + extraContent = ( +
+ {children} +
+ ); + } + + if (multiColumn && pinned) { + pinButton = ( + + ); + + moveButtons = ( +
+ + +
+ ); + } else if (multiColumn && onPin) { + pinButton = ( + + ); + } + + if ( + !pinned && + ((multiColumn && history.location.state?.fromMastodon) || showBackButton) + ) { + backButton = ; + } + + const collapsedContent = [extraContent]; + + if (multiColumn) { + collapsedContent.push( +
+ {pinButton} + {moveButtons} +
, + ); + } + + if (signedIn && (children || (multiColumn && onPin))) { + collapseButton = ( + + ); + } + + const hasIcon = icon && iconComponent; + const hasTitle = hasIcon && title; + + const component = ( +
+

+ {hasTitle && ( + <> + {backButton} + + + + )} + + {!hasTitle && backButton} + +
+ {extraButton} + {collapseButton} +
+

+ +
+
+ {(!collapsed || animating) && collapsedContent} +
+
+ + {appendContent} +
+ ); + + if (placeholder) { + return component; + } else { + return {component}; + } +}; + +// eslint-disable-next-line import/no-default-export +export default ColumnHeader; diff --git a/app/javascript/flavours/glitch/components/follow_button.tsx b/app/javascript/flavours/glitch/components/follow_button.tsx new file mode 100644 index 0000000000..a6b2064703 --- /dev/null +++ b/app/javascript/flavours/glitch/components/follow_button.tsx @@ -0,0 +1,125 @@ +import { useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { useIdentity } from '@/flavours/glitch/identity_context'; +import { + fetchRelationships, + followAccount, + unfollowAccount, +} from 'flavours/glitch/actions/accounts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { Button } from 'flavours/glitch/components/button'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import { me } from 'flavours/glitch/initial_state'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; + +const messages = defineMessages({ + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, +}); + +export const FollowButton: React.FC<{ + accountId?: string; +}> = ({ accountId }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { signedIn } = useIdentity(); + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + const relationship = useAppSelector((state) => + accountId ? state.relationships.get(accountId) : undefined, + ); + const following = relationship?.following || relationship?.requested; + + useEffect(() => { + if (accountId && signedIn) { + dispatch(fetchRelationships([accountId])); + } + }, [dispatch, accountId, signedIn]); + + const handleClick = useCallback(() => { + if (!signedIn) { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'follow', + accountId: accountId, + url: account?.url, + }, + }), + ); + } + + if (!relationship) return; + + if (accountId === me) { + return; + } else if (relationship.following || relationship.requested) { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + message: ( + @{account?.acct} }} + /> + ), + confirm: intl.formatMessage(messages.unfollow), + onConfirm: () => { + dispatch(unfollowAccount(accountId)); + }, + }, + }), + ); + } else { + dispatch(followAccount(accountId)); + } + }, [dispatch, intl, accountId, relationship, account, signedIn]); + + let label; + + if (!signedIn) { + label = intl.formatMessage(messages.follow); + } else if (accountId === me) { + label = intl.formatMessage(messages.edit_profile); + } else if (!relationship) { + label = ; + } else if (!relationship.following && relationship.followed_by) { + label = intl.formatMessage(messages.followBack); + } else if (relationship.following || relationship.requested) { + label = intl.formatMessage(messages.unfollow); + } else { + label = intl.formatMessage(messages.follow); + } + + if (accountId === me) { + return ( + + {label} + + ); + } + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/components/hover_card_account.tsx b/app/javascript/flavours/glitch/components/hover_card_account.tsx new file mode 100644 index 0000000000..56f431a786 --- /dev/null +++ b/app/javascript/flavours/glitch/components/hover_card_account.tsx @@ -0,0 +1,78 @@ +import { useEffect, forwardRef } from 'react'; + +import classNames from 'classnames'; + +import { fetchAccount } from 'flavours/glitch/actions/accounts'; +import { AccountBio } from 'flavours/glitch/components/account_bio'; +import { AccountFields } from 'flavours/glitch/components/account_fields'; +import { Avatar } from 'flavours/glitch/components/avatar'; +import { FollowersCounter } from 'flavours/glitch/components/counters'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { FollowButton } from 'flavours/glitch/components/follow_button'; +import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; +import { Permalink } from 'flavours/glitch/components/permalink'; +import { ShortNumber } from 'flavours/glitch/components/short_number'; +import { domain } from 'flavours/glitch/initial_state'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; + +export const HoverCardAccount = forwardRef< + HTMLDivElement, + { accountId?: string } +>(({ accountId }, ref) => { + const dispatch = useAppDispatch(); + + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + + useEffect(() => { + if (accountId && !account) { + dispatch(fetchAccount(accountId)); + } + }, [dispatch, accountId, account]); + + return ( + + ); +}); + +HoverCardAccount.displayName = 'HoverCardAccount'; diff --git a/app/javascript/flavours/glitch/components/hover_card_controller.tsx b/app/javascript/flavours/glitch/components/hover_card_controller.tsx new file mode 100644 index 0000000000..347dcd4f2f --- /dev/null +++ b/app/javascript/flavours/glitch/components/hover_card_controller.tsx @@ -0,0 +1,176 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +import { useLocation } from 'react-router-dom'; + +import Overlay from 'react-overlays/Overlay'; +import type { + OffsetValue, + UsePopperOptions, +} from 'react-overlays/esm/usePopper'; + +import { HoverCardAccount } from 'flavours/glitch/components/hover_card_account'; +import { useTimeout } from 'flavours/glitch/hooks/useTimeout'; + +const offset = [-12, 4] as OffsetValue; +const enterDelay = 750; +const leaveDelay = 150; +const popperConfig = { strategy: 'fixed' } as UsePopperOptions; + +const isHoverCardAnchor = (element: HTMLElement) => + element.matches('[data-hover-card-account]'); + +export const HoverCardController: React.FC = () => { + const [open, setOpen] = useState(false); + const [accountId, setAccountId] = useState(); + const [anchor, setAnchor] = useState(null); + const cardRef = useRef(null); + const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); + const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); + const [setScrollTimeout] = useTimeout(); + const location = useLocation(); + + const handleClose = useCallback(() => { + cancelEnterTimeout(); + cancelLeaveTimeout(); + setOpen(false); + setAnchor(null); + }, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]); + + useEffect(() => { + handleClose(); + }, [handleClose, location]); + + useEffect(() => { + let isScrolling = false; + let currentAnchor: HTMLElement | null = null; + + const open = (target: HTMLElement) => { + target.setAttribute('aria-describedby', 'hover-card'); + setOpen(true); + setAnchor(target); + setAccountId(target.getAttribute('data-hover-card-account') ?? undefined); + }; + + const close = () => { + currentAnchor?.removeAttribute('aria-describedby'); + currentAnchor = null; + setOpen(false); + setAnchor(null); + setAccountId(undefined); + }; + + const handleMouseEnter = (e: MouseEvent) => { + const { target } = e; + + // We've exited the window + if (!(target instanceof HTMLElement)) { + close(); + return; + } + + // We've entered an anchor + if (!isScrolling && isHoverCardAnchor(target)) { + cancelLeaveTimeout(); + + currentAnchor?.removeAttribute('aria-describedby'); + currentAnchor = target; + + setEnterTimeout(() => { + open(target); + }, enterDelay); + } + + // We've entered the hover card + if ( + !isScrolling && + (target === currentAnchor || target === cardRef.current) + ) { + cancelLeaveTimeout(); + } + }; + + const handleMouseLeave = (e: MouseEvent) => { + if (!currentAnchor) { + return; + } + + if (e.target === currentAnchor || e.target === cardRef.current) { + cancelEnterTimeout(); + + setLeaveTimeout(() => { + close(); + }, leaveDelay); + } + }; + + const handleScrollEnd = () => { + isScrolling = false; + }; + + const handleScroll = () => { + isScrolling = true; + cancelEnterTimeout(); + setScrollTimeout(handleScrollEnd, 100); + }; + + const handleMouseMove = () => { + delayEnterTimeout(enterDelay); + }; + + document.body.addEventListener('mouseenter', handleMouseEnter, { + passive: true, + capture: true, + }); + + document.body.addEventListener('mousemove', handleMouseMove, { + passive: true, + capture: false, + }); + + document.body.addEventListener('mouseleave', handleMouseLeave, { + passive: true, + capture: true, + }); + + document.addEventListener('scroll', handleScroll, { + passive: true, + capture: true, + }); + + return () => { + document.body.removeEventListener('mouseenter', handleMouseEnter); + document.body.removeEventListener('mousemove', handleMouseMove); + document.body.removeEventListener('mouseleave', handleMouseLeave); + document.removeEventListener('scroll', handleScroll); + }; + }, [ + setEnterTimeout, + setLeaveTimeout, + setScrollTimeout, + cancelEnterTimeout, + cancelLeaveTimeout, + delayEnterTimeout, + setOpen, + setAccountId, + setAnchor, + ]); + + return ( + + {({ props }) => ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/components/more_from_author.jsx b/app/javascript/flavours/glitch/components/more_from_author.jsx new file mode 100644 index 0000000000..4f20ae76bf --- /dev/null +++ b/app/javascript/flavours/glitch/components/more_from_author.jsx @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + +import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link'; + +export const MoreFromAuthor = ({ accountId }) => ( +
+ + + + + }} /> +
+); + +MoreFromAuthor.propTypes = { + accountId: PropTypes.string.isRequired, +}; diff --git a/app/javascript/flavours/glitch/components/server_banner.jsx b/app/javascript/flavours/glitch/components/server_banner.jsx index 1b449ff1a8..43afebe9cf 100644 --- a/app/javascript/flavours/glitch/components/server_banner.jsx +++ b/app/javascript/flavours/glitch/components/server_banner.jsx @@ -42,10 +42,12 @@ class ServerBanner extends PureComponent { return (
- {domain}, mastodon: Mastodon }} /> + {domain}, mastodon: Mastodon }} />
- + + +
{isLoading ? ( @@ -84,10 +86,6 @@ class ServerBanner extends PureComponent { )}
- -
- -
); } diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index bfbad18367..2f70332280 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -810,7 +810,7 @@ class Status extends ImmutablePureComponent { {prepend}
diff --git a/app/javascript/flavours/glitch/components/status_content.jsx b/app/javascript/flavours/glitch/components/status_content.jsx index 24da69cdf2..c28f85eb72 100644 --- a/app/javascript/flavours/glitch/components/status_content.jsx +++ b/app/javascript/flavours/glitch/components/status_content.jsx @@ -181,7 +181,8 @@ class StatusContent extends PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', `@${mention.get('acct')}`); + link.removeAttribute('title'); + link.setAttribute('data-hover-card-account', mention.get('id')); if (rewriteMentions !== 'no') { while (link.firstChild) link.removeChild(link.firstChild); link.appendChild(document.createTextNode('@')); diff --git a/app/javascript/flavours/glitch/components/status_header.jsx b/app/javascript/flavours/glitch/components/status_header.jsx index 692dca5c7b..ee4573659c 100644 --- a/app/javascript/flavours/glitch/components/status_header.jsx +++ b/app/javascript/flavours/glitch/components/status_header.jsx @@ -51,6 +51,7 @@ export default class StatusHeader extends PureComponent { target='_blank' onClick={this.handleAccountClick} rel='noopener noreferrer' + data-hover-card-account={status.getIn(['account', 'id'])} >
{statusAvatar} diff --git a/app/javascript/flavours/glitch/components/status_list.jsx b/app/javascript/flavours/glitch/components/status_list.jsx index dde8bd9663..374d14a56a 100644 --- a/app/javascript/flavours/glitch/components/status_list.jsx +++ b/app/javascript/flavours/glitch/components/status_list.jsx @@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent { withCounters: PropTypes.bool, timelineId: PropTypes.string.isRequired, lastId: PropTypes.string, + bindToDocument: PropTypes.bool, regex: PropTypes.string, }; diff --git a/app/javascript/flavours/glitch/components/status_prepend.jsx b/app/javascript/flavours/glitch/components/status_prepend.jsx index d911465408..12eaccec85 100644 --- a/app/javascript/flavours/glitch/components/status_prepend.jsx +++ b/app/javascript/flavours/glitch/components/status_prepend.jsx @@ -40,6 +40,7 @@ export default class StatusPrepend extends PureComponent { onClick={this.handleClick} href={account.get('url')} className='status__display-name' + data-hover-card-account={account.get('id')} > { const mapDispatchToProps = (dispatch, { intl }) => ({ onFollow (account) { - if (account.getIn(['relationship', 'following'])) { + if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { dispatch(openModal({ modalType: 'CONFIRM', modalProps: { @@ -52,15 +51,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onConfirm: () => dispatch(unfollowAccount(account.get('id'))), }, })); - } else if (account.getIn(['relationship', 'requested'])) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, - })); } else { dispatch(followAccount(account.get('id'))); } diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx index 8edf75203f..1ad9e03040 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -110,18 +110,6 @@ class LanguageDropdownMenu extends PureComponent { }).map(result => result.obj); } - frequentlyUsed () { - const { languages, value } = this.props; - const current = languages.find(lang => lang[0] === value); - const results = []; - - if (current) { - results.push(current); - } - - return results; - } - handleClick = e => { const value = e.currentTarget.getAttribute('data-index'); diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx index 458a547d02..7071c8719d 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.jsx @@ -185,7 +185,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete }); const names = accounts.map(a => ( - + { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { id }) => ({ - account: getAccount(state, id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onFollow(account) { - if (account.getIn(['relationship', 'following'])) { - dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - message: ( - @{account.get('acct')} }} - /> - ), - confirm: intl.formatMessage(messages.unfollowConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - } }), - ); - } else if (account.getIn(['relationship', 'requested'])) { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: @{account.get('acct')} }} />, - confirm: intl.formatMessage(messages.cancelFollowRequestConfirm), - onConfirm: () => dispatch(unfollowAccount(account.get('id'))), - }, - })); - } else { - dispatch(followAccount(account.get('id'))); - } - }, - - onBlock(account) { - if (account.getIn(['relationship', 'blocking'])) { - dispatch(unblockAccount(account.get('id'))); - } - }, - - onMute(account) { - if (account.getIn(['relationship', 'muting'])) { - dispatch(unmuteAccount(account.get('id'))); - } - }, - -}); - -class AccountCard extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - intl: PropTypes.object.isRequired, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onDismiss: PropTypes.func, - }; - - handleMouseEnter = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-original'); - } - }; - - handleMouseLeave = ({ currentTarget }) => { - if (autoPlayGif) { - return; - } - - const emojis = currentTarget.querySelectorAll('.custom-emoji'); - - for (var i = 0; i < emojis.length; i++) { - let emoji = emojis[i]; - emoji.src = emoji.getAttribute('data-static'); - } - }; - - handleFollow = () => { - this.props.onFollow(this.props.account); - }; - - handleBlock = () => { - this.props.onBlock(this.props.account); - }; - - handleMute = () => { - this.props.onMute(this.props.account); - }; - - handleEditProfile = () => { - window.open('/settings/profile', '_blank'); - }; - - handleDismiss = (e) => { - const { account, onDismiss } = this.props; - onDismiss(account.get('id')); - - e.preventDefault(); - e.stopPropagation(); - }; - - render() { - const { account, intl } = this.props; - - let actionBtn; - - if (me !== account.get('id')) { - if (!account.get('relationship')) { // Wait until the relationship is loaded - actionBtn = ''; - } else if (account.getIn(['relationship', 'requested'])) { - actionBtn =
diff --git a/app/javascript/flavours/glitch/features/explore/components/story.jsx b/app/javascript/flavours/glitch/features/explore/components/story.jsx index 0e5ab92122..b07425b277 100644 --- a/app/javascript/flavours/glitch/features/explore/components/story.jsx +++ b/app/javascript/flavours/glitch/features/explore/components/story.jsx @@ -1,61 +1,91 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useState, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + + import { Blurhash } from 'flavours/glitch/components/blurhash'; -import { accountsCountRenderer } from 'flavours/glitch/components/hashtag'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import { ShortNumber } from 'flavours/glitch/components/short_number'; import { Skeleton } from 'flavours/glitch/components/skeleton'; -export default class Story extends PureComponent { +import { AuthorLink } from './author_link'; - static propTypes = { - url: PropTypes.string, - title: PropTypes.string, - lang: PropTypes.string, - publisher: PropTypes.string, - publishedAt: PropTypes.string, - author: PropTypes.string, - sharedTimes: PropTypes.number, - thumbnail: PropTypes.string, - thumbnailDescription: PropTypes.string, - blurhash: PropTypes.string, - expanded: PropTypes.bool, - }; +const sharesCountRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); - state = { - thumbnailLoaded: false, - }; +export const Story = ({ + url, + title, + lang, + publisher, + publishedAt, + author, + authorAccount, + sharedTimes, + thumbnail, + thumbnailDescription, + blurhash, + expanded +}) => { + const [thumbnailLoaded, setThumbnailLoaded] = useState(false); - handleImageLoad = () => this.setState({ thumbnailLoaded: true }); + const handleImageLoad = useCallback(() => { + setThumbnailLoaded(true); + }, [setThumbnailLoaded]); - render () { - const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props; - - const { thumbnailLoaded } = this.state; - - return ( - -
-
{publisher ? {publisher} : }{publishedAt && <> · }
-
{title ? title : }
-
{author && <>{author} }} /> · }{typeof sharedTimes === 'number' ? : }
+ return ( +
+ + ); +}; -} +Story.propTypes = { + url: PropTypes.string, + title: PropTypes.string, + lang: PropTypes.string, + publisher: PropTypes.string, + publishedAt: PropTypes.string, + author: PropTypes.string, + authorAccount: PropTypes.string, + sharedTimes: PropTypes.number, + thumbnail: PropTypes.string, + thumbnailDescription: PropTypes.string, + blurhash: PropTypes.string, + expanded: PropTypes.bool, +}; diff --git a/app/javascript/flavours/glitch/features/explore/links.jsx b/app/javascript/flavours/glitch/features/explore/links.jsx index 5dee66d183..b5eb9c9d4f 100644 --- a/app/javascript/flavours/glitch/features/explore/links.jsx +++ b/app/javascript/flavours/glitch/features/explore/links.jsx @@ -13,7 +13,7 @@ import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; -import Story from './components/story'; +import { Story } from './components/story'; const mapStateToProps = state => ({ links: state.getIn(['trends', 'links', 'items']), @@ -75,6 +75,7 @@ class Links extends PureComponent { publisher={link.get('provider_name')} publishedAt={link.get('published_at')} author={link.get('author_name')} + authorAccount={link.getIn(['authors', 0, 'account', 'id'])} sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1} thumbnail={link.get('image')} thumbnailDescription={link.get('image_description')} diff --git a/app/javascript/flavours/glitch/features/home_timeline/components/inline_follow_suggestions.jsx b/app/javascript/flavours/glitch/features/home_timeline/components/inline_follow_suggestions.jsx index 97b64a09b1..4e727a63ed 100644 --- a/app/javascript/flavours/glitch/features/home_timeline/components/inline_follow_suggestions.jsx +++ b/app/javascript/flavours/glitch/features/home_timeline/components/inline_follow_suggestions.jsx @@ -12,12 +12,11 @@ import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; -import { followAccount, unfollowAccount } from 'flavours/glitch/actions/accounts'; import { changeSetting } from 'flavours/glitch/actions/settings'; import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions'; import { Avatar } from 'flavours/glitch/components/avatar'; -import { Button } from 'flavours/glitch/components/button'; import { DisplayName } from 'flavours/glitch/components/display_name'; +import { FollowButton } from 'flavours/glitch/components/follow_button'; import { Icon } from 'flavours/glitch/components/icon'; import { IconButton } from 'flavours/glitch/components/icon_button'; import { VerifiedBadge } from 'flavours/glitch/components/verified_badge'; @@ -79,18 +78,8 @@ Source.propTypes = { const Card = ({ id, sources }) => { const intl = useIntl(); const account = useSelector(state => state.getIn(['accounts', id])); - const relationship = useSelector(state => state.getIn(['relationships', id])); const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at')); const dispatch = useDispatch(); - const following = relationship?.get('following') ?? relationship?.get('requested'); - - const handleFollow = useCallback(() => { - if (following) { - dispatch(unfollowAccount(id)); - } else { - dispatch(followAccount(id)); - } - }, [id, following, dispatch]); const handleDismiss = useCallback(() => { dispatch(dismissSuggestion(id)); @@ -109,7 +98,7 @@ const Card = ({ id, sources }) => { {firstVerifiedField ? : }
-
); }; diff --git a/app/javascript/flavours/glitch/features/link_timeline/index.tsx b/app/javascript/flavours/glitch/features/link_timeline/index.tsx new file mode 100644 index 0000000000..bbe295d474 --- /dev/null +++ b/app/javascript/flavours/glitch/features/link_timeline/index.tsx @@ -0,0 +1,77 @@ +import { useRef, useEffect, useCallback } from 'react'; + +import { Helmet } from 'react-helmet'; +import { useParams } from 'react-router-dom'; + +import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; +import { expandLinkTimeline } from 'flavours/glitch/actions/timelines'; +import Column from 'flavours/glitch/components/column'; +import { ColumnHeader } from 'flavours/glitch/components/column_header'; +import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container'; +import type { Card } from 'flavours/glitch/models/status'; +import { useAppDispatch, useAppSelector } from 'flavours/glitch/store'; + +export const LinkTimeline: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const { url } = useParams<{ url: string }>(); + const decodedUrl = url ? decodeURIComponent(url) : undefined; + const dispatch = useAppDispatch(); + const columnRef = useRef(null); + const firstStatusId = useAppSelector((state) => + decodedUrl + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) + : undefined, + ); + const story = useAppSelector((state) => + firstStatusId + ? (state.statuses.getIn([firstStatusId, 'card']) as Card) + : undefined, + ); + + const handleHeaderClick = useCallback(() => { + columnRef.current?.scrollTop(); + }, []); + + const handleLoadMore = useCallback( + (maxId: string) => { + dispatch(expandLinkTimeline(decodedUrl, { maxId })); + }, + [dispatch, decodedUrl], + ); + + useEffect(() => { + dispatch(expandLinkTimeline(decodedUrl)); + }, [dispatch, decodedUrl]); + + return ( + + + + + + + {story?.title} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default LinkTimeline; diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx index 2a97ebd5bd..0d77b3d581 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx @@ -25,7 +25,7 @@ class ColumnSettings extends PureComponent { alertsEnabled: PropTypes.bool, browserSupport: PropTypes.bool, browserPermission: PropTypes.string, - notificationPolicy: ImmutablePropTypes.map, + notificationPolicy: PropTypes.object.isRequired, onChangePolicy: PropTypes.func.isRequired, }; @@ -84,22 +84,22 @@ class ColumnSettings extends PureComponent {

- + - + - + - + diff --git a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx deleted file mode 100644 index 8c8b9e4b86..0000000000 --- a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useDispatch, useSelector } from 'react-redux'; - -import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; -import { fetchNotificationPolicy } from 'flavours/glitch/actions/notifications'; -import { Icon } from 'flavours/glitch/components/icon'; -import { toCappedNumber } from 'flavours/glitch/utils/numbers'; - -export const FilteredNotificationsBanner = () => { - const dispatch = useDispatch(); - const policy = useSelector(state => state.get('notificationPolicy')); - - useEffect(() => { - dispatch(fetchNotificationPolicy()); - - const interval = setInterval(() => { - dispatch(fetchNotificationPolicy()); - }, 120000); - - return () => { - clearInterval(interval); - }; - }, [dispatch]); - - if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) { - return null; - } - - return ( - - - -
- - -
- -
-
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
- -
- - ); -}; diff --git a/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx new file mode 100644 index 0000000000..70762be0e3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/filtered_notifications_banner.tsx @@ -0,0 +1,68 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; +import { fetchNotificationPolicy } from 'flavours/glitch/actions/notification_policies'; +import { Icon } from 'flavours/glitch/components/icon'; +import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; +import { toCappedNumber } from 'flavours/glitch/utils/numbers'; + +export const FilteredNotificationsBanner: React.FC = () => { + const dispatch = useAppDispatch(); + const policy = useAppSelector((state) => state.notificationPolicy); + + useEffect(() => { + void dispatch(fetchNotificationPolicy()); + + const interval = setInterval(() => { + void dispatch(fetchNotificationPolicy()); + }, 120000); + + return () => { + clearInterval(interval); + }; + }, [dispatch]); + + if (policy === null || policy.summary.pending_notifications_count === 0) { + return null; + } + + return ( + + + +
+ + + + + + +
+ +
+
+ {toCappedNumber(policy.summary.pending_notifications_count)} +
+ +
+ + ); +}; diff --git a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx index c66b6d8b87..8e74584165 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notification.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/notification.jsx @@ -414,6 +414,7 @@ class Notification extends ImmutablePureComponent { title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} + data-hover-card-account={targetAccount.get('id')} /> ); @@ -448,6 +449,7 @@ class Notification extends ImmutablePureComponent { title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} + data-hover-card-account={account.get('id')} /> ); diff --git a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js index de266160f8..4547fde042 100644 --- a/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js +++ b/app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js @@ -4,7 +4,8 @@ import { connect } from 'react-redux'; import { showAlert } from '../../../actions/alerts'; import { openModal } from '../../../actions/modal'; -import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications'; +import { updateNotificationsPolicy } from '../../../actions/notification_policies'; +import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { changeSetting } from '../../../actions/settings'; import ColumnSettings from '../components/column_settings'; @@ -15,13 +16,16 @@ const messages = defineMessages({ permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' }, }); +/** + * @param {import('flavours/glitch/store').RootState} state + */ const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), pushSettings: state.get('push_notifications'), alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true), browserSupport: state.getIn(['notifications', 'browserSupport']), browserPermission: state.getIn(['notifications', 'browserPermission']), - notificationPolicy: state.get('notificationPolicy'), + notificationPolicy: state.notificationPolicy, }); const mapDispatchToProps = (dispatch, { intl }) => ({ diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx index c6094d0beb..0d315cbbf1 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.jsx +++ b/app/javascript/flavours/glitch/features/status/components/card.jsx @@ -11,10 +11,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react'; import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react'; -import { Avatar } from 'flavours/glitch/components/avatar'; import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Icon } from 'flavours/glitch/components/icon'; -import { Permalink } from 'flavours/glitch/components/permalink'; +import { MoreFromAuthor } from 'flavours/glitch/components/more_from_author'; import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; import { useBlurhash } from 'flavours/glitch/initial_state'; import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; @@ -48,20 +47,6 @@ const addAutoPlay = html => { return html; }; -const MoreFromAuthor = ({ author }) => ( -
- - - - - {author.get('display_name')} }} /> -
-); - -MoreFromAuthor.propTypes = { - author: ImmutablePropTypes.map, -}; - export default class Card extends PureComponent { static propTypes = { @@ -142,7 +127,7 @@ export default class Card extends PureComponent { const interactive = card.get('type') === 'video'; const language = card.get('language') || ''; const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive; - const showAuthor = !!card.get('author_account'); + const showAuthor = !!card.getIn(['authors', 0, 'accountId']); const description = (
@@ -248,7 +233,7 @@ export default class Card extends PureComponent { {description} - {showAuthor && } + {showAuthor && } ); } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx index c9dd0faa4b..d8c62aa16a 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.jsx @@ -289,7 +289,7 @@ class DetailedStatus extends ImmutablePureComponent { return (
- +
diff --git a/app/javascript/flavours/glitch/features/ui/components/column_loading.tsx b/app/javascript/flavours/glitch/features/ui/components/column_loading.tsx index 42174838cf..aa6d105dcc 100644 --- a/app/javascript/flavours/glitch/features/ui/components/column_loading.tsx +++ b/app/javascript/flavours/glitch/features/ui/components/column_loading.tsx @@ -1,11 +1,8 @@ -import Column from '../../../components/column'; -import ColumnHeader from '../../../components/column_header'; +import Column from 'flavours/glitch/components/column'; +import { ColumnHeader } from 'flavours/glitch/components/column_header'; +import type { Props as ColumnHeaderProps } from 'flavours/glitch/components/column_header'; -interface Props { - multiColumn?: boolean; -} - -export const ColumnLoading: React.FC = (otherProps) => ( +export const ColumnLoading: React.FC = (otherProps) => (
diff --git a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx index 5db3cb492b..bda9aed025 100644 --- a/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/sign_in_banner.jsx @@ -22,7 +22,8 @@ const SignInBanner = () => { if (sso_redirect) { return (
-

+

+

); @@ -44,7 +45,8 @@ const SignInBanner = () => { return (
-

+

+

{signupButton}
diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index 4a7b9ebfde..747922f23e 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -15,6 +15,7 @@ import { HotKeys } from 'react-hotkeys'; import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding'; +import { HoverCardController } from 'flavours/glitch/components/hover_card_controller'; import { Permalink } from 'flavours/glitch/components/permalink'; import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture'; import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context'; @@ -57,6 +58,7 @@ import { FavouritedStatuses, BookmarkedStatuses, FollowedTags, + LinkTimeline, ListTimeline, Blocks, DomainBlocks, @@ -210,6 +212,7 @@ class SwitchingColumnsArea extends PureComponent { + @@ -648,6 +651,7 @@ class UI extends PureComponent { {layout !== 'mobile' && } + diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index 6a140e3fd7..a312cefff7 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -213,3 +213,7 @@ export function NotificationRequests () { export function NotificationRequest () { return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request'); } + +export function LinkTimeline () { + return import(/*webpackChunkName: "features/glitch/link_timeline" */'../../link_timeline'); +} diff --git a/app/javascript/flavours/glitch/hooks/useLinks.ts b/app/javascript/flavours/glitch/hooks/useLinks.ts new file mode 100644 index 0000000000..bb988665fd --- /dev/null +++ b/app/javascript/flavours/glitch/hooks/useLinks.ts @@ -0,0 +1,61 @@ +import { useCallback } from 'react'; + +import { useHistory } from 'react-router-dom'; + +import { openURL } from 'flavours/glitch/actions/search'; +import { useAppDispatch } from 'flavours/glitch/store'; + +const isMentionClick = (element: HTMLAnchorElement) => + element.classList.contains('mention'); + +const isHashtagClick = (element: HTMLAnchorElement) => + element.textContent?.[0] === '#' || + element.previousSibling?.textContent?.endsWith('#'); + +export const useLinks = () => { + const history = useHistory(); + const dispatch = useAppDispatch(); + + const handleHashtagClick = useCallback( + (element: HTMLAnchorElement) => { + const { textContent } = element; + + if (!textContent) return; + + history.push(`/tags/${textContent.replace(/^#/, '')}`); + }, + [history], + ); + + const handleMentionClick = useCallback( + (element: HTMLAnchorElement) => { + dispatch( + openURL(element.href, history, () => { + window.location.href = element.href; + }), + ); + }, + [dispatch, history], + ); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const target = (e.target as HTMLElement).closest('a'); + + if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) { + return; + } + + if (isMentionClick(target)) { + e.preventDefault(); + handleMentionClick(target); + } else if (isHashtagClick(target)) { + e.preventDefault(); + handleHashtagClick(target); + } + }, + [handleMentionClick, handleHashtagClick], + ); + + return handleClick; +}; diff --git a/app/javascript/flavours/glitch/hooks/useTimeout.ts b/app/javascript/flavours/glitch/hooks/useTimeout.ts new file mode 100644 index 0000000000..bb1e8848dd --- /dev/null +++ b/app/javascript/flavours/glitch/hooks/useTimeout.ts @@ -0,0 +1,44 @@ +import { useRef, useCallback, useEffect } from 'react'; + +export const useTimeout = () => { + const timeoutRef = useRef>(); + const callbackRef = useRef<() => void>(); + + const set = useCallback((callback: () => void, delay: number) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + callbackRef.current = callback; + timeoutRef.current = setTimeout(callback, delay); + }, []); + + const delay = useCallback((delay: number) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (!callbackRef.current) { + return; + } + + timeoutRef.current = setTimeout(callbackRef.current, delay); + }, []); + + const cancel = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = undefined; + callbackRef.current = undefined; + } + }, []); + + useEffect( + () => () => { + cancel(); + }, + [cancel], + ); + + return [set, cancel, delay] as const; +}; diff --git a/app/javascript/flavours/glitch/models/notification_policy.ts b/app/javascript/flavours/glitch/models/notification_policy.ts new file mode 100644 index 0000000000..7d97559413 --- /dev/null +++ b/app/javascript/flavours/glitch/models/notification_policy.ts @@ -0,0 +1,3 @@ +import type { NotificationPolicyJSON } from 'flavours/glitch/api_types/notification_policies'; + +export type NotificationPolicy = NotificationPolicyJSON; // No changes from the API type diff --git a/app/javascript/flavours/glitch/models/status.ts b/app/javascript/flavours/glitch/models/status.ts index d9566daf39..bf1784bc61 100644 --- a/app/javascript/flavours/glitch/models/status.ts +++ b/app/javascript/flavours/glitch/models/status.ts @@ -1,4 +1,12 @@ +import type { RecordOf } from 'immutable'; + +import type { ApiPreviewCardJSON } from 'flavours/glitch/api_types/statuses'; + export type { StatusVisibility } from 'flavours/glitch/api_types/statuses'; // Temporary until we type it correctly export type Status = Immutable.Map; + +type CardShape = Required; + +export type Card = RecordOf; diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 36430e8238..211402ab2a 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; + import { COMPOSE_MOUNT, COMPOSE_UNMOUNT, @@ -54,7 +56,6 @@ import { } from '../actions/compose'; import { REDRAFT } from '../actions/statuses'; import { STORE_HYDRATE } from '../actions/store'; -import { TIMELINE_DELETE } from '../actions/timelines'; import { me, defaultContentType } from '../initial_state'; import { recoverHashtags } from '../utils/hashtag'; import { unescapeHTML } from '../utils/html'; @@ -551,10 +552,10 @@ export default function compose(state = initialState, action) { return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: return state.set('tagHistory', fromJS(action.tags)); - case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { + case timelineDelete.type: + if (action.payload.statusId === state.get('in_reply_to')) { return state.set('in_reply_to', null); - } else if (action.id === state.get('id')) { + } else if (action.payload.statusId === state.get('id')) { return state.set('id', null); } else { return state; diff --git a/app/javascript/flavours/glitch/reducers/contexts.js b/app/javascript/flavours/glitch/reducers/contexts.js index f7d7419a4e..07fe8ffb8a 100644 --- a/app/javascript/flavours/glitch/reducers/contexts.js +++ b/app/javascript/flavours/glitch/reducers/contexts.js @@ -1,11 +1,13 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; + import { blockAccountSuccess, muteAccountSuccess, } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; +import { TIMELINE_UPDATE } from '../actions/timelines'; import { compareId } from '../compare_id'; const initialState = ImmutableMap({ @@ -97,8 +99,8 @@ export default function replies(state = initialState, action) { return filterContexts(state, action.payload.relationship, action.payload.statuses); case CONTEXT_FETCH_SUCCESS: return normalizeContext(state, action.id, action.ancestors, action.descendants); - case TIMELINE_DELETE: - return deleteFromContexts(state, [action.id]); + case timelineDelete.type: + return deleteFromContexts(state, [action.payload.statusId]); case TIMELINE_UPDATE: return updateContext(state, action.status); default: diff --git a/app/javascript/flavours/glitch/reducers/modal.ts b/app/javascript/flavours/glitch/reducers/modal.ts index 368f26542c..c9ecdb7fdf 100644 --- a/app/javascript/flavours/glitch/reducers/modal.ts +++ b/app/javascript/flavours/glitch/reducers/modal.ts @@ -1,10 +1,11 @@ import type { Reducer } from '@reduxjs/toolkit'; import { Record as ImmutableRecord, Stack } from 'immutable'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; + import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import type { ModalType } from '../actions/modal'; import { openModal, closeModal } from '../actions/modal'; -import { TIMELINE_DELETE } from '../actions/timelines'; export type ModalProps = Record; interface Modal { @@ -72,10 +73,10 @@ export const modalReducer: Reducer = (state = initialState, action) => { // TODO: type those actions else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS) return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); - else if (action.type === TIMELINE_DELETE) + else if (timelineDelete.match(action)) return state.update('stack', (stack) => stack.filterNot( - (modal) => modal.get('modalProps').statusId === action.id, + (modal) => modal.get('modalProps').statusId === action.payload.statusId, ), ); else return state; diff --git a/app/javascript/flavours/glitch/reducers/notification_policy.js b/app/javascript/flavours/glitch/reducers/notification_policy.js deleted file mode 100644 index 579f2afdb2..0000000000 --- a/app/javascript/flavours/glitch/reducers/notification_policy.js +++ /dev/null @@ -1,12 +0,0 @@ -import { fromJS } from 'immutable'; - -import { NOTIFICATION_POLICY_FETCH_SUCCESS } from 'flavours/glitch/actions/notifications'; - -export const notificationPolicyReducer = (state = null, action) => { - switch(action.type) { - case NOTIFICATION_POLICY_FETCH_SUCCESS: - return fromJS(action.policy); - default: - return state; - } -}; diff --git a/app/javascript/flavours/glitch/reducers/notification_policy.ts b/app/javascript/flavours/glitch/reducers/notification_policy.ts new file mode 100644 index 0000000000..2d5450ce44 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/notification_policy.ts @@ -0,0 +1,18 @@ +import { createReducer, isAnyOf } from '@reduxjs/toolkit'; + +import { + fetchNotificationPolicy, + updateNotificationsPolicy, +} from 'flavours/glitch/actions/notification_policies'; +import type { NotificationPolicy } from 'flavours/glitch/models/notification_policy'; + +export const notificationPolicyReducer = + createReducer(null, (builder) => { + builder.addMatcher( + isAnyOf( + fetchNotificationPolicy.fulfilled, + updateNotificationsPolicy.fulfilled, + ), + (_state, action) => action.payload, + ); + }); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index c5f36dd94a..226f2affe1 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -1,6 +1,7 @@ import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { blockDomainSuccess } from 'flavours/glitch/actions/domain_blocks'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; import { authorizeFollowRequestSuccess, @@ -33,7 +34,7 @@ import { NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, } from '../actions/notifications'; -import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; +import { disconnectTimeline } from '../actions/timelines'; import { compareId } from '../compare_id'; const initialState = ImmutableMap({ @@ -333,11 +334,11 @@ export default function notifications(state = initialState, action) { return filterNotifications(state, [action.payload.id], 'follow_request'); case NOTIFICATIONS_CLEAR: return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); - case TIMELINE_DELETE: - return deleteByStatus(state, action.id); - case TIMELINE_DISCONNECT: - return action.timeline === 'home' ? - state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : + case timelineDelete.type: + return deleteByStatus(state, action.payload.statusId); + case disconnectTimeline.type: + return action.payload.timeline === 'home' ? + state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; case NOTIFICATIONS_SET_BROWSER_SUPPORT: return state.set('browserSupport', action.value); diff --git a/app/javascript/flavours/glitch/reducers/picture_in_picture.ts b/app/javascript/flavours/glitch/reducers/picture_in_picture.ts index 35eb10cf57..6ad31d42bf 100644 --- a/app/javascript/flavours/glitch/reducers/picture_in_picture.ts +++ b/app/javascript/flavours/glitch/reducers/picture_in_picture.ts @@ -4,8 +4,7 @@ import { deployPictureInPictureAction, removePictureInPicture, } from 'flavours/glitch/actions/picture_in_picture'; - -import { TIMELINE_DELETE } from '../actions/timelines'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; export interface PIPMediaProps { src: string; @@ -49,8 +48,9 @@ export const pictureInPictureReducer: Reducer = ( ...action.payload.props, }; else if (removePictureInPicture.match(action)) return initialState; - else if (action.type === TIMELINE_DELETE) - if (state.type && state.statusId === action.id) return initialState; + else if (timelineDelete.match(action)) + if (state.type && state.statusId === action.payload.statusId) + return initialState; return state; }; diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 0d1e13f260..ca7e98aa48 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; + import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { normalizeStatusTranslation } from '../actions/importer/normalizer'; import { @@ -32,7 +34,6 @@ import { STATUS_FETCH_REQUEST, STATUS_FETCH_FAIL, } from '../actions/statuses'; -import { TIMELINE_DELETE } from '../actions/timelines'; const importStatus = (state, status) => state.set(status.id, fromJS(status)); @@ -164,8 +165,8 @@ export default function statuses(state = initialState, action) { }); case STATUS_COLLAPSE: return state.setIn([action.id, 'collapsed'], action.isCollapsed); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references); + case timelineDelete.type: + return deleteStatus(state, action.payload.statusId, action.payload.references); case STATUS_TRANSLATE_SUCCESS: return statusTranslateSuccess(state, action.id, action.translation); case STATUS_TRANSLATE_UNDO: diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 08c7a48336..f4944472bb 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { timelineDelete } from 'flavours/glitch/actions/timelines_typed'; + import { blockAccountSuccess, muteAccountSuccess, @@ -7,19 +9,18 @@ import { } from '../actions/accounts'; import { TIMELINE_UPDATE, - TIMELINE_DELETE, TIMELINE_CLEAR, TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, - TIMELINE_DISCONNECT, TIMELINE_LOAD_PENDING, TIMELINE_MARK_AS_PARTIAL, TIMELINE_INSERT, TIMELINE_GAP, TIMELINE_SUGGESTIONS, + disconnectTimeline, } from '../actions/timelines'; import { compareId } from '../compare_id'; @@ -164,7 +165,7 @@ const filterTimelines = (state, relationship, statuses) => { return; } - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); + references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')).valueSeq().toJSON(); state = deleteStatus(state, status.get('id'), references, relationship.id); }); @@ -207,8 +208,8 @@ export default function timelines(state = initialState, action) { return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references, action.reblogOf); + case timelineDelete.type: + return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf); case TIMELINE_CLEAR: return clearTimeline(state, action.timeline); case blockAccountSuccess.type: @@ -220,11 +221,11 @@ export default function timelines(state = initialState, action) { return updateTop(state, action.timeline, action.top); case TIMELINE_CONNECT: return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems)); - case TIMELINE_DISCONNECT: + case disconnectTimeline.type: return state.update( - action.timeline, + action.payload.timeline, initialTimeline, - map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), + map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), ); case TIMELINE_MARK_AS_PARTIAL: return state.update( diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js index 3eb80da437..0ea3debc6c 100644 --- a/app/javascript/flavours/glitch/reducers/user_lists.js +++ b/app/javascript/flavours/glitch/reducers/user_lists.js @@ -1,12 +1,8 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { - DIRECTORY_FETCH_REQUEST, - DIRECTORY_FETCH_SUCCESS, - DIRECTORY_FETCH_FAIL, - DIRECTORY_EXPAND_REQUEST, - DIRECTORY_EXPAND_SUCCESS, - DIRECTORY_EXPAND_FAIL, + expandDirectory, + fetchDirectory } from 'flavours/glitch/actions/directory'; import { FEATURED_TAGS_FETCH_REQUEST, @@ -117,6 +113,7 @@ const normalizeFeaturedTags = (state, path, featuredTags, accountId) => { })); }; +/** @type {import('@reduxjs/toolkit').Reducer} */ export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: @@ -194,16 +191,6 @@ export default function userLists(state = initialState, action) { case MUTES_FETCH_FAIL: case MUTES_EXPAND_FAIL: return state.setIn(['mutes', 'isLoading'], false); - case DIRECTORY_FETCH_SUCCESS: - return normalizeList(state, ['directory'], action.accounts, action.next); - case DIRECTORY_EXPAND_SUCCESS: - return appendToList(state, ['directory'], action.accounts, action.next); - case DIRECTORY_FETCH_REQUEST: - case DIRECTORY_EXPAND_REQUEST: - return state.setIn(['directory', 'isLoading'], true); - case DIRECTORY_FETCH_FAIL: - case DIRECTORY_EXPAND_FAIL: - return state.setIn(['directory', 'isLoading'], false); case FEATURED_TAGS_FETCH_SUCCESS: return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id); case FEATURED_TAGS_FETCH_REQUEST: @@ -211,6 +198,17 @@ export default function userLists(state = initialState, action) { case FEATURED_TAGS_FETCH_FAIL: return state.setIn(['featured_tags', action.id, 'isLoading'], false); default: - return state; + if(fetchDirectory.fulfilled.match(action)) + return normalizeList(state, ['directory'], action.payload.accounts, undefined); + else if( expandDirectory.fulfilled.match(action)) + return appendToList(state, ['directory'], action.payload.accounts, undefined); + else if(fetchDirectory.pending.match(action) || + expandDirectory.pending.match(action)) + return state.setIn(['directory', 'isLoading'], true); + else if(fetchDirectory.rejected.match(action) || + expandDirectory.rejected.match(action)) + return state.setIn(['directory', 'isLoading'], false); + else + return state; } } diff --git a/app/javascript/flavours/glitch/store/typed_functions.ts b/app/javascript/flavours/glitch/store/typed_functions.ts index dae37e6225..e5820149db 100644 --- a/app/javascript/flavours/glitch/store/typed_functions.ts +++ b/app/javascript/flavours/glitch/store/typed_functions.ts @@ -82,13 +82,19 @@ export function createThunk( const discardLoadDataInPayload = Symbol('discardLoadDataInPayload'); type DiscardLoadData = typeof discardLoadDataInPayload; -type OnData = ( +type OnData = ( data: LoadDataResult, api: AppThunkApi & { + actionArg: ActionArg; discardLoadData: DiscardLoadData; }, ) => ReturnedData | DiscardLoadData | Promise; +type LoadData = ( + args: Args, + api: AppThunkApi, +) => Promise; + type ArgsType = Record | undefined; // Overload when there is no `onData` method, the payload is the `onData` result @@ -101,18 +107,18 @@ export function createDataLoadingThunk( // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty export function createDataLoadingThunk( name: string, - loadData: (args: Args) => Promise, + loadData: LoadData, onDataOrThunkOptions?: | AppThunkOptions - | OnData, + | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns nothing, then the mayload is the `onData` result export function createDataLoadingThunk( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; @@ -123,8 +129,10 @@ export function createDataLoadingThunk< Returned, >( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: + | AppThunkOptions + | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; @@ -159,11 +167,13 @@ export function createDataLoadingThunk< Returned, >( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: + | AppThunkOptions + | OnData, maybeThunkOptions?: AppThunkOptions, ) { - let onData: OnData | undefined; + let onData: OnData | undefined; let thunkOptions: AppThunkOptions | undefined; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; @@ -177,7 +187,10 @@ export function createDataLoadingThunk< return createThunk( name, async (arg, { getState, dispatch }) => { - const data = await loadData(arg); + const data = await loadData(arg, { + dispatch, + getState, + }); if (!onData) return data as Returned; @@ -185,6 +198,7 @@ export function createDataLoadingThunk< dispatch, getState, discardLoadData: discardLoadDataInPayload, + actionArg: arg, }); // if there is no return in `onData`, we return the `onData` result diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 23c07f6a7b..c803133e39 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -120,8 +120,27 @@ text-decoration: none; } - &:disabled { - opacity: 0.5; + &.button--destructive { + &:active, + &:focus, + &:hover { + border-color: $ui-button-destructive-focus-background-color; + color: $ui-button-destructive-focus-background-color; + } + } + + &:disabled, + &.disabled { + opacity: 0.7; + border-color: $ui-primary-color; + color: $ui-primary-color; + + &:active, + &:focus, + &:hover { + border-color: $ui-primary-color; + color: $ui-primary-color; + } } } @@ -952,9 +971,15 @@ body > [data-popper-placement] { padding: 10px; p { + font-size: 15px; + line-height: 22px; color: $darker-text-color; margin-bottom: 20px; + strong { + font-weight: 700; + } + a { color: $secondary-text-color; text-decoration: none; @@ -1472,7 +1497,7 @@ body > [data-popper-placement] { .status__action-bar, .reactions-bar { margin-inline-start: $thread-margin; - width: calc(100% - ($thread-margin)); + width: calc(100% - $thread-margin); } .status__content__read-more-button { @@ -1517,74 +1542,49 @@ body > [data-popper-placement] { } } } +} - &.collapsed { +.status__wrapper.collapsed { + .status { background-position: center; background-size: cover; user-select: none; min-height: 0; + } - &.has-background::before { - display: block; - position: absolute; - inset-inline-start: 0; - inset-inline-end: 0; - top: 0; - bottom: 0; - background-image: linear-gradient( - to bottom, - rgba($base-shadow-color, 0.75), - rgba($base-shadow-color, 0.65) 24px, - rgba($base-shadow-color, 0.8) - ); - pointer-events: none; - content: ''; - } + &.has-background::before { + display: block; + position: absolute; + inset-inline-start: 0; + inset-inline-end: 0; + top: 0; + bottom: 0; + background-image: linear-gradient( + to bottom, + rgba($base-shadow-color, 0.75), + rgba($base-shadow-color, 0.65) 24px, + rgba($base-shadow-color, 0.8) + ); + pointer-events: none; + content: ''; + } - .display-name:hover .display-name__html { + .display-name:hover .display-name__html { + text-decoration: none; + } + + .status__content { + height: 20px; + overflow: hidden; + text-overflow: ellipsis; + padding-top: 0; + mask-image: linear-gradient(rgb(0 0 0 / 100%), transparent); + + a:hover { text-decoration: none; } - - .status__content { - height: 20px; - overflow: hidden; - text-overflow: ellipsis; - padding-top: 0; - - &::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - inset-inline-start: 0; - inset-inline-end: 0; - background: linear-gradient(transparent, var(--background-color)); - pointer-events: none; - } - - a:hover { - text-decoration: none; - } - } - - &:focus > .status__content::after { - background: linear-gradient( - rgba(lighten($ui-base-color, 4%), 0), - rgba(lighten($ui-base-color, 4%), 1) - ); - } - - // TODO: review - &.status-direct > .status__content::after { - background: linear-gradient( - rgba(mix($ui-base-color, $ui-highlight-color, 95%), 0), - rgba(mix($ui-base-color, $ui-highlight-color, 95%), 1) - ); - } } -} -.status__wrapper.collapsed { .notification__message { margin-bottom: 0; white-space: nowrap; @@ -1799,10 +1799,6 @@ body > [data-popper-placement] { .status__wrapper-direct { background: rgba($ui-highlight-color, 0.05); - &:focus { - background: rgba($ui-highlight-color, 0.05); - } - .status__prepend { color: $highlight-text-color; } @@ -2636,7 +2632,7 @@ a.account__display-name { } .dropdown-animation { - animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1); + animation: dropdown 250ms cubic-bezier(0.1, 0.7, 0.1, 1); @keyframes dropdown { from { @@ -4379,6 +4375,13 @@ a.status-card { border-end-start-radius: 0; } +.status-card.bottomless .status-card__image, +.status-card.bottomless .status-card__image-image, +.status-card.bottomless .status-card__image-preview { + border-end-end-radius: 0; + border-end-start-radius: 0; +} + .status-card.expanded > a { width: 100%; } @@ -9391,43 +9394,80 @@ noscript { display: flex; align-items: center; color: $primary-text-color; - text-decoration: none; - padding: 15px; + padding: 16px; border-bottom: 1px solid var(--background-border-color); - gap: 15px; + gap: 16px; &:last-child { border-bottom: 0; } - &:hover, - &:active, - &:focus { - color: $highlight-text-color; - - .story__details__publisher, - .story__details__shared { - color: $highlight-text-color; - } - } - &__details { flex: 1 1 auto; &__publisher { color: $darker-text-color; margin-bottom: 8px; + font-size: 14px; + line-height: 20px; } &__title { + display: block; font-size: 19px; line-height: 24px; font-weight: 500; margin-bottom: 8px; + text-decoration: none; + color: $primary-text-color; + + &:hover, + &:active, + &:focus { + color: $highlight-text-color; + } } &__shared { + display: flex; + align-items: center; color: $darker-text-color; + gap: 8px; + justify-content: space-between; + font-size: 14px; + line-height: 20px; + + & > span { + display: flex; + align-items: center; + gap: 4px; + } + + &__pill { + background: var(--surface-variant-background-color); + border-radius: 4px; + color: inherit; + text-decoration: none; + padding: 4px 12px; + font-size: 12px; + font-weight: 500; + line-height: 16px; + } + + &__author-link { + display: inline-flex; + align-items: center; + gap: 4px; + color: $primary-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:active, + &:focus { + color: $highlight-text-color; + } + } } strong { @@ -9492,14 +9532,14 @@ noscript { } .server-banner { - padding: 20px 0; - &__introduction { + font-size: 15px; + line-height: 22px; color: $darker-text-color; margin-bottom: 20px; strong { - font-weight: 600; + font-weight: 700; } a { @@ -9527,6 +9567,9 @@ noscript { } &__description { + font-size: 15px; + line-height: 22px; + color: $darker-text-color; margin-bottom: 20px; } @@ -10498,14 +10541,14 @@ noscript { color: inherit; text-decoration: none; padding: 4px 12px; - background: $ui-base-color; + background: var(--surface-variant-background-color); border-radius: 4px; font-weight: 500; &:hover, &:focus, &:active { - background: lighten($ui-base-color, 4%); + background: var(--surface-variant-active-background-color); } } @@ -10836,6 +10879,7 @@ noscript { } .more-from-author { + box-sizing: border-box; font-size: 14px; color: $darker-text-color; background: var(--surface-background-color); @@ -10873,3 +10917,158 @@ noscript { } } } + +.hover-card-controller[data-popper-reference-hidden='true'] { + opacity: 0; + pointer-events: none; +} + +.hover-card { + box-shadow: var(--dropdown-shadow); + background: var(--modal-background-color); + backdrop-filter: var(--background-filter); + border: 1px solid var(--modal-border-color); + border-radius: 8px; + padding: 16px; + width: 270px; + display: flex; + flex-direction: column; + gap: 12px; + + &--loading { + position: relative; + min-height: 100px; + } + + &__name { + display: flex; + gap: 12px; + text-decoration: none; + color: inherit; + } + + &__number { + font-size: 15px; + line-height: 22px; + color: $secondary-text-color; + + strong { + font-weight: 700; + } + } + + &__text-row { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__bio { + color: $secondary-text-color; + font-size: 14px; + line-height: 20px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + max-height: 2 * 20px; + overflow: hidden; + + p { + margin-bottom: 0; + } + + a { + color: inherit; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + } + + .display-name { + font-size: 15px; + line-height: 22px; + + bdi { + font-weight: 500; + color: $primary-text-color; + } + + &__account { + display: block; + color: $dark-text-color; + } + } + + .account-fields { + color: $secondary-text-color; + font-size: 14px; + line-height: 20px; + + a { + color: inherit; + text-decoration: none; + + &:focus, + &:hover, + &:active { + text-decoration: underline; + } + } + + dl { + display: flex; + align-items: center; + gap: 4px; + + dt { + flex: 0 0 auto; + color: $dark-text-color; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + dd { + flex: 1 1 auto; + font-weight: 500; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-align: end; + } + + &.verified { + dd { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + overflow: hidden; + white-space: nowrap; + color: $valid-value-color; + + & > span { + overflow: hidden; + text-overflow: ellipsis; + } + + a { + font-weight: 500; + } + + .icon { + width: 16px; + height: 16px; + } + } + } + } + } +} diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss index 09a75a834b..9f571b3f26 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -56,11 +56,13 @@ $account-background-color: $white !default; $emojis-requiring-inversion: 'chains'; -.theme-mastodon-light { +body { --dropdown-border-color: #d9e1e8; --dropdown-background-color: #fff; + --modal-border-color: #d9e1e8; + --modal-background-color: var(--background-color-tint); --background-border-color: #d9e1e8; --background-color: #fff; - --background-color-tint: rgba(255, 255, 255, 90%); + --background-color-tint: rgba(255, 255, 255, 80%); --background-filter: blur(10px); } diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 60e354dfcc..ab945f6d26 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -112,4 +112,6 @@ $dismiss-overlay-width: 4rem; --background-color: #{darken($ui-base-color, 8%)}; --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)}; --surface-background-color: #{darken($ui-base-color, 4%)}; + --surface-variant-background-color: #{$ui-base-color}; + --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; } diff --git a/app/javascript/hooks/useLinks.ts b/app/javascript/hooks/useLinks.ts new file mode 100644 index 0000000000..f08b9500da --- /dev/null +++ b/app/javascript/hooks/useLinks.ts @@ -0,0 +1,61 @@ +import { useCallback } from 'react'; + +import { useHistory } from 'react-router-dom'; + +import { openURL } from 'mastodon/actions/search'; +import { useAppDispatch } from 'mastodon/store'; + +const isMentionClick = (element: HTMLAnchorElement) => + element.classList.contains('mention'); + +const isHashtagClick = (element: HTMLAnchorElement) => + element.textContent?.[0] === '#' || + element.previousSibling?.textContent?.endsWith('#'); + +export const useLinks = () => { + const history = useHistory(); + const dispatch = useAppDispatch(); + + const handleHashtagClick = useCallback( + (element: HTMLAnchorElement) => { + const { textContent } = element; + + if (!textContent) return; + + history.push(`/tags/${textContent.replace(/^#/, '')}`); + }, + [history], + ); + + const handleMentionClick = useCallback( + (element: HTMLAnchorElement) => { + dispatch( + openURL(element.href, history, () => { + window.location.href = element.href; + }), + ); + }, + [dispatch, history], + ); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const target = (e.target as HTMLElement).closest('a'); + + if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) { + return; + } + + if (isMentionClick(target)) { + e.preventDefault(); + handleMentionClick(target); + } else if (isHashtagClick(target)) { + e.preventDefault(); + handleHashtagClick(target); + } + }, + [handleMentionClick, handleHashtagClick], + ); + + return handleClick; +}; diff --git a/app/javascript/hooks/useTimeout.ts b/app/javascript/hooks/useTimeout.ts new file mode 100644 index 0000000000..bb1e8848dd --- /dev/null +++ b/app/javascript/hooks/useTimeout.ts @@ -0,0 +1,44 @@ +import { useRef, useCallback, useEffect } from 'react'; + +export const useTimeout = () => { + const timeoutRef = useRef>(); + const callbackRef = useRef<() => void>(); + + const set = useCallback((callback: () => void, delay: number) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + callbackRef.current = callback; + timeoutRef.current = setTimeout(callback, delay); + }, []); + + const delay = useCallback((delay: number) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (!callbackRef.current) { + return; + } + + timeoutRef.current = setTimeout(callbackRef.current, delay); + }, []); + + const cancel = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = undefined; + callbackRef.current = undefined; + } + }, []); + + useEffect( + () => () => { + cancel(); + }, + [cancel], + ); + + return [set, cancel, delay] as const; +}; diff --git a/app/javascript/mastodon/actions/directory.js b/app/javascript/mastodon/actions/directory.js deleted file mode 100644 index 7a0748029d..0000000000 --- a/app/javascript/mastodon/actions/directory.js +++ /dev/null @@ -1,62 +0,0 @@ -import api from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST'; -export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS'; -export const DIRECTORY_FETCH_FAIL = 'DIRECTORY_FETCH_FAIL'; - -export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST'; -export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS'; -export const DIRECTORY_EXPAND_FAIL = 'DIRECTORY_EXPAND_FAIL'; - -export const fetchDirectory = params => (dispatch) => { - dispatch(fetchDirectoryRequest()); - - api().get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(fetchDirectorySuccess(data)); - dispatch(fetchRelationships(data.map(x => x.id))); - }).catch(error => dispatch(fetchDirectoryFail(error))); -}; - -export const fetchDirectoryRequest = () => ({ - type: DIRECTORY_FETCH_REQUEST, -}); - -export const fetchDirectorySuccess = accounts => ({ - type: DIRECTORY_FETCH_SUCCESS, - accounts, -}); - -export const fetchDirectoryFail = error => ({ - type: DIRECTORY_FETCH_FAIL, - error, -}); - -export const expandDirectory = params => (dispatch, getState) => { - dispatch(expandDirectoryRequest()); - - const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size; - - api().get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => { - dispatch(importFetchedAccounts(data)); - dispatch(expandDirectorySuccess(data)); - dispatch(fetchRelationships(data.map(x => x.id))); - }).catch(error => dispatch(expandDirectoryFail(error))); -}; - -export const expandDirectoryRequest = () => ({ - type: DIRECTORY_EXPAND_REQUEST, -}); - -export const expandDirectorySuccess = accounts => ({ - type: DIRECTORY_EXPAND_SUCCESS, - accounts, -}); - -export const expandDirectoryFail = error => ({ - type: DIRECTORY_EXPAND_FAIL, - error, -}); diff --git a/app/javascript/mastodon/actions/directory.ts b/app/javascript/mastodon/actions/directory.ts new file mode 100644 index 0000000000..34ac309c66 --- /dev/null +++ b/app/javascript/mastodon/actions/directory.ts @@ -0,0 +1,37 @@ +import type { List as ImmutableList } from 'immutable'; + +import { apiGetDirectory } from 'mastodon/api/directory'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +import { fetchRelationships } from './accounts'; +import { importFetchedAccounts } from './importer'; + +export const fetchDirectory = createDataLoadingThunk( + 'directory/fetch', + async (params: Parameters[0]) => + apiGetDirectory(params), + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((x) => x.id))); + + return { accounts: data }; + }, +); + +export const expandDirectory = createDataLoadingThunk( + 'directory/expand', + async (params: Parameters[0], { getState }) => { + const loadedItems = getState().user_lists.getIn([ + 'directory', + 'items', + ]) as ImmutableList; + + return apiGetDirectory({ ...params, offset: loadedItems.size }, 20); + }, + (data, { dispatch }) => { + dispatch(importFetchedAccounts(data)); + dispatch(fetchRelationships(data.map((x) => x.id))); + + return { accounts: data }; + }, +); diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 16f191b584..516a7a7973 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -68,13 +68,17 @@ export function importFetchedStatuses(statuses) { status.filtered.forEach(result => pushUnique(filters, result.filter)); } - if (status.reblog && status.reblog.id) { + if (status.reblog?.id) { processStatus(status.reblog); } - if (status.poll && status.poll.id) { + if (status.poll?.id) { pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } + + if (status.card) { + status.card.authors.forEach(author => author.account && pushUnique(accounts, author.account)); + } } statuses.forEach(processStatus); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index b5a30343e4..c09a3f442c 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -36,6 +36,17 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + if (status.card) { + normalStatus.card = { + ...status.card, + authors: status.card.authors.map(author => ({ + ...author, + accountId: author.account?.id, + account: undefined, + })), + }; + } + if (status.filtered) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } diff --git a/app/javascript/mastodon/actions/notification_policies.ts b/app/javascript/mastodon/actions/notification_policies.ts new file mode 100644 index 0000000000..fcc9919c49 --- /dev/null +++ b/app/javascript/mastodon/actions/notification_policies.ts @@ -0,0 +1,16 @@ +import { + apiGetNotificationPolicy, + apiUpdateNotificationsPolicy, +} from 'mastodon/api/notification_policies'; +import type { NotificationPolicy } from 'mastodon/models/notification_policy'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +export const fetchNotificationPolicy = createDataLoadingThunk( + 'notificationPolicy/fetch', + () => apiGetNotificationPolicy(), +); + +export const updateNotificationsPolicy = createDataLoadingThunk( + 'notificationPolicy/update', + (policy: Partial) => apiUpdateNotificationsPolicy(policy), +); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 55d079a3a1..13e09a4bba 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -44,10 +44,6 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; -export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST'; -export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS'; -export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL'; - export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST'; export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS'; export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL'; @@ -347,40 +343,6 @@ export function setBrowserPermission (value) { }; } -export const fetchNotificationPolicy = () => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().get('/api/v1/notifications/policy').then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - -export const fetchNotificationPolicyRequest = () => ({ - type: NOTIFICATION_POLICY_FETCH_REQUEST, -}); - -export const fetchNotificationPolicySuccess = policy => ({ - type: NOTIFICATION_POLICY_FETCH_SUCCESS, - policy, -}); - -export const fetchNotificationPolicyFail = error => ({ - type: NOTIFICATION_POLICY_FETCH_FAIL, - error, -}); - -export const updateNotificationsPolicy = params => (dispatch) => { - dispatch(fetchNotificationPolicyRequest()); - - api().put('/api/v1/notifications/policy', params).then(({ data }) => { - dispatch(fetchNotificationPolicySuccess(data)); - }).catch(err => { - dispatch(fetchNotificationPolicyFail(err)); - }); -}; - export const fetchNotificationRequests = () => (dispatch, getState) => { const params = {}; diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 9daeb3c60f..e7fe1c53ed 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -77,7 +77,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }, onDisconnect() { - dispatch(disconnectTimeline(timelineId)); + dispatch(disconnectTimeline({ timeline: timelineId })); if (options.fallback) { // @ts-expect-error diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index dc37cdf1f1..f0ea46118e 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -6,9 +6,11 @@ import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { importFetchedStatus, importFetchedStatuses } from './importer'; import { submitMarkers } from './markers'; +import {timelineDelete} from './timelines_typed'; + +export { disconnectTimeline } from './timelines_typed'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; @@ -17,7 +19,6 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL'; @@ -62,16 +63,10 @@ export function updateTimeline(timeline, status, accept) { export function deleteFromTimelines(id) { return (dispatch, getState) => { const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')).valueSeq().toJSON(); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); - dispatch({ - type: TIMELINE_DELETE, - id, - accountId, - references, - reblogOf, - }); + dispatch(timelineDelete({ statusId: id, accountId, references, reblogOf })); }; } @@ -163,6 +158,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +export const expandLinkTimeline = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, @@ -225,12 +221,6 @@ export function connectTimeline(timeline) { }; } -export const disconnectTimeline = timeline => ({ - type: TIMELINE_DISCONNECT, - timeline, - usePendingItems: preferPendingItems, -}); - export const markAsPartial = timeline => ({ type: TIMELINE_MARK_AS_PARTIAL, timeline, diff --git a/app/javascript/mastodon/actions/timelines_typed.ts b/app/javascript/mastodon/actions/timelines_typed.ts new file mode 100644 index 0000000000..07d82b2f01 --- /dev/null +++ b/app/javascript/mastodon/actions/timelines_typed.ts @@ -0,0 +1,20 @@ +import { createAction } from '@reduxjs/toolkit'; + +import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; + +export const disconnectTimeline = createAction( + 'timeline/disconnect', + ({ timeline }: { timeline: string }) => ({ + payload: { + timeline, + usePendingItems: preferPendingItems, + }, + }), +); + +export const timelineDelete = createAction<{ + statusId: string; + accountId: string; + references: string[]; + reblogOf: string | null; +}>('timelines/delete'); diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 0b840b41ce..0bdf17a5d2 100644 --- a/app/javascript/mastodon/actions/trends.js +++ b/app/javascript/mastodon/actions/trends.js @@ -1,6 +1,6 @@ import api, { getLinks } from '../api'; -import { importFetchedStatuses } from './importer'; +import { importFetchedStatuses, importFetchedAccounts } from './importer'; export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST'; export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS'; @@ -49,8 +49,11 @@ export const fetchTrendingLinks = () => (dispatch) => { dispatch(fetchTrendingLinksRequest()); api() - .get('/api/v1/trends/links') - .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data))) + .get('/api/v1/trends/links', { params: { limit: 20 } }) + .then(({ data }) => { + dispatch(importFetchedAccounts(data.flatMap(link => link.authors.map(author => author.account)).filter(account => !!account))); + dispatch(fetchTrendingLinksSuccess(data)); + }) .catch(err => dispatch(fetchTrendingLinksFail(err))); }; diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index e133125a29..24672290c7 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -59,16 +59,49 @@ export default function api(withAuthorization = true) { }); } +type RequestParamsOrData = Record; + export async function apiRequest( method: Method, url: string, - params?: Record, + args: { + params?: RequestParamsOrData; + data?: RequestParamsOrData; + } = {}, ) { const { data } = await api().request({ method, url: '/api/' + url, - data: params, + ...args, }); return data; } + +export async function apiRequestGet( + url: string, + params?: RequestParamsOrData, +) { + return apiRequest('GET', url, { params }); +} + +export async function apiRequestPost( + url: string, + data?: RequestParamsOrData, +) { + return apiRequest('POST', url, { data }); +} + +export async function apiRequestPut( + url: string, + data?: RequestParamsOrData, +) { + return apiRequest('PUT', url, { data }); +} + +export async function apiRequestDelete( + url: string, + params?: RequestParamsOrData, +) { + return apiRequest('DELETE', url, { params }); +} diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 3d89e44b26..bd1757e827 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -1,7 +1,7 @@ -import { apiRequest } from 'mastodon/api'; +import { apiRequestPost } from 'mastodon/api'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; export const apiSubmitAccountNote = (id: string, value: string) => - apiRequest('post', `v1/accounts/${id}/note`, { + apiRequestPost(`v1/accounts/${id}/note`, { comment: value, }); diff --git a/app/javascript/mastodon/api/directory.ts b/app/javascript/mastodon/api/directory.ts new file mode 100644 index 0000000000..cd39f8f269 --- /dev/null +++ b/app/javascript/mastodon/api/directory.ts @@ -0,0 +1,15 @@ +import { apiRequestGet } from 'mastodon/api'; +import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; + +export const apiGetDirectory = ( + params: { + order: string; + local: boolean; + offset?: number; + }, + limit = 20, +) => + apiRequestGet('v1/directory', { + ...params, + limit, + }); diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts index 4c466a1b46..118b5f06d2 100644 --- a/app/javascript/mastodon/api/interactions.ts +++ b/app/javascript/mastodon/api/interactions.ts @@ -1,10 +1,10 @@ -import { apiRequest } from 'mastodon/api'; +import { apiRequestPost } from 'mastodon/api'; import type { Status, StatusVisibility } from 'mastodon/models/status'; export const apiReblog = (statusId: string, visibility: StatusVisibility) => - apiRequest<{ reblog: Status }>('post', `v1/statuses/${statusId}/reblog`, { + apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, { visibility, }); export const apiUnreblog = (statusId: string) => - apiRequest('post', `v1/statuses/${statusId}/unreblog`); + apiRequestPost(`v1/statuses/${statusId}/unreblog`); diff --git a/app/javascript/mastodon/api/notification_policies.ts b/app/javascript/mastodon/api/notification_policies.ts new file mode 100644 index 0000000000..4032134fb5 --- /dev/null +++ b/app/javascript/mastodon/api/notification_policies.ts @@ -0,0 +1,9 @@ +import { apiRequestGet, apiRequestPut } from 'mastodon/api'; +import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies'; + +export const apiGetNotificationPolicy = () => + apiRequestGet('/v1/notifications/policy'); + +export const apiUpdateNotificationsPolicy = ( + policy: Partial, +) => apiRequestPut('/v1/notifications/policy', policy); diff --git a/app/javascript/mastodon/api_types/notification_policies.ts b/app/javascript/mastodon/api_types/notification_policies.ts new file mode 100644 index 0000000000..0f4a2d132e --- /dev/null +++ b/app/javascript/mastodon/api_types/notification_policies.ts @@ -0,0 +1,12 @@ +// See app/serializers/rest/notification_policy_serializer.rb + +export interface NotificationPolicyJSON { + filter_not_following: boolean; + filter_not_followers: boolean; + filter_new_accounts: boolean; + filter_private_mentions: boolean; + summary: { + pending_requests_count: number; + pending_notifications_count: number; + }; +} diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index c7dd33b5da..a934faeb7a 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -30,6 +30,12 @@ export interface ApiMentionJSON { acct: string; } +export interface ApiPreviewCardAuthorJSON { + name: string; + url: string; + account?: ApiAccountJSON; +} + export interface ApiPreviewCardJSON { url: string; title: string; @@ -38,6 +44,7 @@ export interface ApiPreviewCardJSON { type: string; author_name: string; author_url: string; + author_account?: ApiAccountJSON; provider_name: string; provider_url: string; html: string; @@ -48,6 +55,7 @@ export interface ApiPreviewCardJSON { embed_url: string; blurhash: string; published_at: string; + authors: ApiPreviewCardAuthorJSON[]; } export interface ApiStatusJSON { diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx new file mode 100644 index 0000000000..9d523c7402 --- /dev/null +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -0,0 +1,20 @@ +import { useLinks } from 'mastodon/../hooks/useLinks'; + +export const AccountBio: React.FC<{ + note: string; + className: string; +}> = ({ note, className }) => { + const handleClick = useLinks(); + + if (note.length === 0 || note === '

') { + return null; + } + + return ( +
+ ); +}; diff --git a/app/javascript/mastodon/components/account_fields.tsx b/app/javascript/mastodon/components/account_fields.tsx new file mode 100644 index 0000000000..e297f99e3a --- /dev/null +++ b/app/javascript/mastodon/components/account_fields.tsx @@ -0,0 +1,42 @@ +import classNames from 'classnames'; + +import CheckIcon from '@/material-icons/400-24px/check.svg?react'; +import { useLinks } from 'mastodon/../hooks/useLinks'; +import { Icon } from 'mastodon/components/icon'; +import type { Account } from 'mastodon/models/account'; + +export const AccountFields: React.FC<{ + fields: Account['fields']; + limit: number; +}> = ({ fields, limit = -1 }) => { + const handleClick = useLinks(); + + if (fields.size === 0) { + return null; + } + + return ( +
+ {fields.take(limit).map((pair, i) => ( +
+
+ +
+ {pair.get('verified_at') && ( + + )} + +
+
+ ))} +
+ ); +}; diff --git a/app/javascript/mastodon/components/column_header.jsx b/app/javascript/mastodon/components/column_header.jsx deleted file mode 100644 index 42183f336d..0000000000 --- a/app/javascript/mastodon/components/column_header.jsx +++ /dev/null @@ -1,233 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent, useCallback } from 'react'; - -import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl'; - -import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; - -import AddIcon from '@/material-icons/400-24px/add.svg?react'; -import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; -import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; -import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; -import { Icon } from 'mastodon/components/icon'; -import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; -import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { WithRouterPropTypes } from 'mastodon/utils/react_router'; - - -import { useAppHistory } from './router'; - -const messages = defineMessages({ - show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, - hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, - moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, - moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, - back: { id: 'column_back_button.label', defaultMessage: 'Back' }, -}); - -const BackButton = ({ onlyIcon }) => { - const history = useAppHistory(); - const intl = useIntl(); - - const handleBackClick = useCallback(() => { - if (history.location?.state?.fromMastodon) { - history.goBack(); - } else { - history.push('/'); - } - }, [history]); - - return ( - - ); -}; - -BackButton.propTypes = { - onlyIcon: PropTypes.bool, -}; - -class ColumnHeader extends PureComponent { - static propTypes = { - identity: identityContextPropShape, - intl: PropTypes.object.isRequired, - title: PropTypes.node, - icon: PropTypes.string, - iconComponent: PropTypes.func, - active: PropTypes.bool, - multiColumn: PropTypes.bool, - extraButton: PropTypes.node, - showBackButton: PropTypes.bool, - children: PropTypes.node, - pinned: PropTypes.bool, - placeholder: PropTypes.bool, - onPin: PropTypes.func, - onMove: PropTypes.func, - onClick: PropTypes.func, - appendContent: PropTypes.node, - collapseIssues: PropTypes.bool, - ...WithRouterPropTypes, - }; - - state = { - collapsed: true, - animating: false, - }; - - handleToggleClick = (e) => { - e.stopPropagation(); - this.setState({ collapsed: !this.state.collapsed, animating: true }); - }; - - handleTitleClick = () => { - this.props.onClick?.(); - }; - - handleMoveLeft = () => { - this.props.onMove(-1); - }; - - handleMoveRight = () => { - this.props.onMove(1); - }; - - handleTransitionEnd = () => { - this.setState({ animating: false }); - }; - - handlePin = () => { - if (!this.props.pinned) { - this.props.history.replace('/'); - } - - this.props.onPin(); - }; - - render () { - const { title, icon, iconComponent, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues, history } = this.props; - const { collapsed, animating } = this.state; - - const wrapperClassName = classNames('column-header__wrapper', { - 'active': active, - }); - - const buttonClassName = classNames('column-header', { - 'active': active, - }); - - const collapsibleClassName = classNames('column-header__collapsible', { - 'collapsed': collapsed, - 'animating': animating, - }); - - const collapsibleButtonClassName = classNames('column-header__button', { - 'active': !collapsed, - }); - - let extraContent, pinButton, moveButtons, backButton, collapseButton; - - if (children) { - extraContent = ( -
- {children} -
- ); - } - - if (multiColumn && pinned) { - pinButton = ; - - moveButtons = ( -
- - -
- ); - } else if (multiColumn && this.props.onPin) { - pinButton = ; - } - - if (history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || showBackButton)) { - backButton = ; - } - - const collapsedContent = [ - extraContent, - ]; - - if (multiColumn) { - collapsedContent.push( -
- {pinButton} - {moveButtons} -
- ); - } - - if (this.props.identity.signedIn && (children || (multiColumn && this.props.onPin))) { - collapseButton = ( - - ); - } - - const hasTitle = (icon || iconComponent) && title; - - const component = ( -
-

- {hasTitle && ( - <> - {backButton} - - - - )} - - {!hasTitle && backButton} - -
- {extraButton} - {collapseButton} -
-

- -
-
- {(!collapsed || animating) && collapsedContent} -
-
- - {appendContent} -
- ); - - if (placeholder) { - return component; - } else { - return ( - {component} - ); - } - } - -} - -export default injectIntl(withIdentity(withRouter(ColumnHeader))); diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx new file mode 100644 index 0000000000..ec946cab3e --- /dev/null +++ b/app/javascript/mastodon/components/column_header.tsx @@ -0,0 +1,301 @@ +import { useCallback, useState } from 'react'; + +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import AddIcon from '@/material-icons/400-24px/add.svg?react'; +import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react'; +import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; +import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; +import type { IconProp } from 'mastodon/components/icon'; +import { Icon } from 'mastodon/components/icon'; +import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context'; +import { useIdentity } from 'mastodon/identity_context'; + +import { useAppHistory } from './router'; + +const messages = defineMessages({ + show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, + hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, + moveLeft: { + id: 'column_header.moveLeft_settings', + defaultMessage: 'Move column to the left', + }, + moveRight: { + id: 'column_header.moveRight_settings', + defaultMessage: 'Move column to the right', + }, + back: { id: 'column_back_button.label', defaultMessage: 'Back' }, +}); + +const BackButton: React.FC<{ + onlyIcon: boolean; +}> = ({ onlyIcon }) => { + const history = useAppHistory(); + const intl = useIntl(); + + const handleBackClick = useCallback(() => { + if (history.location.state?.fromMastodon) { + history.goBack(); + } else { + history.push('/'); + } + }, [history]); + + return ( + + ); +}; + +export interface Props { + title?: string; + icon?: string; + iconComponent?: IconProp; + active?: boolean; + children?: React.ReactNode; + pinned?: boolean; + multiColumn?: boolean; + extraButton?: React.ReactNode; + showBackButton?: boolean; + placeholder?: boolean; + appendContent?: React.ReactNode; + collapseIssues?: boolean; + onClick?: () => void; + onMove?: (arg0: number) => void; + onPin?: () => void; +} + +export const ColumnHeader: React.FC = ({ + title, + icon, + iconComponent, + active, + children, + pinned, + multiColumn, + extraButton, + showBackButton, + placeholder, + appendContent, + collapseIssues, + onClick, + onMove, + onPin, +}) => { + const intl = useIntl(); + const { signedIn } = useIdentity(); + const history = useAppHistory(); + const [collapsed, setCollapsed] = useState(true); + const [animating, setAnimating] = useState(false); + + const handleToggleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + setCollapsed((value) => !value); + setAnimating(true); + }, + [setCollapsed, setAnimating], + ); + + const handleTitleClick = useCallback(() => { + onClick?.(); + }, [onClick]); + + const handleMoveLeft = useCallback(() => { + onMove?.(-1); + }, [onMove]); + + const handleMoveRight = useCallback(() => { + onMove?.(1); + }, [onMove]); + + const handleTransitionEnd = useCallback(() => { + setAnimating(false); + }, [setAnimating]); + + const handlePin = useCallback(() => { + if (!pinned) { + history.replace('/'); + } + + onPin?.(); + }, [history, pinned, onPin]); + + const wrapperClassName = classNames('column-header__wrapper', { + active, + }); + + const buttonClassName = classNames('column-header', { + active, + }); + + const collapsibleClassName = classNames('column-header__collapsible', { + collapsed, + animating, + }); + + const collapsibleButtonClassName = classNames('column-header__button', { + active: !collapsed, + }); + + let extraContent, pinButton, moveButtons, backButton, collapseButton; + + if (children) { + extraContent = ( +
+ {children} +
+ ); + } + + if (multiColumn && pinned) { + pinButton = ( + + ); + + moveButtons = ( +
+ + +
+ ); + } else if (multiColumn && onPin) { + pinButton = ( + + ); + } + + if ( + !pinned && + ((multiColumn && history.location.state?.fromMastodon) || showBackButton) + ) { + backButton = ; + } + + const collapsedContent = [extraContent]; + + if (multiColumn) { + collapsedContent.push( +
+ {pinButton} + {moveButtons} +
, + ); + } + + if (signedIn && (children || (multiColumn && onPin))) { + collapseButton = ( + + ); + } + + const hasIcon = icon && iconComponent; + const hasTitle = hasIcon && title; + + const component = ( +
+

+ {hasTitle && ( + <> + {backButton} + + + + )} + + {!hasTitle && backButton} + +
+ {extraButton} + {collapseButton} +
+

+ +
+
+ {(!collapsed || animating) && collapsedContent} +
+
+ + {appendContent} +
+ ); + + if (placeholder) { + return component; + } else { + return {component}; + } +}; + +// eslint-disable-next-line import/no-default-export +export default ColumnHeader; diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx new file mode 100644 index 0000000000..ecc4e1ee17 --- /dev/null +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -0,0 +1,128 @@ +import { useCallback, useEffect } from 'react'; + +import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; + +import { useIdentity } from '@/mastodon/identity_context'; +import { + fetchRelationships, + followAccount, + unfollowAccount, +} from 'mastodon/actions/accounts'; +import { openModal } from 'mastodon/actions/modal'; +import { Button } from 'mastodon/components/button'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { me } from 'mastodon/initial_state'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +const messages = defineMessages({ + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' }, + mutual: { id: 'account.mutual', defaultMessage: 'Mutual' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, +}); + +export const FollowButton: React.FC<{ + accountId?: string; +}> = ({ accountId }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { signedIn } = useIdentity(); + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + const relationship = useAppSelector((state) => + accountId ? state.relationships.get(accountId) : undefined, + ); + const following = relationship?.following || relationship?.requested; + + useEffect(() => { + if (accountId && signedIn) { + dispatch(fetchRelationships([accountId])); + } + }, [dispatch, accountId, signedIn]); + + const handleClick = useCallback(() => { + if (!signedIn) { + dispatch( + openModal({ + modalType: 'INTERACTION', + modalProps: { + type: 'follow', + accountId: accountId, + url: account?.url, + }, + }), + ); + } + + if (!relationship) return; + + if (accountId === me) { + return; + } else if (relationship.following || relationship.requested) { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + message: ( + @{account?.acct} }} + /> + ), + confirm: intl.formatMessage(messages.unfollow), + onConfirm: () => { + dispatch(unfollowAccount(accountId)); + }, + }, + }), + ); + } else { + dispatch(followAccount(accountId)); + } + }, [dispatch, intl, accountId, relationship, account, signedIn]); + + let label; + + if (!signedIn) { + label = intl.formatMessage(messages.follow); + } else if (accountId === me) { + label = intl.formatMessage(messages.edit_profile); + } else if (!relationship) { + label = ; + } else if (relationship.following && relationship.followed_by) { + label = intl.formatMessage(messages.mutual); + } else if (!relationship.following && relationship.followed_by) { + label = intl.formatMessage(messages.followBack); + } else if (relationship.following || relationship.requested) { + label = intl.formatMessage(messages.unfollow); + } else { + label = intl.formatMessage(messages.follow); + } + + if (accountId === me) { + return ( + + {label} + + ); + } + + return ( + + ); +}; diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx new file mode 100644 index 0000000000..8933e14a98 --- /dev/null +++ b/app/javascript/mastodon/components/hover_card_account.tsx @@ -0,0 +1,74 @@ +import { useEffect, forwardRef } from 'react'; + +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + +import { fetchAccount } from 'mastodon/actions/accounts'; +import { AccountBio } from 'mastodon/components/account_bio'; +import { AccountFields } from 'mastodon/components/account_fields'; +import { Avatar } from 'mastodon/components/avatar'; +import { FollowersCounter } from 'mastodon/components/counters'; +import { DisplayName } from 'mastodon/components/display_name'; +import { FollowButton } from 'mastodon/components/follow_button'; +import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { ShortNumber } from 'mastodon/components/short_number'; +import { domain } from 'mastodon/initial_state'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; + +export const HoverCardAccount = forwardRef< + HTMLDivElement, + { accountId?: string } +>(({ accountId }, ref) => { + const dispatch = useAppDispatch(); + + const account = useAppSelector((state) => + accountId ? state.accounts.get(accountId) : undefined, + ); + + useEffect(() => { + if (accountId && !account) { + dispatch(fetchAccount(accountId)); + } + }, [dispatch, accountId, account]); + + return ( + + ); +}); + +HoverCardAccount.displayName = 'HoverCardAccount'; diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx new file mode 100644 index 0000000000..5ca55ebde9 --- /dev/null +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -0,0 +1,176 @@ +import { useEffect, useRef, useState, useCallback } from 'react'; + +import { useLocation } from 'react-router-dom'; + +import Overlay from 'react-overlays/Overlay'; +import type { + OffsetValue, + UsePopperOptions, +} from 'react-overlays/esm/usePopper'; + +import { useTimeout } from 'mastodon/../hooks/useTimeout'; +import { HoverCardAccount } from 'mastodon/components/hover_card_account'; + +const offset = [-12, 4] as OffsetValue; +const enterDelay = 750; +const leaveDelay = 150; +const popperConfig = { strategy: 'fixed' } as UsePopperOptions; + +const isHoverCardAnchor = (element: HTMLElement) => + element.matches('[data-hover-card-account]'); + +export const HoverCardController: React.FC = () => { + const [open, setOpen] = useState(false); + const [accountId, setAccountId] = useState(); + const [anchor, setAnchor] = useState(null); + const cardRef = useRef(null); + const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); + const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); + const [setScrollTimeout] = useTimeout(); + const location = useLocation(); + + const handleClose = useCallback(() => { + cancelEnterTimeout(); + cancelLeaveTimeout(); + setOpen(false); + setAnchor(null); + }, [cancelEnterTimeout, cancelLeaveTimeout, setOpen, setAnchor]); + + useEffect(() => { + handleClose(); + }, [handleClose, location]); + + useEffect(() => { + let isScrolling = false; + let currentAnchor: HTMLElement | null = null; + + const open = (target: HTMLElement) => { + target.setAttribute('aria-describedby', 'hover-card'); + setOpen(true); + setAnchor(target); + setAccountId(target.getAttribute('data-hover-card-account') ?? undefined); + }; + + const close = () => { + currentAnchor?.removeAttribute('aria-describedby'); + currentAnchor = null; + setOpen(false); + setAnchor(null); + setAccountId(undefined); + }; + + const handleMouseEnter = (e: MouseEvent) => { + const { target } = e; + + // We've exited the window + if (!(target instanceof HTMLElement)) { + close(); + return; + } + + // We've entered an anchor + if (!isScrolling && isHoverCardAnchor(target)) { + cancelLeaveTimeout(); + + currentAnchor?.removeAttribute('aria-describedby'); + currentAnchor = target; + + setEnterTimeout(() => { + open(target); + }, enterDelay); + } + + // We've entered the hover card + if ( + !isScrolling && + (target === currentAnchor || target === cardRef.current) + ) { + cancelLeaveTimeout(); + } + }; + + const handleMouseLeave = (e: MouseEvent) => { + if (!currentAnchor) { + return; + } + + if (e.target === currentAnchor || e.target === cardRef.current) { + cancelEnterTimeout(); + + setLeaveTimeout(() => { + close(); + }, leaveDelay); + } + }; + + const handleScrollEnd = () => { + isScrolling = false; + }; + + const handleScroll = () => { + isScrolling = true; + cancelEnterTimeout(); + setScrollTimeout(handleScrollEnd, 100); + }; + + const handleMouseMove = () => { + delayEnterTimeout(enterDelay); + }; + + document.body.addEventListener('mouseenter', handleMouseEnter, { + passive: true, + capture: true, + }); + + document.body.addEventListener('mousemove', handleMouseMove, { + passive: true, + capture: false, + }); + + document.body.addEventListener('mouseleave', handleMouseLeave, { + passive: true, + capture: true, + }); + + document.addEventListener('scroll', handleScroll, { + passive: true, + capture: true, + }); + + return () => { + document.body.removeEventListener('mouseenter', handleMouseEnter); + document.body.removeEventListener('mousemove', handleMouseMove); + document.body.removeEventListener('mouseleave', handleMouseLeave); + document.removeEventListener('scroll', handleScroll); + }; + }, [ + setEnterTimeout, + setLeaveTimeout, + setScrollTimeout, + cancelEnterTimeout, + cancelLeaveTimeout, + delayEnterTimeout, + setOpen, + setAccountId, + setAnchor, + ]); + + return ( + + {({ props }) => ( +
+ +
+ )} +
+ ); +}; diff --git a/app/javascript/mastodon/components/more_from_author.jsx b/app/javascript/mastodon/components/more_from_author.jsx new file mode 100644 index 0000000000..c20e76ac45 --- /dev/null +++ b/app/javascript/mastodon/components/more_from_author.jsx @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + +import { AuthorLink } from 'mastodon/features/explore/components/author_link'; + +export const MoreFromAuthor = ({ accountId }) => ( +
+ + + + + }} /> +
+); + +MoreFromAuthor.propTypes = { + accountId: PropTypes.string.isRequired, +}; diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx index 63eec53492..baa220af5e 100644 --- a/app/javascript/mastodon/components/server_banner.jsx +++ b/app/javascript/mastodon/components/server_banner.jsx @@ -42,10 +42,12 @@ class ServerBanner extends PureComponent { return (
- {domain}, mastodon: Mastodon }} /> + {domain}, mastodon: Mastodon }} />
- + + +
{isLoading ? ( @@ -84,10 +86,6 @@ class ServerBanner extends PureComponent { )}
- -
- -
); } diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index c11609e264..491841b494 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -429,7 +429,7 @@ class Status extends ImmutablePureComponent { prepend = (
- }} /> + }} />
); @@ -450,7 +450,7 @@ class Status extends ImmutablePureComponent { prepend = (
- }} /> + }} />
); } @@ -566,7 +566,7 @@ class Status extends ImmutablePureComponent { {status.get('edited_at') && *} - +
{statusAvatar}
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 24483cf512..82135b85ca 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -116,8 +116,9 @@ class StatusContent extends PureComponent { if (mention) { link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', `@${mention.get('acct')}`); + link.removeAttribute('title'); link.setAttribute('href', `/@${mention.get('acct')}`); + link.setAttribute('data-hover-card-account', mention.get('id')); } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx index 3ed20f65eb..fee6675faa 100644 --- a/app/javascript/mastodon/components/status_list.jsx +++ b/app/javascript/mastodon/components/status_list.jsx @@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent { withCounters: PropTypes.bool, timelineId: PropTypes.string, lastId: PropTypes.string, + bindToDocument: PropTypes.bool, }; static defaultProps = { diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index b10ef6ef76..1326874e50 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -94,7 +94,7 @@ const messageForFollowButton = relationship => { return messages.mutual; } else if (!relationship.get('following') && relationship.get('followed_by')) { return messages.followBack; - } else if (relationship.get('following')) { + } else if (relationship.get('following') || relationship.get('requested')) { return messages.unfollow; } else { return messages.follow; @@ -291,10 +291,8 @@ class Header extends ImmutablePureComponent { if (me !== account.get('id')) { if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ; - } else if (account.getIn(['relationship', 'requested'])) { - actionBtn =
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index 80dd5200fc..125df412a1 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -1,61 +1,91 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useState, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { Link } from 'react-router-dom'; + + import { Blurhash } from 'mastodon/components/blurhash'; -import { accountsCountRenderer } from 'mastodon/components/hashtag'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { ShortNumber } from 'mastodon/components/short_number'; import { Skeleton } from 'mastodon/components/skeleton'; -export default class Story extends PureComponent { +import { AuthorLink } from './author_link'; - static propTypes = { - url: PropTypes.string, - title: PropTypes.string, - lang: PropTypes.string, - publisher: PropTypes.string, - publishedAt: PropTypes.string, - author: PropTypes.string, - sharedTimes: PropTypes.number, - thumbnail: PropTypes.string, - thumbnailDescription: PropTypes.string, - blurhash: PropTypes.string, - expanded: PropTypes.bool, - }; +const sharesCountRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); - state = { - thumbnailLoaded: false, - }; +export const Story = ({ + url, + title, + lang, + publisher, + publishedAt, + author, + authorAccount, + sharedTimes, + thumbnail, + thumbnailDescription, + blurhash, + expanded +}) => { + const [thumbnailLoaded, setThumbnailLoaded] = useState(false); - handleImageLoad = () => this.setState({ thumbnailLoaded: true }); + const handleImageLoad = useCallback(() => { + setThumbnailLoaded(true); + }, [setThumbnailLoaded]); - render () { - const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props; - - const { thumbnailLoaded } = this.state; - - return ( - -
-
{publisher ? {publisher} : }{publishedAt && <> · }
-
{title ? title : }
-
{author && <>{author} }} /> · }{typeof sharedTimes === 'number' ? : }
+ return ( +
+ + ); +}; -} +Story.propTypes = { + url: PropTypes.string, + title: PropTypes.string, + lang: PropTypes.string, + publisher: PropTypes.string, + publishedAt: PropTypes.string, + author: PropTypes.string, + authorAccount: PropTypes.string, + sharedTimes: PropTypes.number, + thumbnail: PropTypes.string, + thumbnailDescription: PropTypes.string, + blurhash: PropTypes.string, + expanded: PropTypes.bool, +}; diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx index 9e143b4505..035e5aaad8 100644 --- a/app/javascript/mastodon/features/explore/links.jsx +++ b/app/javascript/mastodon/features/explore/links.jsx @@ -13,7 +13,7 @@ import { DismissableBanner } from 'mastodon/components/dismissable_banner'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; -import Story from './components/story'; +import { Story } from './components/story'; const mapStateToProps = state => ({ links: state.getIn(['trends', 'links', 'items']), @@ -75,6 +75,7 @@ class Links extends PureComponent { publisher={link.get('provider_name')} publishedAt={link.get('published_at')} author={link.get('author_name')} + authorAccount={link.getIn(['authors', 0, 'account', 'id'])} sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1} thumbnail={link.get('image')} thumbnailDescription={link.get('image_description')} diff --git a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx index c39b43bade..1b8040e55b 100644 --- a/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx +++ b/app/javascript/mastodon/features/home_timeline/components/inline_follow_suggestions.jsx @@ -12,12 +12,11 @@ import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import InfoIcon from '@/material-icons/400-24px/info.svg?react'; -import { followAccount, unfollowAccount } from 'mastodon/actions/accounts'; import { changeSetting } from 'mastodon/actions/settings'; import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions'; import { Avatar } from 'mastodon/components/avatar'; -import { Button } from 'mastodon/components/button'; import { DisplayName } from 'mastodon/components/display_name'; +import { FollowButton } from 'mastodon/components/follow_button'; import { Icon } from 'mastodon/components/icon'; import { IconButton } from 'mastodon/components/icon_button'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; @@ -79,18 +78,8 @@ Source.propTypes = { const Card = ({ id, sources }) => { const intl = useIntl(); const account = useSelector(state => state.getIn(['accounts', id])); - const relationship = useSelector(state => state.getIn(['relationships', id])); const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at')); const dispatch = useDispatch(); - const following = relationship?.get('following') ?? relationship?.get('requested'); - - const handleFollow = useCallback(() => { - if (following) { - dispatch(unfollowAccount(id)); - } else { - dispatch(followAccount(id)); - } - }, [id, following, dispatch]); const handleDismiss = useCallback(() => { dispatch(dismissSuggestion(id)); @@ -109,7 +98,7 @@ const Card = ({ id, sources }) => { {firstVerifiedField ? : }
-
); }; diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx new file mode 100644 index 0000000000..dd726dac1a --- /dev/null +++ b/app/javascript/mastodon/features/link_timeline/index.tsx @@ -0,0 +1,76 @@ +import { useRef, useEffect, useCallback } from 'react'; + +import { Helmet } from 'react-helmet'; +import { useParams } from 'react-router-dom'; + +import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; +import { expandLinkTimeline } from 'mastodon/actions/timelines'; +import Column from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import StatusListContainer from 'mastodon/features/ui/containers/status_list_container'; +import type { Card } from 'mastodon/models/status'; +import { useAppDispatch, useAppSelector } from 'mastodon/store'; + +export const LinkTimeline: React.FC<{ + multiColumn: boolean; +}> = ({ multiColumn }) => { + const { url } = useParams<{ url: string }>(); + const decodedUrl = url ? decodeURIComponent(url) : undefined; + const dispatch = useAppDispatch(); + const columnRef = useRef(null); + const firstStatusId = useAppSelector((state) => + decodedUrl + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) + : undefined, + ); + const story = useAppSelector((state) => + firstStatusId + ? (state.statuses.getIn([firstStatusId, 'card']) as Card) + : undefined, + ); + + const handleHeaderClick = useCallback(() => { + columnRef.current?.scrollTop(); + }, []); + + const handleLoadMore = useCallback( + (maxId: string) => { + dispatch(expandLinkTimeline(decodedUrl, { maxId })); + }, + [dispatch, decodedUrl], + ); + + useEffect(() => { + dispatch(expandLinkTimeline(decodedUrl)); + }, [dispatch, decodedUrl]); + + return ( + + + + + + + {story?.title} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default LinkTimeline; diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index e375b856c9..39e394e449 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -24,7 +24,7 @@ class ColumnSettings extends PureComponent { alertsEnabled: PropTypes.bool, browserSupport: PropTypes.bool, browserPermission: PropTypes.string, - notificationPolicy: ImmutablePropTypes.map, + notificationPolicy: PropTypes.object.isRequired, onChangePolicy: PropTypes.func.isRequired, }; @@ -82,22 +82,22 @@ class ColumnSettings extends PureComponent {

- + - + - + - + diff --git a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx deleted file mode 100644 index 56da7ba626..0000000000 --- a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect } from 'react'; - -import { FormattedMessage } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import { useDispatch, useSelector } from 'react-redux'; - -import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; -import { fetchNotificationPolicy } from 'mastodon/actions/notifications'; -import { Icon } from 'mastodon/components/icon'; -import { toCappedNumber } from 'mastodon/utils/numbers'; - -export const FilteredNotificationsBanner = () => { - const dispatch = useDispatch(); - const policy = useSelector(state => state.get('notificationPolicy')); - - useEffect(() => { - dispatch(fetchNotificationPolicy()); - - const interval = setInterval(() => { - dispatch(fetchNotificationPolicy()); - }, 120000); - - return () => { - clearInterval(interval); - }; - }, [dispatch]); - - if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) === 0) { - return null; - } - - return ( - - - -
- - -
- -
-
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
- -
- - ); -}; diff --git a/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx new file mode 100644 index 0000000000..2c4b3b9717 --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/filtered_notifications_banner.tsx @@ -0,0 +1,68 @@ +import { useEffect } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react'; +import { fetchNotificationPolicy } from 'mastodon/actions/notification_policies'; +import { Icon } from 'mastodon/components/icon'; +import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { toCappedNumber } from 'mastodon/utils/numbers'; + +export const FilteredNotificationsBanner: React.FC = () => { + const dispatch = useAppDispatch(); + const policy = useAppSelector((state) => state.notificationPolicy); + + useEffect(() => { + void dispatch(fetchNotificationPolicy()); + + const interval = setInterval(() => { + void dispatch(fetchNotificationPolicy()); + }, 120000); + + return () => { + clearInterval(interval); + }; + }, [dispatch]); + + if (policy === null || policy.summary.pending_notifications_count === 0) { + return null; + } + + return ( + + + +
+ + + + + + +
+ +
+
+ {toCappedNumber(policy.summary.pending_notifications_count)} +
+ +
+ + ); +}; diff --git a/app/javascript/mastodon/features/notifications/components/notification.jsx b/app/javascript/mastodon/features/notifications/components/notification.jsx index 4cbf46ff0a..41bd47c2d0 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.jsx +++ b/app/javascript/mastodon/features/notifications/components/notification.jsx @@ -468,7 +468,7 @@ class Notification extends ImmutablePureComponent { const targetAccount = report.get('target_account'); const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') }; - const targetLink = ; + const targetLink = ; return ( @@ -491,7 +491,7 @@ class Notification extends ImmutablePureComponent { const { notification } = this.props; const account = notification.get('account'); const displayNameHtml = { __html: account.get('display_name_html') }; - const link = ; + const link = ; switch(notification.get('type')) { case 'follow': diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index de266160f8..94383d0bb5 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -4,7 +4,8 @@ import { connect } from 'react-redux'; import { showAlert } from '../../../actions/alerts'; import { openModal } from '../../../actions/modal'; -import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications'; +import { updateNotificationsPolicy } from '../../../actions/notification_policies'; +import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { changeSetting } from '../../../actions/settings'; import ColumnSettings from '../components/column_settings'; @@ -15,13 +16,16 @@ const messages = defineMessages({ permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' }, }); +/** + * @param {import('mastodon/store').RootState} state + */ const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), pushSettings: state.get('push_notifications'), alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true), browserSupport: state.getIn(['notifications', 'browserSupport']), browserPermission: state.getIn(['notifications', 'browserPermission']), - notificationPolicy: state.get('notificationPolicy'), + notificationPolicy: state.notificationPolicy, }); const mapDispatchToProps = (dispatch, { intl }) => ({ diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index c2f5703b3c..f0ae40cbc4 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -6,7 +6,6 @@ import { PureComponent } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; -import { Link } from 'react-router-dom'; import Immutable from 'immutable'; @@ -15,9 +14,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react'; import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react'; -import { Avatar } from 'mastodon/components/avatar'; import { Blurhash } from 'mastodon/components/blurhash'; import { Icon } from 'mastodon/components/icon'; +import { MoreFromAuthor } from 'mastodon/components/more_from_author'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; import { useBlurhash } from 'mastodon/initial_state'; @@ -59,20 +58,6 @@ const addAutoPlay = html => { return html; }; -const MoreFromAuthor = ({ author }) => ( -
- - - - - {author.get('display_name')} }} /> -
-); - -MoreFromAuthor.propTypes = { - author: ImmutablePropTypes.map, -}; - export default class Card extends PureComponent { static propTypes = { @@ -153,7 +138,7 @@ export default class Card extends PureComponent { const interactive = card.get('type') === 'video'; const language = card.get('language') || ''; const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive; - const showAuthor = !!card.get('author_account'); + const showAuthor = !!card.getIn(['authors', 0, 'accountId']); const description = (
@@ -259,7 +244,7 @@ export default class Card extends PureComponent { {description} - {showAuthor && } + {showAuthor && } ); } diff --git a/app/javascript/mastodon/features/status/components/detailed_status.jsx b/app/javascript/mastodon/features/status/components/detailed_status.jsx index 31bc593a1a..e3f390321a 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.jsx +++ b/app/javascript/mastodon/features/status/components/detailed_status.jsx @@ -279,7 +279,7 @@ class DetailedStatus extends ImmutablePureComponent {
)} - +
diff --git a/app/javascript/mastodon/features/ui/components/column_loading.tsx b/app/javascript/mastodon/features/ui/components/column_loading.tsx index 42174838cf..d9563dda7a 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.tsx +++ b/app/javascript/mastodon/features/ui/components/column_loading.tsx @@ -1,11 +1,8 @@ -import Column from '../../../components/column'; -import ColumnHeader from '../../../components/column_header'; +import Column from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import type { Props as ColumnHeaderProps } from 'mastodon/components/column_header'; -interface Props { - multiColumn?: boolean; -} - -export const ColumnLoading: React.FC = (otherProps) => ( +export const ColumnLoading: React.FC = (otherProps) => (
diff --git a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx index 4216f3da38..74a8fdb841 100644 --- a/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx +++ b/app/javascript/mastodon/features/ui/components/sign_in_banner.jsx @@ -22,7 +22,8 @@ const SignInBanner = () => { if (sso_redirect) { return (
-

+

+

); @@ -44,7 +45,8 @@ const SignInBanner = () => { return (
-

+

+

{signupButton}
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 7742f64860..d41132f9ca 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -14,6 +14,7 @@ import { HotKeys } from 'react-hotkeys'; import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers'; import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding'; +import { HoverCardController } from 'mastodon/components/hover_card_controller'; import { PictureInPicture } from 'mastodon/features/picture_in_picture'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { layoutFromWindow } from 'mastodon/is_mobile'; @@ -55,6 +56,7 @@ import { FavouritedStatuses, BookmarkedStatuses, FollowedTags, + LinkTimeline, ListTimeline, Blocks, DomainBlocks, @@ -201,6 +203,7 @@ class SwitchingColumnsArea extends PureComponent { + @@ -585,6 +588,7 @@ class UI extends PureComponent { {layout !== 'mobile' && } + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index e1f5bfdaf6..b8a2359d92 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -201,3 +201,7 @@ export function NotificationRequests () { export function NotificationRequest () { return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request'); } + +export function LinkTimeline () { + return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline'); +} diff --git a/app/javascript/mastodon/locales/af.json b/app/javascript/mastodon/locales/af.json index e4f7f12b0e..77e15eb2c6 100644 --- a/app/javascript/mastodon/locales/af.json +++ b/app/javascript/mastodon/locales/af.json @@ -50,7 +50,6 @@ "account.requested_follow": "{name} het versoek om jou te volg", "account.share": "Deel @{name} se profiel", "account.show_reblogs": "Wys aangestuurde plasings van @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Plaas} other {{counter} Plasings}}", "account.unblock": "Deblokkeer @{name}", "account.unblock_domain": "Deblokkeer domein {domain}", "account.unblock_short": "Deblokkeer", diff --git a/app/javascript/mastodon/locales/an.json b/app/javascript/mastodon/locales/an.json index 3f1fd376ff..752b6c3564 100644 --- a/app/javascript/mastodon/locales/an.json +++ b/app/javascript/mastodon/locales/an.json @@ -31,9 +31,7 @@ "account.follow": "Seguir", "account.followers": "Seguidores", "account.followers.empty": "Encara no sigue dengún a este usuario.", - "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}", "account.following": "Seguindo", - "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Seguindo}}", "account.follows.empty": "Este usuario encara no sigue a dengún.", "account.go_to_profile": "Ir ta lo perfil", "account.hide_reblogs": "Amagar retutz de @{name}", @@ -54,7 +52,6 @@ "account.requested_follow": "{name} ha demandau seguir-te", "account.share": "Compartir lo perfil de @{name}", "account.show_reblogs": "Amostrar retutz de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicaciones}}", "account.unblock": "Desblocar a @{name}", "account.unblock_domain": "Amostrar a {domain}", "account.unblock_short": "Desblocar", @@ -476,8 +473,6 @@ "server_banner.about_active_users": "Usuarios activos en o servidor entre los zaguers 30 días (Usuarios Activos Mensuals)", "server_banner.active_users": "usuarios activos", "server_banner.administered_by": "Administrau per:", - "server_banner.introduction": "{domain} ye parte d'o ret social descentralizau liderada per {mastodon}.", - "server_banner.learn_more": "Saber mas", "server_banner.server_stats": "Estatisticas d'o servidor:", "sign_in_banner.create_account": "Creyar cuenta", "sign_in_banner.sign_in": "Iniciar sesión", diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 68e32dd2aa..a9e1bdb270 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -35,9 +35,7 @@ "account.follow_back": "رد المتابعة", "account.followers": "مُتابِعون", "account.followers.empty": "لا أحدَ يُتابع هذا المُستخدم إلى حد الآن.", - "account.followers_counter": "{count, plural, zero{لا مُتابع} one {مُتابعٌ واحِد} two {مُتابعانِ اِثنان} few {{counter} مُتابِعين} many {{counter} مُتابِعًا} other {{counter} مُتابع}}", "account.following": "الاشتراكات", - "account.following_counter": "{count, plural, zero{لا يُتابِع أحدًا} one {يُتابِعُ واحد} two{يُتابِعُ اِثنان} few{يُتابِعُ {counter}} many{يُتابِعُ {counter}} other {يُتابِعُ {counter}}}", "account.follows.empty": "لا يُتابع هذا المُستخدمُ أيَّ أحدٍ حتى الآن.", "account.go_to_profile": "اذهب إلى الملف الشخصي", "account.hide_reblogs": "إخفاء المعاد نشرها مِن @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "لقد طلب {name} متابعتك", "account.share": "شارِك الملف التعريفي لـ @{name}", "account.show_reblogs": "اعرض إعادات نشر @{name}", - "account.statuses_counter": "{count, plural, zero {لَا منشورات} one {منشور واحد} two {منشوران إثنان} few {{counter} منشورات} many {{counter} منشورًا} other {{counter} منشور}}", "account.unblock": "إلغاء الحَظر عن @{name}", "account.unblock_domain": "إلغاء الحَظر عن النِّطاق {domain}", "account.unblock_short": "ألغ الحجب", @@ -225,7 +222,11 @@ "domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.", "domain_pill.username": "اسم المستخدم", "domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟", + "domain_pill.who_they_are": "بما أن المعالجات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الشبكة الاجتماعية لـ .", + "domain_pill.who_you_are": "لأن معالجتك تقول من أنت ومكان وجودك، يمكن الناس التفاعل معك عبر الشبكة الاجتماعية لـ .", "domain_pill.your_handle": "عنوانك الكامل:", + "domain_pill.your_server": "منزلك الرقمي، حيث تعيش جميع مشاركاتك. لا تحب هذا؟ إنقل الخوادم في أي وقت واخضر متابعينك أيضًا.", + "domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.", "embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.", "embed.preview": "إليك ما سيبدو عليه:", "emoji_button.activity": "الأنشطة", @@ -262,6 +263,7 @@ "empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.", "empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.", "empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.", + "empty_column.notification_requests": "لا يوجد شيء هنا. عندما تتلقى إشعارات جديدة، سوف تظهر هنا وفقًا لإعداداتك.", "empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.", "empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات", "error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.", @@ -292,6 +294,8 @@ "filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة", "filter_modal.select_filter.title": "تصفية هذا المنشور", "filter_modal.title.status": "تصفية منشور", + "filtered_notifications_banner.mentions": "{count, plural, one {إشارة} two {إشارتين} few {# إشارات} other {# إشارة}}", + "filtered_notifications_banner.pending_requests": "إشعارات من {count, plural, zero {}=0 {لا أحد} one {شخص واحد قد تعرفه} two {شخصين قد تعرفهما} few {# أشخاص قد تعرفهم} many {# شخص قد تعرفهم} other {# شخص قد تعرفهم}}", "filtered_notifications_banner.title": "الإشعارات المصفاة", "firehose.all": "الكل", "firehose.local": "هذا الخادم", @@ -301,6 +305,8 @@ "follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.", "follow_suggestions.curated_suggestion": "اختيار الموظفين", "follow_suggestions.dismiss": "لا تُظهرها مجدّدًا", + "follow_suggestions.featured_longer": "مختار يدوياً من قِبل فريق {domain}", + "follow_suggestions.friends_of_friends_longer": "مشهور بين الأشخاص الذين تتابعهم", "follow_suggestions.hints.featured": "تم اختيار هذا الملف الشخصي يدوياً من قبل فريق {domain}.", "follow_suggestions.hints.friends_of_friends": "هذا الملف الشخصي مشهور بين الأشخاص الذين تتابعهم.", "follow_suggestions.hints.most_followed": "هذا الملف الشخصي هو واحد من الأكثر متابعة على {domain}.", @@ -405,6 +411,7 @@ "limited_account_hint.action": "إظهار الملف التعريفي على أي حال", "limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.", "link_preview.author": "مِن {name}", + "link_preview.more_from_author": "المزيد من {name}", "lists.account.add": "أضف إلى القائمة", "lists.account.remove": "احذف من القائمة", "lists.delete": "احذف القائمة", @@ -465,10 +472,13 @@ "notification.follow_request": "لقد طلب {name} متابعتك", "notification.mention": "{name} ذكرك", "notification.moderation-warning.learn_more": "اعرف المزيد", + "notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف", + "notification.moderation_warning.action_delete_statuses": "تم إزالة بعض مشاركاتك.", "notification.moderation_warning.action_disable": "تم تعطيل حسابك.", "notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.", "notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.", "notification.moderation_warning.action_sensitive": "سيتم وضع علامة على منشوراتك على أنها حساسة من الآن فصاعدا.", + "notification.moderation_warning.action_silence": "لقد تم تقييد حسابك.", "notification.moderation_warning.action_suspend": "لقد تم تعليق حسابك.", "notification.own_poll": "انتهى استطلاعك للرأي", "notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه", @@ -682,13 +692,10 @@ "server_banner.about_active_users": "الأشخاص الذين يستخدمون هذا الخادم خلال الأيام الثلاثين الأخيرة (المستخدمون النشطون شهريًا)", "server_banner.active_users": "مستخدم نشط", "server_banner.administered_by": "يُديره:", - "server_banner.introduction": "{domain} هو جزء من الشبكة الاجتماعية اللامركزية التي تعمل بواسطة {mastodon}.", - "server_banner.learn_more": "تعلم المزيد", "server_banner.server_stats": "إحصائيات الخادم:", "sign_in_banner.create_account": "أنشئ حسابًا", "sign_in_banner.sign_in": "تسجيل الدخول", "sign_in_banner.sso_redirect": "تسجيل الدخول أو إنشاء حساب", - "sign_in_banner.text": "قم بالولوج بحسابك لمتابعة الصفحات الشخصية أو الوسوم، أو لإضافة المنشورات إلى المفضلة ومشاركتها والرد عليها أو التفاعل بواسطة حسابك المتواجد على خادم مختلف.", "status.admin_account": "افتح الواجهة الإدارية لـ @{name}", "status.admin_domain": "فتح واجهة الإشراف لـ {domain}", "status.admin_status": "افتح هذا المنشور على واجهة الإشراف", diff --git a/app/javascript/mastodon/locales/ast.json b/app/javascript/mastodon/locales/ast.json index b5015c75d8..3f32a8bf15 100644 --- a/app/javascript/mastodon/locales/ast.json +++ b/app/javascript/mastodon/locales/ast.json @@ -32,7 +32,6 @@ "account.followers": "Siguidores", "account.followers.empty": "Naide sigue a esti perfil.", "account.following": "Siguiendo", - "account.following_counter": "{count, plural,one {Sigue a {counter}} other {Sigue a {counter}}}", "account.follows.empty": "Esti perfil nun sigue a naide.", "account.go_to_profile": "Dir al perfil", "account.hide_reblogs": "Anubrir los artículos compartíos de @{name}", @@ -49,7 +48,6 @@ "account.report": "Informar de: @{name}", "account.requested_follow": "{name} solicitó siguite", "account.show_reblogs": "Amosar los artículos compartíos de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} artículu} other {{counter} artículos}}", "account.unblock": "Desbloquiar a @{name}", "account.unblock_domain": "Desbloquiar el dominiu «{domain}»", "account.unblock_short": "Desbloquiar", @@ -409,8 +407,6 @@ "search_results.see_all": "Ver too", "search_results.statuses": "Artículos", "search_results.title": "Busca de: {q}", - "server_banner.introduction": "{domain} ye parte de la rede social descentralizada que tien la teunoloxía de {mastodon}.", - "server_banner.learn_more": "Saber más", "server_banner.server_stats": "Estadístiques del sirvidor:", "sign_in_banner.create_account": "Crear una cuenta", "sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 61e96e4b58..643725270c 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -35,9 +35,7 @@ "account.follow_back": "Падпісацца ў адказ", "account.followers": "Падпісчыкі", "account.followers.empty": "Ніхто пакуль не падпісаны на гэтага карыстальніка.", - "account.followers_counter": "{count, plural, one {{counter} падпісчык} few {{counter} падпісчыкі} many {{counter} падпісчыкаў} other {{counter} падпісчыка}}", "account.following": "Падпіскі", - "account.following_counter": "{count, plural, one {{counter} падпіска} few {{counter} падпіскі} many {{counter} падпісак} other {{counter} падпіскі}}", "account.follows.empty": "Карыстальнік ні на каго не падпісаны.", "account.go_to_profile": "Перайсці да профілю", "account.hide_reblogs": "Схаваць пашырэнні ад @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} адправіў запыт на падпіску", "account.share": "Абагуліць профіль @{name}", "account.show_reblogs": "Паказаць падштурхоўванні ад @{name}", - "account.statuses_counter": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}", "account.unblock": "Разблакіраваць @{name}", "account.unblock_domain": "Разблакіраваць дамен {domain}", "account.unblock_short": "Разблакіраваць", @@ -308,6 +305,8 @@ "follow_requests.unlocked_explanation": "Ваш акаўнт не схаваны, аднак прадстаўнікі {domain} палічылі, што вы можаце захацець праглядзець запыты на падпіску з гэтых профіляў уручную.", "follow_suggestions.curated_suggestion": "Выбар адміністрацыі", "follow_suggestions.dismiss": "Не паказваць зноў", + "follow_suggestions.featured_longer": "Адабраныя камандай {domain} уручную", + "follow_suggestions.friends_of_friends_longer": "Папулярнае сярод людзей, на якіх Вы падпісаны", "follow_suggestions.hints.featured": "Гэты профіль быў выбраны ўручную камандай {domain}.", "follow_suggestions.hints.friends_of_friends": "Гэты профіль папулярны сярод людзей, на якіх вы падпісаліся.", "follow_suggestions.hints.most_followed": "Гэты профіль - адзін з профіляў з самай вялікай колькасцю падпісак на {domain}.", @@ -315,6 +314,8 @@ "follow_suggestions.hints.similar_to_recently_followed": "Гэты профіль падобны на профілі, на якія вы нядаўна падпісаліся.", "follow_suggestions.personalized_suggestion": "Персаналізаваная прапанова", "follow_suggestions.popular_suggestion": "Папулярная прапанова", + "follow_suggestions.popular_suggestion_longer": "Папулярнае на {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Падобныя профілі, за якімі вы нядаўна сачылі", "follow_suggestions.view_all": "Праглядзець усё", "follow_suggestions.who_to_follow": "На каго падпісацца", "followed_tags": "Падпіскі", @@ -410,6 +411,7 @@ "limited_account_hint.action": "Усе роўна паказваць профіль", "limited_account_hint.title": "Гэты профіль быў схаваны мадэратарамі", "link_preview.author": "Ад {name}", + "link_preview.more_from_author": "Больш ад {name}", "lists.account.add": "Дадаць да спісу", "lists.account.remove": "Выдаліць са спісу", "lists.delete": "Выдаліць спіс", @@ -439,7 +441,7 @@ "mute_modal.you_wont_see_posts": "Карыстальнік па-ранейшаму будзе бачыць вашыя паведамленні, але вы не будзеце паведамленні карыстальніка.", "navigation_bar.about": "Пра нас", "navigation_bar.advanced_interface": "Адкрыць у пашыраным вэб-інтэрфейсе", - "navigation_bar.blocks": "Заблакаваныя карыстальнікі", + "navigation_bar.blocks": "Заблакіраваныя карыстальнікі", "navigation_bar.bookmarks": "Закладкі", "navigation_bar.community_timeline": "Лакальная стужка", "navigation_bar.compose": "Стварыць новы допіс", @@ -458,7 +460,7 @@ "navigation_bar.opened_in_classic_interface": "Допісы, уліковыя запісы і іншыя спецыфічныя старонкі па змоўчанні адчыняюцца ў класічным вэб-інтэрфейсе.", "navigation_bar.personal": "Асабістае", "navigation_bar.pins": "Замацаваныя допісы", - "navigation_bar.preferences": "Параметры", + "navigation_bar.preferences": "Налады", "navigation_bar.public_timeline": "Глабальная стужка", "navigation_bar.search": "Пошук", "navigation_bar.security": "Бяспека", @@ -470,10 +472,22 @@ "notification.follow_request": "{name} адправіў запыт на падпіску", "notification.mention": "{name} згадаў вас", "notification.moderation-warning.learn_more": "Даведацца больш", + "notification.moderation_warning": "Вы атрымалі папярэджанне аб мадэрацыі", + "notification.moderation_warning.action_delete_statuses": "Некаторыя вашыя допісы былі выдаленыя.", + "notification.moderation_warning.action_disable": "Ваш уліковы запіс быў адключаны.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Некаторыя з вашых допісаў былі пазначаныя як далікатныя.", + "notification.moderation_warning.action_none": "Ваш уліковы запіс атрымаў папярэджанне ад мадэратараў.", + "notification.moderation_warning.action_sensitive": "З гэтага моманту вашыя допісы будуць пазначаныя як далікатныя.", + "notification.moderation_warning.action_silence": "Ваш уліковы запіс быў абмежаваны.", + "notification.moderation_warning.action_suspend": "Ваш уліковы запіс быў прыпынены.", "notification.own_poll": "Ваша апытанне скончылася", "notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася", "notification.reblog": "{name} пашырыў ваш допіс", + "notification.relationships_severance_event": "Страціў сувязь з {name}", + "notification.relationships_severance_event.account_suspension": "Адміністратар з {from} прыпыніў працу {target}, што азначае, што вы больш не можаце атрымліваць ад іх абнаўлення ці ўзаемадзейнічаць з імі.", + "notification.relationships_severance_event.domain_block": "Адміністратар з {from} заблакіраваў {target}, у тым ліку {followersCount} вашых падпісчыка(-аў) і {followingCount, plural, one {# уліковы запіс} few {# уліковыя запісы} many {# уліковых запісаў} other {# уліковых запісаў}}.", "notification.relationships_severance_event.learn_more": "Даведацца больш", + "notification.relationships_severance_event.user_domain_block": "Вы заблакіравалі {target} выдаліўшы {followersCount} сваіх падпісчыкаў і {followingCount, plural, one {# уліковы запіс} few {# уліковыя запісы} many {# уліковых запісаў} other {# уліковых запісаў}}, за якімі вы сочыце.", "notification.status": "Новы допіс ад {name}", "notification.update": "Допіс {name} адрэдагаваны", "notification_requests.accept": "Прыняць", @@ -678,13 +692,10 @@ "server_banner.about_active_users": "Людзі, якія карыстаюцца гэтым сервера на працягу апошніх 30 дзён (Штомесячна Актыўныя Карыстальнікі)", "server_banner.active_users": "актыўныя карыстальнікі", "server_banner.administered_by": "Адміністратар:", - "server_banner.introduction": "{domain} ёсць часткай дэцэнтралізаванай сацыяльнай сеткі ад {mastodon}.", - "server_banner.learn_more": "Даведацца больш", "server_banner.server_stats": "Статыстыка сервера:", "sign_in_banner.create_account": "Стварыць уліковы запіс", "sign_in_banner.sign_in": "Увайсці", "sign_in_banner.sso_redirect": "Уваход ці рэгістрацыя", - "sign_in_banner.text": "Увайдзіце, каб падпісацца на людзей і тэгі, каб адказваць на допісы, дзяліцца імі і падабаць іх, альбо кантактаваць з вашага ўліковага запісу на іншым серверы.", "status.admin_account": "Адкрыць інтэрфейс мадэратара для @{name}", "status.admin_domain": "Адкрыць інтэрфейс мадэратара для {domain}", "status.admin_status": "Адкрыць гэты допіс у інтэрфейсе мадэрацыі", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index b17172058b..323890ba2e 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -35,9 +35,7 @@ "account.follow_back": "Последване взаимно", "account.followers": "Последователи", "account.followers.empty": "Още никой не следва потребителя.", - "account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}", "account.following": "Последвано", - "account.following_counter": "{count, plural, one {{counter} последван} other {{counter} последвани}}", "account.follows.empty": "Потребителят още никого не следва.", "account.go_to_profile": "Към профила", "account.hide_reblogs": "Скриване на подсилвания от @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} поиска да ви последва", "account.share": "Споделяне на профила на @{name}", "account.show_reblogs": "Показване на подсилвания от @{name}", - "account.statuses_counter": "{count, plural, one {{counter} публикация} other {{counter} публикации}}", "account.unblock": "Отблокиране на @{name}", "account.unblock_domain": "Отблокиране на домейн {domain}", "account.unblock_short": "Отблокиране", @@ -415,6 +412,7 @@ "limited_account_hint.title": "Този профил е бил скрит от модераторите на {domain}.", "link_preview.author": "От {name}", "link_preview.more_from_author": "Още от {name}", + "link_preview.shares": "{count, plural, one {{counter} публикация} other {{counter} публикации}}", "lists.account.add": "Добавяне към списък", "lists.account.remove": "Премахване от списъка", "lists.delete": "Изтриване на списъка", @@ -695,13 +693,13 @@ "server_banner.about_active_users": "Ползващите сървъра през последните 30 дни (дейните месечно потребители)", "server_banner.active_users": "дейни потребители", "server_banner.administered_by": "Администрира се от:", - "server_banner.introduction": "{domain} е част от децентрализираната социална мрежа, поддържана от {mastodon}.", - "server_banner.learn_more": "Научете повече", + "server_banner.is_one_of_many": "{domain} е един от многото независими сървъри на Mastodon, които може да употребявате, за да участвате във федивселената.", "server_banner.server_stats": "Статистика на сървъра:", "sign_in_banner.create_account": "Създаване на акаунт", + "sign_in_banner.follow_anyone": "Последвайте някого през федивселената и вижте всичко в хронологичен ред. Без алгоритми, реклами, или примамващи връзки в полезрението.", + "sign_in_banner.mastodon_is": "Mastodon е най-добрия начин да бъдете в крак със случващото се.", "sign_in_banner.sign_in": "Вход", "sign_in_banner.sso_redirect": "Влизане или регистриране", - "sign_in_banner.text": "Влезте, за да последвате профили или хаштагове, отбелязвате като любими, споделяте и отговаряте на публикации. Може също така да взаимодействате от акаунта си на друг сървър.", "status.admin_account": "Отваряне на интерфейс за модериране за @{name}", "status.admin_domain": "Отваряне на модериращия интерфейс за {domain}", "status.admin_status": "Отваряне на публикацията в модериращия интерфейс", @@ -742,7 +740,7 @@ "status.reblogged_by": "{name} подсили", "status.reblogs": "{count, plural, one {подсилване} other {подсилвания}}", "status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.", - "status.redraft": "Изтриване и преначертаване", + "status.redraft": "Изтриване и преработване", "status.remove_bookmark": "Премахване на отметката", "status.replied_to": "В отговор до {name}", "status.reply": "Отговор", diff --git a/app/javascript/mastodon/locales/bn.json b/app/javascript/mastodon/locales/bn.json index 797b93e243..a203c43f03 100644 --- a/app/javascript/mastodon/locales/bn.json +++ b/app/javascript/mastodon/locales/bn.json @@ -33,9 +33,7 @@ "account.follow": "অনুসরণ", "account.followers": "অনুসরণকারী", "account.followers.empty": "এই ব্যক্তিকে এখনো কেউ অনুসরণ করে না.", - "account.followers_counter": "{count, plural,one {{counter} জন অনুসরণকারী } other {{counter} জন অনুসরণকারী}}", "account.following": "অনুসরণ করা হচ্ছে", - "account.following_counter": "{count, plural,one {{counter} জনকে অনুসরণ} other {{counter} জনকে অনুসরণ}}", "account.follows.empty": "এই সদস্য কাউকে এখনো ফলো করেন না.", "account.go_to_profile": "প্রোফাইলে যান", "account.hide_reblogs": "@{name}'র সমর্থনগুলি লুকিয়ে ফেলুন", @@ -60,7 +58,6 @@ "account.requested_follow": "{name} আপনাকে অনুসরণ করার জন্য অনুরোধ করেছে", "account.share": "@{name} র প্রোফাইল অন্যদের দেখান", "account.show_reblogs": "@{name} র সমর্থনগুলো দেখান", - "account.statuses_counter": "{count, plural,one {{counter} টুট} other {{counter} টুট}}", "account.unblock": "@{name} র কার্যকলাপ দেখুন", "account.unblock_domain": "{domain} কে আবার দেখুন", "account.unblock_short": "আনব্লক করুন", @@ -407,7 +404,6 @@ "search_results.all": "সব", "search_results.hashtags": "হ্যাশট্যাগগুলি", "search_results.statuses": "টুট", - "server_banner.learn_more": "আরো জানো", "sign_in_banner.sign_in": "Sign in", "status.admin_account": "@{name} র জন্য পরিচালনার ইন্টারফেসে ঢুকুন", "status.admin_status": "যায় লেখাটি পরিচালনার ইন্টারফেসে খুলুন", diff --git a/app/javascript/mastodon/locales/br.json b/app/javascript/mastodon/locales/br.json index 9ec26c8c12..a150fb4902 100644 --- a/app/javascript/mastodon/locales/br.json +++ b/app/javascript/mastodon/locales/br.json @@ -35,9 +35,8 @@ "account.follow_back": "Heuliañ d'ho tro", "account.followers": "Tud koumanantet", "account.followers.empty": "Den na heul an implijer·ez-mañ c'hoazh.", - "account.followers_counter": "{count, plural, other{{counter} Heulier·ez}}", + "account.followers_counter": "{count, plural, one {{counter} heulier} two {{counter} heulier} few {{counter} heulier} many {{counter} heulier} other {{counter} heulier}}", "account.following": "Koumanantoù", - "account.following_counter": "{count, plural, one{{counter} C'houmanant} two{{counter} Goumanant} other {{counter} a Goumanant}}", "account.follows.empty": "An implijer·ez-mañ na heul den ebet.", "account.go_to_profile": "Gwelet ar profil", "account.hide_reblogs": "Kuzh skignadennoù gant @{name}", @@ -62,7 +61,7 @@ "account.requested_follow": "Gant {name} eo bet goulennet ho heuliañ", "account.share": "Skignañ profil @{name}", "account.show_reblogs": "Diskouez skignadennoù @{name}", - "account.statuses_counter": "{count, plural, one {{counter} C'hannad} two {{counter} Gannad} other {{counter} a Gannadoù}}", + "account.statuses_counter": "{count, plural, one {{counter} embannadur} two {{counter} embannadur} few {{counter} embannadur} many {{counter} embannadur} other {{counter} embannadur}}", "account.unblock": "Diverzañ @{name}", "account.unblock_domain": "Diverzañ an domani {domain}", "account.unblock_short": "Distankañ", @@ -566,7 +565,6 @@ "search_results.title": "Klask {q}", "server_banner.active_users": "implijerien·ezed oberiant", "server_banner.administered_by": "Meret gant :", - "server_banner.learn_more": "Gouzout hiroc'h", "server_banner.server_stats": "Stadegoù ar servijer :", "sign_in_banner.create_account": "Krouiñ ur gont", "sign_in_banner.sign_in": "Kevreañ", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 68429b093c..3123e29d8d 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -35,9 +35,9 @@ "account.follow_back": "Segueix tu també", "account.followers": "Seguidors", "account.followers.empty": "A aquest usuari encara no el segueix ningú.", - "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} Seguidors}}", + "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidors}}", "account.following": "Seguint", - "account.following_counter": "{count, plural, other {{counter} Seguint-ne}}", + "account.following_counter": "{count, plural, other {Seguint-ne {counter}}}", "account.follows.empty": "Aquest usuari encara no segueix ningú.", "account.go_to_profile": "Vés al perfil", "account.hide_reblogs": "Amaga els impulsos de @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ha demanat de seguir-te", "account.share": "Comparteix el perfil de @{name}", "account.show_reblogs": "Mostra els impulsos de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Tut} other {{counter} Tuts}}", + "account.statuses_counter": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}", "account.unblock": "Desbloca @{name}", "account.unblock_domain": "Desbloca el domini {domain}", "account.unblock_short": "Desbloca", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Aquest perfil l'han amagat els moderadors de {domain}.", "link_preview.author": "Per {name}", "link_preview.more_from_author": "Més de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicació} other {{counter} publicacions}}", "lists.account.add": "Afegeix a la llista", "lists.account.remove": "Elimina de la llista", "lists.delete": "Elimina la llista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Gent que ha fet servir aquest servidor en els darrers 30 dies (Usuaris Actius Mensuals)", "server_banner.active_users": "usuaris actius", "server_banner.administered_by": "Administrat per:", - "server_banner.introduction": "{domain} és part de la xarxa social descentralitzada impulsada per {mastodon}.", - "server_banner.learn_more": "Més informació", + "server_banner.is_one_of_many": "{domain} és un dels molts servidors de Mastodon que pots fer servir per a participar en el fedivers.", "server_banner.server_stats": "Estadístiques del servidor:", "sign_in_banner.create_account": "Crea un compte", + "sign_in_banner.follow_anyone": "Segueix qui sigui al fedivers i ho veuràs tot en ordre cronològic. Sense algorismes, anuncis o pescaclics.", + "sign_in_banner.mastodon_is": "Mastodon és la millor manera de seguir al moment què passa.", "sign_in_banner.sign_in": "Inici de sessió", "sign_in_banner.sso_redirect": "Inici de sessió o Registre", - "sign_in_banner.text": "Inicia la sessió per a seguir perfils o etiquetes, afavorir, compartir i respondre tuts. També pots interactuar des del teu compte a un servidor diferent.", "status.admin_account": "Obre la interfície de moderació per a @{name}", "status.admin_domain": "Obre la interfície de moderació per a @{domain}", "status.admin_status": "Obre aquest tut a la interfície de moderació", diff --git a/app/javascript/mastodon/locales/ckb.json b/app/javascript/mastodon/locales/ckb.json index c3c365b3a1..3ebf9391d2 100644 --- a/app/javascript/mastodon/locales/ckb.json +++ b/app/javascript/mastodon/locales/ckb.json @@ -35,9 +35,7 @@ "account.follow_back": "فۆڵۆو بکەنەوە", "account.followers": "شوێنکەوتووان", "account.followers.empty": "کەسێک شوێن ئەم بەکارهێنەرە نەکەوتووە", - "account.followers_counter": "{count, plural, one {{counter} شوێنکەوتوو} other {{counter} شوێنکەوتوو}}", "account.following": "بەدوادا", - "account.following_counter": "{count, plural, one {{counter} شوێنکەوتوو} other {{counter} شوێنکەوتوو}}", "account.follows.empty": "ئەم بەکارهێنەرە تا ئێستا شوێن کەس نەکەوتووە.", "account.go_to_profile": "بڕۆ بۆ پڕۆفایلی", "account.hide_reblogs": "داشاردنی بووستەکان لە @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} داوای کردووە شوێنت بکەوێت", "account.share": "پرۆفایلی @{name} هاوبەش بکە", "account.show_reblogs": "پیشاندانی بەرزکردنەوەکان لە @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}", "account.unblock": "@{name} لاببە", "account.unblock_domain": "کردنەوەی دۆمەینی {domain}", "account.unblock_short": "لابردنی بەربەست", @@ -533,8 +530,6 @@ "server_banner.about_active_users": "ئەو کەسانەی لە ماوەی ٣٠ ڕۆژی ڕابردوودا ئەم سێرڤەرە بەکاردەهێنن (بەکارهێنەرانی چالاک مانگانە)", "server_banner.active_users": "بەکارهێنەرانی چالاک", "server_banner.administered_by": "بەڕێوەبردن لەلایەن:", - "server_banner.introduction": "{domain} بەشێکە لەو تۆڕە کۆمەڵایەتییە لامەرکەزییەی کە لەلایەن {mastodon}ەوە بەهێز دەکرێت.", - "server_banner.learn_more": "زیاتر فێربه", "server_banner.server_stats": "دۆخی ڕاژەکار:", "sign_in_banner.create_account": "هەژمار دروستبکە", "sign_in_banner.sign_in": "بچۆ ژوورەوە", diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json index be4cce2692..78f8e6fd78 100644 --- a/app/javascript/mastodon/locales/co.json +++ b/app/javascript/mastodon/locales/co.json @@ -16,8 +16,6 @@ "account.follow": "Siguità", "account.followers": "Abbunati", "account.followers.empty": "Nisunu hè abbunatu à st'utilizatore.", - "account.followers_counter": "{count, plural, one {{counter} Abbunatu} other {{counter} Abbunati}}", - "account.following_counter": "{count, plural, one {{counter} Abbunamentu} other {{counter} Abbunamenti}}", "account.follows.empty": "St'utilizatore ùn seguita nisunu.", "account.hide_reblogs": "Piattà spartere da @{name}", "account.link_verified_on": "A prupietà di stu ligame hè stata verificata u {date}", @@ -32,7 +30,6 @@ "account.requested": "In attesa d'apprubazione. Cliccate per annullà a dumanda", "account.share": "Sparte u prufile di @{name}", "account.show_reblogs": "Vede spartere da @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Statutu} other {{counter} Statuti}}", "account.unblock": "Sbluccà @{name}", "account.unblock_domain": "Ùn piattà più {domain}", "account.unendorse": "Ùn fà figurà nant'à u prufilu", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index f2a1f023a3..e96e283970 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -20,7 +20,7 @@ "account.block_short": "Zablokovat", "account.blocked": "Blokovaný", "account.browse_more_on_origin_server": "Více na původním profilu", - "account.cancel_follow_request": "Zrušit žádost o sledování", + "account.cancel_follow_request": "Zrušit sledování", "account.copy": "Kopírovat odkaz na profil", "account.direct": "Soukromě zmínit @{name}", "account.disable_notifications": "Přestat mě upozorňovat, když @{name} zveřejní příspěvek", @@ -35,9 +35,9 @@ "account.follow_back": "Také sledovat", "account.followers": "Sledující", "account.followers.empty": "Tohoto uživatele zatím nikdo nesleduje.", - "account.followers_counter": "{count, plural, one {{counter} Sledující} few {{counter} Sledující} many {{counter} Sledujících} other {{counter} Sledujících}}", + "account.followers_counter": "{count, plural, one {{counter} sledující} few {{counter} sledující} many {{counter} sledujících} other {{counter} sledujících}}", "account.following": "Sledujete", - "account.following_counter": "{count, plural, one {{counter} Sledovaný} few {{counter} Sledovaní} many {{counter} Sledovaných} other {{counter} Sledovaných}}", + "account.following_counter": "{count, plural, one {{counter} sledovaný} few {{counter} sledovaní} many {{counter} sledovaných} other {{counter} sledovaných}}", "account.follows.empty": "Tento uživatel zatím nikoho nesleduje.", "account.go_to_profile": "Přejít na profil", "account.hide_reblogs": "Skrýt boosty od @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} tě požádal o sledování", "account.share": "Sdílet profil @{name}", "account.show_reblogs": "Zobrazit boosty od @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Příspěvek} few {{counter} Příspěvky} many {{counter} Příspěvků} other {{counter} Příspěvků}}", + "account.statuses_counter": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}", "account.unblock": "Odblokovat @{name}", "account.unblock_domain": "Odblokovat doménu {domain}", "account.unblock_short": "Odblokovat", @@ -78,9 +78,9 @@ "admin.dashboard.retention.average": "Průměr", "admin.dashboard.retention.cohort": "Měsíc registrace", "admin.dashboard.retention.cohort_size": "Noví uživatelé", - "admin.impact_report.instance_accounts": "Profily účtů, které by odstranily", - "admin.impact_report.instance_followers": "Sledovatelé, o které by naši uživatelé přišli", - "admin.impact_report.instance_follows": "Následovníci jejich uživatelé by ztratili", + "admin.impact_report.instance_accounts": "Profily účtů, které by byli odstaněny", + "admin.impact_report.instance_followers": "Sledující, o které by naši uživatelé přišli", + "admin.impact_report.instance_follows": "Sledující, o které by naši uživatelé přišli", "admin.impact_report.title": "Shrnutí dopadu", "alert.rate_limited.message": "Zkuste to prosím znovu po {retry_time, time, medium}.", "alert.rate_limited.title": "Spojení omezena", @@ -89,7 +89,7 @@ "announcement.announcement": "Oznámení", "attachments_list.unprocessed": "(nezpracováno)", "audio.hide": "Skrýt zvuk", - "block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.", + "block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou stále být viditelné pro nepřihlášené uživatele.", "block_modal.show_less": "Zobrazit méně", "block_modal.show_more": "Zobrazit více", "block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.", @@ -414,6 +414,8 @@ "limited_account_hint.action": "Přesto profil zobrazit", "limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.", "link_preview.author": "Podle {name}", + "link_preview.more_from_author": "Více od {name}", + "link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}", "lists.account.add": "Přidat do seznamu", "lists.account.remove": "Odebrat ze seznamu", "lists.delete": "Smazat seznam", @@ -694,13 +696,13 @@ "server_banner.about_active_users": "Lidé používající tento server během posledních 30 dní (měsíční aktivní uživatelé)", "server_banner.active_users": "aktivní uživatelé", "server_banner.administered_by": "Spravováno:", - "server_banner.introduction": "{domain} je součástí decentralizované sociální sítě běžící na {mastodon}.", - "server_banner.learn_more": "Zjistit více", + "server_banner.is_one_of_many": "{domain} je jedním z mnoha Mastodon serverů, které můžete použít k účasti na fediversu.", "server_banner.server_stats": "Statistiky serveru:", "sign_in_banner.create_account": "Vytvořit účet", + "sign_in_banner.follow_anyone": "Sledujte kohokoli napříč fediversem a uvidíte vše v chronologickém pořadí. Bez algoritmů, reklam a clickbaitu.", + "sign_in_banner.mastodon_is": "Mastodon je ten nejlepší způsob, jak udržet krok s tím, co se právě děje.", "sign_in_banner.sign_in": "Přihlásit se", "sign_in_banner.sso_redirect": "Přihlášení nebo Registrace", - "sign_in_banner.text": "Přihlaste se pro sledování profilů nebo hashtagů, oblíbení, sdílení a odpovídání na příspěvky. Svůj účet můžete také používat k interagování i na jiném serveru.", "status.admin_account": "Otevřít moderátorské rozhraní pro @{name}", "status.admin_domain": "Otevřít moderátorské rozhraní pro {domain}", "status.admin_status": "Otevřít tento příspěvek v moderátorském rozhraní", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 2c59769959..1c7e61832c 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -35,9 +35,7 @@ "account.follow_back": "Dilyn yn ôl", "account.followers": "Dilynwyr", "account.followers.empty": "Does neb yn dilyn y defnyddiwr hwn eto.", - "account.followers_counter": "{count, plural, one {Dilynwr: {counter}} other {Dilynwyr: {counter}}}", "account.following": "Yn dilyn", - "account.following_counter": "{count, plural, one {Yn dilyn: {counter}} other {Yn dilyn: {counter}}}", "account.follows.empty": "Nid yw'r defnyddiwr hwn yn dilyn unrhyw un eto.", "account.go_to_profile": "Mynd i'r proffil", "account.hide_reblogs": "Cuddio hybiau gan @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn", "account.share": "Rhannwch broffil @{name}", "account.show_reblogs": "Dangos hybiau gan @{name}", - "account.statuses_counter": "{count, plural, one {Postiad: {counter}} other {Postiad: {counter}}}", "account.unblock": "Dadflocio @{name}", "account.unblock_domain": "Dadflocio parth {domain}", "account.unblock_short": "Dadflocio", @@ -694,13 +691,10 @@ "server_banner.about_active_users": "Pobl sy'n defnyddio'r gweinydd hwn yn ystod y 30 diwrnod diwethaf (Defnyddwyr Gweithredol Misol)", "server_banner.active_users": "defnyddwyr gweithredol", "server_banner.administered_by": "Gweinyddir gan:", - "server_banner.introduction": "Mae {domain} yn rhan o'r rhwydwaith cymdeithasol datganoledig sy'n cael ei bweru gan {mastodon}.", - "server_banner.learn_more": "Dysgu mwy", "server_banner.server_stats": "Ystadegau'r gweinydd:", "sign_in_banner.create_account": "Creu cyfrif", "sign_in_banner.sign_in": "Mewngofnodi", "sign_in_banner.sso_redirect": "Mewngofnodi neu Gofrestru", - "sign_in_banner.text": "Mewngofnodwch i ddilyn proffiliau neu hashnodau, ffefrynnau, rhannu ac ymateb i bostiadau. Gallwch hefyd ryngweithio o'ch cyfrif ar weinyddion gwahanol.", "status.admin_account": "Agor rhyngwyneb cymedroli ar gyfer @{name}", "status.admin_domain": "Agor rhyngwyneb cymedroli {domain}", "status.admin_status": "Agor y postiad hwn yn y rhyngwyneb cymedroli", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index ae2968087b..d8c178d295 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -35,9 +35,8 @@ "account.follow_back": "Følg tilbage", "account.followers": "Følgere", "account.followers.empty": "Ingen følger denne bruger endnu.", - "account.followers_counter": "{count, plural, one {{counter} Følger} other {{counter} Følgere}}", + "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}", "account.following": "Følger", - "account.following_counter": "{count, plural, one {{counter} Følges} other {{counter} Følges}}", "account.follows.empty": "Denne bruger følger ikke nogen endnu.", "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul boosts fra @{name}", @@ -63,7 +62,6 @@ "account.requested_follow": "{name} har anmodet om at følge dig", "account.share": "Del @{name}s profil", "account.show_reblogs": "Vis fremhævelser fra @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Indlæg} other {{counter} Indlæg}}", "account.unblock": "Afblokér @{name}", "account.unblock_domain": "Afblokér domænet {domain}", "account.unblock_short": "Afblokér", @@ -415,6 +413,7 @@ "limited_account_hint.title": "Denne profil er blevet skjult af {domain}-moderatorerne.", "link_preview.author": "Af {name}", "link_preview.more_from_author": "Mere fra {name}", + "link_preview.shares": "{count, plural, one {{counter} indlæg} other {{counter} indlæg}}", "lists.account.add": "Føj til liste", "lists.account.remove": "Fjern fra liste", "lists.delete": "Slet liste", @@ -695,13 +694,10 @@ "server_banner.about_active_users": "Folk, som brugte denne server de seneste 30 dage (månedlige aktive brugere)", "server_banner.active_users": "aktive brugere", "server_banner.administered_by": "Håndteres af:", - "server_banner.introduction": "{domain} er en del af det decentraliserede, sociale netværk drevet af {mastodon}.", - "server_banner.learn_more": "Læs mere", "server_banner.server_stats": "Serverstatstik:", "sign_in_banner.create_account": "Opret konto", "sign_in_banner.sign_in": "Log ind", "sign_in_banner.sso_redirect": "Log ind eller Tilmeld", - "sign_in_banner.text": "Log ind for at følge profiler eller hashtags, markere som favorit, dele og besvare indlæg eller interagere fra din konto på en anden server.", "status.admin_account": "Åbn modereringsbrugerflade for @{name}", "status.admin_domain": "Åbn modereringsbrugerflade for {domain}", "status.admin_status": "Åbn dette indlæg i modereringsbrugerfladen", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index ef08e9b6d3..86438757a3 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -415,6 +415,7 @@ "limited_account_hint.title": "Dieses Profil wurde von den Moderator*innen von {domain} ausgeblendet.", "link_preview.author": "Von {name}", "link_preview.more_from_author": "Mehr von {name}", + "link_preview.shares": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}", "lists.account.add": "Zur Liste hinzufügen", "lists.account.remove": "Von der Liste entfernen", "lists.delete": "Liste löschen", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Personen, die diesen Server in den vergangenen 30 Tagen verwendet haben (monatlich aktive Nutzer*innen)", "server_banner.active_users": "aktive Profile", "server_banner.administered_by": "Verwaltet von:", - "server_banner.introduction": "{domain} ist Teil eines dezentralisierten sozialen Netzwerks, angetrieben von {mastodon}.", - "server_banner.learn_more": "Mehr erfahren", + "server_banner.is_one_of_many": "{domain} ist einer von vielen unabhängigen Mastodon-Servern, mit dem du dich im Fediverse beteiligen kannst.", "server_banner.server_stats": "Serverstatistik:", "sign_in_banner.create_account": "Konto erstellen", + "sign_in_banner.follow_anyone": "Du kannst jedem im Fediverse folgen und alles in chronologischer Reihenfolge sehen. Keine Algorithmen, Werbung oder Clickbaits vorhanden.", + "sign_in_banner.mastodon_is": "Mastodon ist der beste Zugang, um auf dem Laufenden zu bleiben.", "sign_in_banner.sign_in": "Anmelden", "sign_in_banner.sso_redirect": "Anmelden oder registrieren", - "sign_in_banner.text": "Melde dich an, um Profilen oder Hashtags zu folgen, Beiträge zu favorisieren, zu teilen und auf sie zu antworten. Du kannst auch von deinem Konto aus auf einem anderen Server interagieren.", "status.admin_account": "@{name} moderieren", "status.admin_domain": "{domain} moderieren", "status.admin_status": "Beitrag moderieren", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 937bb5d027..5442624b36 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -35,9 +35,7 @@ "account.follow_back": "Ακολούθησε και εσύ", "account.followers": "Ακόλουθοι", "account.followers.empty": "Κανείς δεν ακολουθεί αυτόν τον χρήστη ακόμα.", - "account.followers_counter": "{count, plural, one {{counter} Ακόλουθος} other {{counter} Ακόλουθοι}}", "account.following": "Ακολουθείτε", - "account.following_counter": "{count, plural, one {{counter} Ακολουθεί} other {{counter} Ακολουθούν}}", "account.follows.empty": "Αυτός ο χρήστης δεν ακολουθεί κανέναν ακόμα.", "account.go_to_profile": "Μετάβαση στο προφίλ", "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "Ο/Η {name} αιτήθηκε να σε ακολουθήσει", "account.share": "Κοινοποίηση του προφίλ @{name}", "account.show_reblogs": "Εμφάνιση ενισχύσεων από @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Ανάρτηση} other {{counter} Αναρτήσεις}}", "account.unblock": "Άρση αποκλεισμού @{name}", "account.unblock_domain": "Άρση αποκλεισμού του τομέα {domain}", "account.unblock_short": "Άρση αποκλεισμού", @@ -558,13 +555,10 @@ "server_banner.about_active_users": "Άτομα που χρησιμοποιούν αυτόν τον διακομιστή κατά τις τελευταίες 30 ημέρες (Μηνιαία Ενεργοί Χρήστες)", "server_banner.active_users": "ενεργοί χρήστες", "server_banner.administered_by": "Διαχειριστής:", - "server_banner.introduction": "Ο {domain} είναι μέρος του αποκεντρωμένου κοινωνικού δικτύου που παρέχεται από {mastodon}.", - "server_banner.learn_more": "Μάθε περισσότερα", "server_banner.server_stats": "Στατιστικά διακομιστή:", "sign_in_banner.create_account": "Δημιουργία λογαριασμού", "sign_in_banner.sign_in": "Σύνδεση", "sign_in_banner.sso_redirect": "Συνδεθείτε ή Εγγραφείτε", - "sign_in_banner.text": "Συνδεθείτε για να ακολουθήσετε προφίλ ή ετικέτες, αγαπήστε, μοιραστείτε και απαντήστε σε δημοσιεύσεις. Μπορείτε επίσης να αλληλεπιδράσετε από τον λογαριασμό σας σε διαφορετικό διακομιστή.", "status.admin_account": "Άνοιγμα διεπαφής συντονισμού για τον/την @{name}", "status.admin_domain": "Άνοιγμα λειτουργίας διαμεσολάβησης για {domain}", "status.admin_status": "Άνοιγμα αυτής της ανάρτησης σε διεπαφή συντονισμού", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 2d497f386e..1bcae3075b 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -35,9 +35,9 @@ "account.follow_back": "Follow back", "account.followers": "Followers", "account.followers.empty": "No one follows this user yet.", - "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}", + "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}", "account.following": "Following", - "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}", + "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}", "account.follows.empty": "This user doesn't follow anyone yet.", "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} has requested to follow you", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}", + "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unblock domain {domain}", "account.unblock_short": "Unblock", @@ -414,6 +414,8 @@ "limited_account_hint.action": "Show profile anyway", "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.", "link_preview.author": "By {name}", + "link_preview.more_from_author": "More from {name}", + "link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}", "lists.account.add": "Add to list", "lists.account.remove": "Remove from list", "lists.delete": "Delete list", @@ -694,13 +696,13 @@ "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.active_users": "active users", "server_banner.administered_by": "Administered by:", - "server_banner.introduction": "{domain} is part of the decentralised social network powered by {mastodon}.", - "server_banner.learn_more": "Learn more", + "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.", "server_banner.server_stats": "Server stats:", "sign_in_banner.create_account": "Create account", + "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", + "sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.", "sign_in_banner.sign_in": "Sign in", "sign_in_banner.sso_redirect": "Login or Register", - "sign_in_banner.text": "Login to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.", "status.admin_account": "Open moderation interface for @{name}", "status.admin_domain": "Open moderation interface for {domain}", "status.admin_status": "Open this post in the moderation interface", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index ef2a3b4008..dd31acc0e0 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -35,9 +35,9 @@ "account.follow_back": "Follow back", "account.followers": "Followers", "account.followers.empty": "No one follows this user yet.", - "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Followers}}", + "account.followers_counter": "{count, plural, one {{counter} follower} other {{counter} followers}}", "account.following": "Following", - "account.following_counter": "{count, plural, one {{counter} Following} other {{counter} Following}}", + "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}", "account.follows.empty": "This user doesn't follow anyone yet.", "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} has requested to follow you", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Show boosts from @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}", + "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}", "account.unblock": "Unblock @{name}", "account.unblock_domain": "Unblock domain {domain}", "account.unblock_short": "Unblock", @@ -415,6 +415,7 @@ "limited_account_hint.title": "This profile has been hidden by the moderators of {domain}.", "link_preview.author": "By {name}", "link_preview.more_from_author": "More from {name}", + "link_preview.shares": "{count, plural, one {{counter} post} other {{counter} posts}}", "lists.account.add": "Add to list", "lists.account.remove": "Remove from list", "lists.delete": "Delete list", @@ -699,13 +700,13 @@ "server_banner.about_active_users": "People using this server during the last 30 days (Monthly Active Users)", "server_banner.active_users": "active users", "server_banner.administered_by": "Administered by:", - "server_banner.introduction": "{domain} is part of the decentralized social network powered by {mastodon}.", - "server_banner.learn_more": "Learn more", + "server_banner.is_one_of_many": "{domain} is one of the many independent Mastodon servers you can use to participate in the fediverse.", "server_banner.server_stats": "Server stats:", "sign_in_banner.create_account": "Create account", + "sign_in_banner.follow_anyone": "Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.", + "sign_in_banner.mastodon_is": "Mastodon is the best way to keep up with what's happening.", "sign_in_banner.sign_in": "Login", "sign_in_banner.sso_redirect": "Login or Register", - "sign_in_banner.text": "Login to follow profiles or hashtags, favorite, share and reply to posts. You can also interact from your account on a different server.", "status.admin_account": "Open moderation interface for @{name}", "status.admin_domain": "Open moderation interface for {domain}", "status.admin_status": "Open this post in the moderation interface", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 2dbbf78773..e7cfc03468 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -35,9 +35,7 @@ "account.follow_back": "Sekvu reen", "account.followers": "Sekvantoj", "account.followers.empty": "Ankoraŭ neniu sekvas ĉi tiun uzanton.", - "account.followers_counter": "{count, plural, one{{counter} Sekvanto} other {{counter} Sekvantoj}}", "account.following": "Sekvatoj", - "account.following_counter": "{count, plural, one {{counter} Sekvato} other {{counter} Sekvatoj}}", "account.follows.empty": "La uzanto ankoraŭ ne sekvas iun ajn.", "account.go_to_profile": "Iri al profilo", "account.hide_reblogs": "Kaŝi diskonigojn de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} petis sekvi vin", "account.share": "Diskonigi la profilon de @{name}", "account.show_reblogs": "Montri diskonigojn de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Afiŝo} other {{counter} Afiŝoj}}", "account.unblock": "Malbloki @{name}", "account.unblock_domain": "Malbloki la domajnon {domain}", "account.unblock_short": "Malbloki", @@ -596,13 +593,10 @@ "server_banner.about_active_users": "Personoj uzantaj ĉi tiun servilon dum la lastaj 30 tagoj (Aktivaj Uzantoj Monate)", "server_banner.active_users": "aktivaj uzantoj", "server_banner.administered_by": "Administrata de:", - "server_banner.introduction": "{domain} apartenas al la malcentra socia retejo povigita de {mastodon}.", - "server_banner.learn_more": "Lernu pli", "server_banner.server_stats": "Statistikoj de la servilo:", "sign_in_banner.create_account": "Krei konton", "sign_in_banner.sign_in": "Saluti", "sign_in_banner.sso_redirect": "Ensalutu aŭ Registriĝi", - "sign_in_banner.text": "Ensalutu por sekvi profilojn aŭ haŝetikedojn, ŝatatajn, dividi kaj respondi afiŝojn. Vi ankaŭ povas interagi de via konto sur alia servilo.", "status.admin_account": "Malfermi fasadon de moderigado por @{name}", "status.admin_domain": "Malfermu moderigan interfacon por {domain}", "status.admin_status": "Malfermi ĉi tiun mesaĝon en la kontrola interfaco", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 4c30bfa25f..28e8de9237 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -37,7 +37,7 @@ "account.followers.empty": "Todavía nadie sigue a este usuario.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.following": "Siguiendo", - "account.following_counter": "{count, plural, other {Siguiendo a {counter}}}", + "account.following_counter": "{count, plural, one {siguiendo a {counter}} other {siguiendo a {counter}}}", "account.follows.empty": "Todavía este usuario no sigue a nadie.", "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar adhesiones de @{name}", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Este perfil fue ocultado por los moderadores de {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Más de {name}", + "link_preview.shares": "{count, plural, one {{counter} mensaje} other {{counter} mensajes}}", "lists.account.add": "Agregar a lista", "lists.account.remove": "Quitar de lista", "lists.delete": "Eliminar lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Personas usando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.active_users": "usuarios activos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} es parte de la red social descentralizada con la tecnología de {mastodon}.", - "server_banner.learn_more": "Aprendé más", + "server_banner.is_one_of_many": "{domain} es uno de los muchos servidores de Mastodon independientes que podés usar para participar en el Fediverso.", "server_banner.server_stats": "Estadísticas del servidor:", "sign_in_banner.create_account": "Crear cuenta", + "sign_in_banner.follow_anyone": "Seguí a cualquiera cuenta a través del Fediverso y leé todo en orden cronológico. Nada de algoritmos, publicidad o titulares engañosos.", + "sign_in_banner.mastodon_is": "Mastodon es la mejor manera de mantenerse al día sobre lo que está sucediendo.", "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Iniciá sesión o registrate", - "sign_in_banner.text": "Iniciá sesión para seguir cuentas o etiquetas, marcar mensajes como favoritos, compartirlos y responderlos. También podés interactuar desde tu cuenta en un servidor diferente.", "status.admin_account": "Abrir interface de moderación para @{name}", "status.admin_domain": "Abrir interface de moderación para {domain}", "status.admin_status": "Abrir este mensaje en la interface de moderación", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 564d7ec57f..c10a161015 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -35,9 +35,9 @@ "account.follow_back": "Seguir también", "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", - "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}", + "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.following": "Siguiendo", - "account.following_counter": "{count, plural, other {{counter} Siguiendo}}", + "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}", "account.follows.empty": "Este usuario todavía no sigue a nadie.", "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar retoots de @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ha solicitado seguirte", "account.share": "Compartir el perfil de @{name}", "account.show_reblogs": "Mostrar retoots de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", + "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "account.unblock": "Desbloquear a @{name}", "account.unblock_domain": "Mostrar a {domain}", "account.unblock_short": "Desbloquear", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Más de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "lists.account.add": "Añadir a lista", "lists.account.remove": "Quitar de lista", "lists.delete": "Borrar lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Personas utilizando este servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.active_users": "usuarios activos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} es parte de la red social descentralizada gestionada por {mastodon}.", - "server_banner.learn_more": "Saber más", + "server_banner.is_one_of_many": "{domain} es uno de los varios servidores independientes de Mastodon que puedes usar para participar en el fediverso.", "server_banner.server_stats": "Estadísticas del servidor:", "sign_in_banner.create_account": "Crear cuenta", + "sign_in_banner.follow_anyone": "Sigue a cualquier persona en el fediverso y velo todo en orden cronológico. Sin algoritmos, sin anuncios o titulares engañosos.", + "sign_in_banner.mastodon_is": "Mastodon es el mejor modo de mantenerse al día sobre qué está ocurriendo.", "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Iniciar sesión o Registrarse", - "sign_in_banner.text": "Inicia sesión para seguir perfiles o etiquetas, así como marcar como favoritas, compartir y responder a publicaciones. También puedes interactuar desde tu cuenta en un servidor diferente.", "status.admin_account": "Abrir interfaz de moderación para @{name}", "status.admin_domain": "Abrir interfaz de moderación para {domain}", "status.admin_status": "Abrir este estado en la interfaz de moderación", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 14d3bf0dda..259fc1795b 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -35,9 +35,9 @@ "account.follow_back": "Seguir también", "account.followers": "Seguidores", "account.followers.empty": "Todavía nadie sigue a este usuario.", - "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidores}}", + "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.following": "Siguiendo", - "account.following_counter": "{count, plural, other {Siguiendo a {counter}}}", + "account.following_counter": "{count, plural, one {{counter} siguiendo} other {{counter} siguiendo}}", "account.follows.empty": "Este usuario todavía no sigue a nadie.", "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ha solicitado seguirte", "account.share": "Compartir el perfil de @{name}", "account.show_reblogs": "Mostrar impulsos de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicaciones}}", + "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "account.unblock": "Desbloquear a @{name}", "account.unblock_domain": "Desbloquear dominio {domain}", "account.unblock_short": "Desbloquear", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Este perfil ha sido ocultado por los moderadores de {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Más de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicaciones}}", "lists.account.add": "Añadir a lista", "lists.account.remove": "Quitar de lista", "lists.delete": "Borrar lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Usuarios activos en el servidor durante los últimos 30 días (Usuarios Activos Mensuales)", "server_banner.active_users": "usuarios activos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} es parte de la red social descentralizada liderada por {mastodon}.", - "server_banner.learn_more": "Saber más", + "server_banner.is_one_of_many": "{domain} es uno de los varios servidores independientes de Mastodon que puedes usar para participar en el fediverso.", "server_banner.server_stats": "Estadísticas del servidor:", "sign_in_banner.create_account": "Crear cuenta", + "sign_in_banner.follow_anyone": "Sigue a cualquier persona en el fediverso y velo todo en orden cronológico. Sin algoritmos, sin anuncios o titulares engañosos.", + "sign_in_banner.mastodon_is": "Mastodon es el mejor modo de mantenerse al día sobre qué está ocurriendo.", "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Iniciar sesión o Registrarse", - "sign_in_banner.text": "Inicia sesión para seguir perfiles o etiquetas, así como marcar como favoritas, compartir y responder a publicaciones. También puedes interactuar desde tu cuenta en un servidor diferente.", "status.admin_account": "Abrir interfaz de moderación para @{name}", "status.admin_domain": "Abrir interfaz de moderación para {domain}", "status.admin_status": "Abrir esta publicación en la interfaz de moderación", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index b2759d66c6..94f5ef5d9e 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -35,9 +35,7 @@ "account.follow_back": "Jälgi vastu", "account.followers": "Jälgijad", "account.followers.empty": "Keegi ei jälgi veel seda kasutajat.", - "account.followers_counter": "{count, plural, one {{counter} jälgija} other {{counter} jälgijat}}", "account.following": "Jälgib", - "account.following_counter": "{count, plural, one {{counter} jälgitav} other {{counter} jälgitavat}}", "account.follows.empty": "See kasutaja ei jälgi veel kedagi.", "account.go_to_profile": "Mine profiilile", "account.hide_reblogs": "Peida @{name} jagamised", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} on taodelnud sinu jälgimist", "account.share": "Jaga @{name} profiili", "account.show_reblogs": "Näita @{name} jagamisi", - "account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}", "account.unblock": "Eemalda blokeering @{name}", "account.unblock_domain": "Tee {domain} nähtavaks", "account.unblock_short": "Eemalda blokeering", @@ -147,14 +144,14 @@ "compose.published.body": "Postitus avaldatud.", "compose.published.open": "Ava", "compose.saved.body": "Postitus salvestatud.", - "compose_form.direct_message_warning_learn_more": "Vaata täpsemalt", + "compose_form.direct_message_warning_learn_more": "Vaata lisa", "compose_form.encryption_warning": "Postitused Mastodonis ei ole otsast-otsani krüpteeritud. Ära jaga mingeid delikaatseid andmeid Mastodoni kaudu.", "compose_form.hashtag_warning": "See postitus ei ilmu ühegi märksõna all, kuna pole avalik. Vaid avalikud postitused on märksõnade kaudu leitavad.", "compose_form.lock_disclaimer": "Su konto ei ole {locked}. Igaüks saab sind jälgida, et näha su ainult-jälgijatele postitusi.", "compose_form.lock_disclaimer.lock": "lukus", "compose_form.placeholder": "Millest mõtled?", "compose_form.poll.duration": "Küsitluse kestus", - "compose_form.poll.multiple": "Valikvastustega", + "compose_form.poll.multiple": "Mitu vastust", "compose_form.poll.option_placeholder": "Valik {number}", "compose_form.poll.single": "Vali üks", "compose_form.poll.switch_to_multiple": "Muuda küsitlust mitmikvaliku lubamiseks", @@ -297,6 +294,7 @@ "filter_modal.select_filter.subtitle": "Kasuta olemasolevat kategooriat või loo uus", "filter_modal.select_filter.title": "Filtreeri seda postitust", "filter_modal.title.status": "Postituse filtreerimine", + "filtered_notifications_banner.mentions": "{count, plural, one {mainimine} other {mainimist}}", "filtered_notifications_banner.pending_requests": "Teateid {count, plural, =0 {mitte üheltki} one {ühelt} other {#}} inimeselt, keda võid teada", "filtered_notifications_banner.title": "Filtreeritud teavitused", "firehose.all": "Kõik", @@ -305,15 +303,19 @@ "follow_request.authorize": "Autoriseeri", "follow_request.reject": "Hülga", "follow_requests.unlocked_explanation": "Kuigi su konto pole lukustatud, soovitab {domain} personal siiski nende kontode jälgimistaotlused käsitsi üle vaadata.", - "follow_suggestions.curated_suggestion": "Teiste valitud", + "follow_suggestions.curated_suggestion": "Meeskonna valitud", "follow_suggestions.dismiss": "Ära enam näita", - "follow_suggestions.hints.featured": "Selle kasutajaprofiili on soovitanud {domain} kasutajad.", - "follow_suggestions.hints.friends_of_friends": "See kasutajaprofiil on jälgitavate seas populaarne.", + "follow_suggestions.featured_longer": "Käsitsi valitud {domain} meeskonna poolt", + "follow_suggestions.friends_of_friends_longer": "Populaarne inimeste hulgas, keda jälgid", + "follow_suggestions.hints.featured": "Selle kasutajaprofiili on soovitanud {domain} meeskond.", + "follow_suggestions.hints.friends_of_friends": "See kasutajaprofiil on sinu jälgitavate seas populaarne.", "follow_suggestions.hints.most_followed": "See on {domain} enim jälgitud kasutajaprofiil.", - "follow_suggestions.hints.most_interactions": "See on {domain} viimasel ajal enim tähelepanu saanud kasutajaprofiil.", + "follow_suggestions.hints.most_interactions": "See kasutajaprofiil on viimasel ajal {domain} saanud palju tähelepanu.", "follow_suggestions.hints.similar_to_recently_followed": "See kasutajaprofiil sarnaneb neile, mida oled hiljuti jälgima asunud.", "follow_suggestions.personalized_suggestion": "Isikupärastatud soovitus", "follow_suggestions.popular_suggestion": "Popuplaarne soovitus", + "follow_suggestions.popular_suggestion_longer": "Populaarne kohas {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Sarnane profiilile, mida hiljuti jälgima hakkasid", "follow_suggestions.view_all": "Vaata kõiki", "follow_suggestions.who_to_follow": "Keda jälgida", "followed_tags": "Jälgitavad märksõnad", @@ -409,6 +411,8 @@ "limited_account_hint.action": "Näita profilli sellegipoolest", "limited_account_hint.title": "See profiil on peidetud {domain} moderaatorite poolt.", "link_preview.author": "{name} poolt", + "link_preview.more_from_author": "Veel kasutajalt {name}", + "link_preview.shares": "{count, plural, one {{counter} postitus} other {{counter} postitust}}", "lists.account.add": "Lisa nimekirja", "lists.account.remove": "Eemalda nimekirjast", "lists.delete": "Kustuta nimekiri", @@ -468,13 +472,22 @@ "notification.follow": "{name} alustas su jälgimist", "notification.follow_request": "{name} soovib sind jälgida", "notification.mention": "{name} mainis sind", + "notification.moderation-warning.learn_more": "Vaata lisa", + "notification.moderation_warning": "Said modereerimise hoiatuse", + "notification.moderation_warning.action_delete_statuses": "Mõni su postitus on eemaldatud.", + "notification.moderation_warning.action_disable": "Su konto on keelatud.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Mõni su postitustest on märgitud kui tundlik.", + "notification.moderation_warning.action_none": "Su konto on saanud modereerimise hoiatuse.", + "notification.moderation_warning.action_sensitive": "Su postitused märgitakse nüüdsest tundlikuks.", + "notification.moderation_warning.action_silence": "Su kontole pandi piirang.", + "notification.moderation_warning.action_suspend": "Su konto on peatatud.", "notification.own_poll": "Su küsitlus on lõppenud", "notification.poll": "Küsitlus, milles osalesid, on lõppenud", "notification.reblog": "{name} jagas edasi postitust", "notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}", "notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.", "notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one {# konto} other {# kontot}}, mida jälgid.", - "notification.relationships_severance_event.learn_more": "Saa rohkem teada", + "notification.relationships_severance_event.learn_more": "Vaata lisa", "notification.relationships_severance_event.user_domain_block": "Blokeerisid {target}, eemaldades oma jälgijate hulgast {followersCount} ja jälgitavate hulgast {followingCount, plural, one {# konto} other {# kontot}}.", "notification.status": "{name} just postitas", "notification.update": "{name} muutis postitust", @@ -680,13 +693,13 @@ "server_banner.about_active_users": "Inimesed, kes kasutavad seda serverit viimase 30 päeva jooksul (kuu aktiivsed kasutajad)", "server_banner.active_users": "aktiivsed kasutajad", "server_banner.administered_by": "Administraator:", - "server_banner.introduction": "{domain} on osa detsentraliseeritud sotsiaalvõrgustikust, mida võimaldab {mastodon}.", - "server_banner.learn_more": "Vaata täpsemalt", + "server_banner.is_one_of_many": "{domain} on üks paljudest sõltumatutest Mastodoni serveritest, mida saab fediversumis osalemiseks kasutada.", "server_banner.server_stats": "Serveri statistika:", "sign_in_banner.create_account": "Loo konto", + "sign_in_banner.follow_anyone": "Jälgi ükskõik keda kogu fediversumist ja näe kõike ajalises järjestuses. Ei mingeid algoritme, reklaame või klikipüüdjaid segamas.", + "sign_in_banner.mastodon_is": "Mastodon on parim viis olemaks kursis sellega, mis toimub.", "sign_in_banner.sign_in": "Logi sisse", "sign_in_banner.sso_redirect": "Sisene või registreeru", - "sign_in_banner.text": "Logi sisse, et jälgida profiile või silte, märkida lemmikuks, jagada ja vastata postitustele. Võid suhelda ka mõne teise serveri konto kaudu.", "status.admin_account": "Ava @{name} moderaatorivaates", "status.admin_domain": "Ava {domain} modeereerimisliides", "status.admin_status": "Ava postitus moderaatorivaates", diff --git a/app/javascript/mastodon/locales/eu.json b/app/javascript/mastodon/locales/eu.json index 9fae074878..97c4250d22 100644 --- a/app/javascript/mastodon/locales/eu.json +++ b/app/javascript/mastodon/locales/eu.json @@ -35,9 +35,7 @@ "account.follow_back": "Jarraitu bueltan", "account.followers": "Jarraitzaileak", "account.followers.empty": "Ez du inork erabiltzaile hau jarraitzen oraindik.", - "account.followers_counter": "{count, plural, one {Jarraitzaile {counter}} other {{counter} jarraitzaile}}", "account.following": "Jarraitzen", - "account.following_counter": "{count, plural, one {{counter} jarraitzen} other {{counter} jarraitzen}}", "account.follows.empty": "Erabiltzaile honek ez du inor jarraitzen oraindik.", "account.go_to_profile": "Joan profilera", "account.hide_reblogs": "Ezkutatu @{name} erabiltzailearen bultzadak", @@ -63,7 +61,6 @@ "account.requested_follow": "{name}-(e)k zu jarraitzeko eskaera egin du", "account.share": "Partekatu @{name} erabiltzailearen profila", "account.show_reblogs": "Erakutsi @{name} erabiltzailearen bultzadak", - "account.statuses_counter": "{count, plural, one {Bidalketa {counter}} other {{counter} bidalketa}}", "account.unblock": "Desblokeatu @{name}", "account.unblock_domain": "Berriz erakutsi {domain}", "account.unblock_short": "Desblokeatu", @@ -692,13 +689,10 @@ "server_banner.about_active_users": "Azken 30 egunetan zerbitzari hau erabili duen jendea (hilabeteko erabiltzaile aktiboak)", "server_banner.active_users": "erabiltzaile aktibo", "server_banner.administered_by": "Administratzailea(k):", - "server_banner.introduction": "{domain} zerbitzaria {mastodon} erabiltzen duen sare sozial deszentralizatuko parte da.", - "server_banner.learn_more": "Ikasi gehiago", "server_banner.server_stats": "Zerbitzariaren estatistikak:", "sign_in_banner.create_account": "Sortu kontua", "sign_in_banner.sign_in": "Hasi saioa", "sign_in_banner.sso_redirect": "Hasi saioa edo izena eman", - "sign_in_banner.text": "Hasi saioa profilak edo traolak jarraitzeko, bidalketak gogokoetara gehitzeko, partekatzeko edo erantzuteko. Zure kontutik ere komunika zaitezke beste zerbitzari ezberdin batean.", "status.admin_account": "Ireki @{name} erabiltzailearen moderazio interfazea", "status.admin_domain": "{domain}-(r)en moderazio-interfazea ireki", "status.admin_status": "Ireki bidalketa hau moderazio interfazean", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 6d6b7d612c..18f6466d48 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -35,9 +35,7 @@ "account.follow_back": "دنبال کردن متقابل", "account.followers": "پی‌گیرندگان", "account.followers.empty": "هنوز کسی پی‌گیر این کاربر نیست.", - "account.followers_counter": "{count, plural, one {{counter} پی‌گیرنده} other {{counter} پی‌گیرنده}}", "account.following": "پی می‌گیرید", - "account.following_counter": "{count, plural, one {{counter} پی‌گرفته} other {{counter} پی‌گرفته}}", "account.follows.empty": "این کاربر هنوز پی‌گیر کسی نیست.", "account.go_to_profile": "رفتن به نمایه", "account.hide_reblogs": "نهفتن تقویت‌های ‎@{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} درخواست پی‌گیریتان را داد", "account.share": "هم‌رسانی نمایهٔ ‎@{name}", "account.show_reblogs": "نمایش تقویت‌های ‎@{name}", - "account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}", "account.unblock": "رفع مسدودیت ‎@{name}", "account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}", "account.unblock_short": "رفع مسدودیت", @@ -620,13 +617,10 @@ "server_banner.about_active_users": "افرادی که در ۳۰ روز گذشته از این کارساز استفاده کرده‌اند (کاربران فعّال ماهانه)", "server_banner.active_users": "کاربر فعّال", "server_banner.administered_by": "به مدیریت:", - "server_banner.introduction": "{domain} بخشی از شبکهٔ اجتماعی نامتمرکزیست که از {mastodon} نیرو گرفته.", - "server_banner.learn_more": "بیش‌تر بیاموزید", "server_banner.server_stats": "آمار کارساز:", "sign_in_banner.create_account": "ایجاد حساب", "sign_in_banner.sign_in": "ورود", "sign_in_banner.sso_redirect": "ورود یا ثبت نام", - "sign_in_banner.text": "برای پی‌گیری نمایه‌ها یا برچسب‌ها، پسندیدن، هم‌رسانی و یا پاسخ به فرسته‌ها وارد شوید. همچنین می‌توانید این کارها را با حسابتان در کارسازی دیگر انجام دهید.", "status.admin_account": "گشودن واسط مدیریت برای ‎@{name}", "status.admin_domain": "گشودن واسط مدیریت برای ‎{domain}", "status.admin_status": "گشودن این فرسته در واسط مدیریت", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 6c2162e52c..0767dd5e37 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -96,7 +96,7 @@ "block_modal.they_cant_see_posts": "Hän ei voi enää nähdä julkaisujasi, etkä sinä voi nähdä hänen.", "block_modal.they_will_know": "Hän voi nähdä, että hänet on estetty.", "block_modal.title": "Estetäänkö käyttäjä?", - "block_modal.you_wont_see_mentions": "Et enää näe hänen julkaisujaan etkä voi seurata häntä.", + "block_modal.you_wont_see_mentions": "Et tule enää näkemään julkaisuja, joissa hänet mainitaan.", "boost_modal.combo": "Ensi kerralla voit ohittaa tämän painamalla {combo}", "bundle_column_error.copy_stacktrace": "Kopioi virheraportti", "bundle_column_error.error.body": "Pyydettyä sivua ei voitu hahmontaa. Se voi johtua virheestä koodissamme tai selaimen yhteensopivuudessa.", @@ -213,7 +213,7 @@ "domain_block_modal.block_account_instead": "Estä sen sijaan @{name}", "domain_block_modal.they_can_interact_with_old_posts": "Ihmiset tältä palvelimelta eivät voi olla vuorovaikutuksessa vanhojen julkaisujesi kanssa.", "domain_block_modal.they_cant_follow": "Kukaan tältä palvelimelta ei voi seurata sinua.", - "domain_block_modal.they_wont_know": "Hän ei saa tietää, että hänet on estetty.", + "domain_block_modal.they_wont_know": "Hän ei saa ilmoitusta tulleensa estetyksi.", "domain_block_modal.title": "Estetäänkö verkkotunnus?", "domain_block_modal.you_will_lose_followers": "Kaikki seuraajasi tältä palvelimelta poistetaan.", "domain_block_modal.you_wont_see_posts": "Et enää näe julkaisuja etkä ilmoituksia tämän palvelimen käyttäjiltä.", @@ -266,7 +266,7 @@ "empty_column.list": "Tällä listalla ei ole vielä mitään. Kun tämän listan jäsenet lähettävät uusia julkaisuja, ne näkyvät tässä.", "empty_column.lists": "Sinulla ei ole vielä yhtään listaa. Kun luot sellaisen, näkyy se tässä.", "empty_column.mutes": "Et ole mykistänyt vielä yhtään käyttäjää.", - "empty_column.notification_requests": "Kaikki kunnossa! Täällä ei ole mitään. Kun saat uusia ilmoituksia, ne näkyvät täällä asetustesi mukaisesti.", + "empty_column.notification_requests": "Olet ajan tasalla! Täällä ei ole mitään uutta kerrottavaa. Kun saat uusia ilmoituksia, ne näkyvät täällä asetustesi mukaisesti.", "empty_column.notifications": "Sinulla ei ole vielä ilmoituksia. Kun keskustelet muille, näet sen täällä.", "empty_column.public": "Täällä ei ole mitään! Kirjoita jotain julkisesti. Voit myös seurata muiden palvelimien käyttäjiä", "error.unexpected_crash.explanation": "Sivua ei voida näyttää oikein ohjelmointivirheen tai selaimen yhteensopivuusvajeen vuoksi.", @@ -308,7 +308,7 @@ "follow_requests.unlocked_explanation": "Vaikkei tiliäsi ole lukittu, palvelimen {domain} ylläpito on arvioinut, että saatat olla halukas tarkistamaan nämä seuraamispyynnöt erikseen.", "follow_suggestions.curated_suggestion": "Ehdotus ylläpidolta", "follow_suggestions.dismiss": "Älä näytä uudelleen", - "follow_suggestions.featured_longer": "Valinnut käsin palvelimen {domain} tiimi", + "follow_suggestions.featured_longer": "Palvelimen {domain} tiimin poimintoja", "follow_suggestions.friends_of_friends_longer": "Suosittu seuraamiesi ihmisten keskuudessa", "follow_suggestions.hints.featured": "Tämän profiilin on valinnut palvelimen {domain} tiimi.", "follow_suggestions.hints.friends_of_friends": "Seuraamasi käyttäjät suosivat tätä profiilia.", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Palvelimen {domain} valvojat ovat piilottaneet tämän käyttäjätilin.", "link_preview.author": "Julkaissut {name}", "link_preview.more_from_author": "Lisää käyttäjältä {name}", + "link_preview.shares": "{count, plural, one {{counter} julkaisu} other {{counter} julkaisua}}", "lists.account.add": "Lisää listalle", "lists.account.remove": "Poista listalta", "lists.delete": "Poista lista", @@ -434,13 +435,13 @@ "media_gallery.toggle_visible": "{number, plural, one {Piilota kuva} other {Piilota kuvat}}", "moved_to_account_banner.text": "Tilisi {disabledAccount} on tällä hetkellä poissa käytöstä, koska teit siirron tiliin {movedToAccount}.", "mute_modal.hide_from_notifications": "Piilota ilmoituksista", - "mute_modal.hide_options": "Piilota valinnat", - "mute_modal.indefinite": "Kunnes poistan mykistyksen häneltä", - "mute_modal.show_options": "Näytä valinnat", + "mute_modal.hide_options": "Piilota vaihtoehdot", + "mute_modal.indefinite": "Kunnes perun häntä koskevan mykistyksen", + "mute_modal.show_options": "Näytä vaihtoehdot", "mute_modal.they_can_mention_and_follow": "Hän voi mainita sinut ja seurata sinua, mutta sinä et näe häntä.", - "mute_modal.they_wont_know": "Hän ei saa tietää, että hänet on mykistetty.", + "mute_modal.they_wont_know": "Hän ei saa ilmoitusta tulleensa mykistetyksi.", "mute_modal.title": "Mykistetäänkö käyttäjä?", - "mute_modal.you_wont_see_mentions": "Et enää näe julkaisuja, joissa hänet mainitaan.", + "mute_modal.you_wont_see_mentions": "Et tule enää näkemään julkaisuja, joissa hänet mainitaan.", "mute_modal.you_wont_see_posts": "Hän voi yhä nähdä julkaisusi, mutta sinä et näe hänen.", "navigation_bar.about": "Tietoja", "navigation_bar.advanced_interface": "Avaa edistyneessä selainkäyttöliittymässä", @@ -530,11 +531,11 @@ "notifications.permission_denied": "Työpöytäilmoitukset eivät ole käytettävissä, koska selaimen käyttöoikeuspyyntö on aiemmin evätty", "notifications.permission_denied_alert": "Työpöytäilmoituksia ei voi ottaa käyttöön, koska selaimen käyttöoikeus on aiemmin estetty", "notifications.permission_required": "Työpöytäilmoitukset eivät ole käytettävissä, koska siihen tarvittavaa lupaa ei ole myönnetty.", - "notifications.policy.filter_new_accounts.hint": "Luotu {days, plural, one {viime päivänä} other {viimeisenä # päivänä}}", + "notifications.policy.filter_new_accounts.hint": "Luotu {days, plural, one {viimeisimmän päivän aikana} other {# viime päivän aikana}}", "notifications.policy.filter_new_accounts_title": "Uudet tilit", - "notifications.policy.filter_not_followers_hint": "Mukaan lukien ne, jotka ovat seuranneet sinua vähemmän kuin {days, plural, one {päivän} other {# päivää}}", + "notifications.policy.filter_not_followers_hint": "Mukaan lukien alle {days, plural, one {päivän} other {# päivän}} verran sinua seuranneet", "notifications.policy.filter_not_followers_title": "Henkilöt, jotka eivät seuraa sinua", - "notifications.policy.filter_not_following_hint": "Kunnes hyväksyt ne manuaalisesti", + "notifications.policy.filter_not_following_hint": "Kunnes hyväksyt ne omin käsin", "notifications.policy.filter_not_following_title": "Henkilöt, joita et seuraa", "notifications.policy.filter_private_mentions_hint": "Suodatetaan, ellei se vastaa omaan mainintaasi tai ellet seuraa lähettäjää", "notifications.policy.filter_private_mentions_title": "Ei-toivotut yksityismaininnat", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Palvelimen käyttäjät viimeisten 30 päivän ajalta (kuukauden aktiiviset käyttäjät)", "server_banner.active_users": "aktiivista käyttäjää", "server_banner.administered_by": "Ylläpitäjä:", - "server_banner.introduction": "{domain} kuuluu hajautettuun sosiaaliseen verkostoon, jonka voimanlähde on {mastodon}.", - "server_banner.learn_more": "Lue lisää", + "server_banner.is_one_of_many": "{domain} on yksi monista itsenäisistä Mastodon-palvelimista, joiden välityksellä voit toimia fediversumissa.", "server_banner.server_stats": "Palvelimen tilastot:", "sign_in_banner.create_account": "Luo tili", + "sign_in_banner.follow_anyone": "Seuraa kenen tahansa julkaisuja fediversumissa ja näe ne kaikki aikajärjestyksessä. Ei algoritmeja, mainoksia tai klikkausten kalastelua.", + "sign_in_banner.mastodon_is": "Mastodon on paras tapa pysyä ajan tasalla siitä, mitä ympärillä tapahtuu.", "sign_in_banner.sign_in": "Kirjaudu", "sign_in_banner.sso_redirect": "Kirjaudu tai rekisteröidy", - "sign_in_banner.text": "Kirjaudu sisään, niin voit seurata profiileja tai aihetunnisteita, lisätä julkaisuja suosikkeihin, jakaa julkaisuja ja vastata niihin. Voit olla vuorovaikutuksessa myös eri palvelimella olevalta tililtäsi.", "status.admin_account": "Avaa tilin @{name} valvontanäkymä", "status.admin_domain": "Avaa palvelimen {domain} valvontanäkymä", "status.admin_status": "Avaa julkaisu valvontanäkymässä", diff --git a/app/javascript/mastodon/locales/fil.json b/app/javascript/mastodon/locales/fil.json index 1f9b0496b9..b8a2987ef0 100644 --- a/app/javascript/mastodon/locales/fil.json +++ b/app/javascript/mastodon/locales/fil.json @@ -118,6 +118,7 @@ "confirmations.delete_list.confirm": "Tanggalin", "confirmations.delete_list.message": "Sigurado ka bang gusto mong burahin ang listahang ito?", "confirmations.discard_edit_media.confirm": "Ipagpaliban", + "confirmations.domain_block.confirm": "Harangan ang serbiro", "confirmations.edit.confirm": "Baguhin", "confirmations.reply.confirm": "Tumugon", "conversation.mark_as_read": "Markahan bilang nabasa na", @@ -186,6 +187,7 @@ "follow_request.authorize": "Tanggapin", "follow_request.reject": "Tanggihan", "follow_suggestions.dismiss": "Huwag nang ipakita muli", + "follow_suggestions.popular_suggestion_longer": "Sikat sa {domain}", "follow_suggestions.view_all": "Tingnan lahat", "follow_suggestions.who_to_follow": "Sinong maaaring sundan", "footer.about": "Tungkol dito", @@ -220,6 +222,7 @@ "link_preview.author": "Ni/ng {name}", "lists.account.add": "Idagdag sa talaan", "lists.account.remove": "Tanggalin mula sa talaan", + "lists.delete": "Burahin ang talaan", "lists.new.create": "Idagdag sa talaan", "lists.new.title_placeholder": "Bagong pangalan ng talaan", "lists.replies_policy.title": "Ipakita ang mga tugon sa:", @@ -287,9 +290,13 @@ "reply_indicator.cancel": "Ipagpaliban", "report.block": "Harangan", "report.categories.other": "Iba pa", + "report.categories.violation": "Lumalabag ang nilalaman sa isa o higit pang mga patakaran ng serbiro", + "report.category.subtitle": "Piliin ang pinakamahusay na tugma", "report.category.title": "Sabihin mo sa amin kung anong nangyari sa {type} na ito", "report.close": "Tapos na", "report.next": "Sunod", + "report.placeholder": "Mga Karagdagang Puna", + "report.reasons.dislike": "Hindi ko gusto ito", "report.reasons.violation": "Lumalabag ito sa mga panuntunan ng serbiro", "report.reasons.violation_description": "Alam mo na lumalabag ito sa mga partikular na panuntunan", "report.rules.title": "Aling mga patakaran ang nilabag?", @@ -308,7 +315,6 @@ "search_popout.recent": "Kamakailang mga paghahanap", "search_results.all": "Lahat", "search_results.see_all": "Ipakita lahat", - "server_banner.learn_more": "Matuto nang higit pa", "server_banner.server_stats": "Katayuan ng serbiro:", "status.block": "Harangan si @{name}", "status.delete": "Tanggalin", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index b77f609a2f..c27ffe0aa7 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -35,7 +35,7 @@ "account.follow_back": "Fylg aftur", "account.followers": "Fylgjarar", "account.followers.empty": "Ongar fylgjarar enn.", - "account.followers_counter": "{count, plural, one {{counter} Fylgjari} other {{counter} Fylgjarar}}", + "account.followers_counter": "{count, plural, one {{counter} fylgjari} other {{counter} fylgjarar}}", "account.following": "Fylgir", "account.following_counter": "{count, plural, one {{counter} fylgir} other {{counter} fylgja}}", "account.follows.empty": "Hesin brúkari fylgir ongum enn.", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Hesin vangin er fjaldur av kjakleiðarunum á {domain}.", "link_preview.author": "Av {name}", "link_preview.more_from_author": "Meira frá {name}", + "link_preview.shares": "{count, plural, one {{counter} postur} other {{counter} postar}}", "lists.account.add": "Legg afturat lista", "lists.account.remove": "Tak av lista", "lists.delete": "Strika lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Fólk, sum hava brúkt hendan ambætaran seinastu 30 dagarnar (mánaðarligir virknir brúkarar)", "server_banner.active_users": "virknir brúkarar", "server_banner.administered_by": "Umsitari:", - "server_banner.introduction": "{domain} er partur av desentrala sosiala netverkinum, sum er drivið av {mastodon}.", - "server_banner.learn_more": "Lær meira", + "server_banner.is_one_of_many": "{domain} er ein av nógvum óheftum Mastodon ambætarum, sum tú kanst brúka at luttaka í fediversinum.", "server_banner.server_stats": "Ambætarahagtøl:", "sign_in_banner.create_account": "Stovna kontu", + "sign_in_banner.follow_anyone": "Fylg ein og hvønn í fediversinum og síggj alt í tíðarrøð. Ongar algoritmur, ongar lýsingar og einki klikkbeit í eygsjón.", + "sign_in_banner.mastodon_is": "Mastodon er best mátin at fylgja við í tí, sum hendir.", "sign_in_banner.sign_in": "Rita inn", "sign_in_banner.sso_redirect": "Rita inn ella Skráset teg", - "sign_in_banner.text": "Innrita fyri at fylgja vangum og frámerkjum, dáma, deila og svara postum. Tú kanst eisini brúka kontuna til at samvirka á einum øðrum ambætara.", "status.admin_account": "Lat kjakleiðaramarkamót upp fyri @{name}", "status.admin_domain": "Lat umsjónarmarkamót upp fyri {domain}", "status.admin_status": "Lat hendan postin upp í kjakleiðaramarkamótinum", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 9c14d05d5c..4324855003 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -35,9 +35,7 @@ "account.follow_back": "S'abonner en retour", "account.followers": "abonné·e·s", "account.followers.empty": "Personne ne suit ce compte pour l'instant.", - "account.followers_counter": "{count, plural, one {{counter} Abonné·e} other {{counter} Abonné·e·s}}", "account.following": "Abonné·e", - "account.following_counter": "{count, plural, one {{counter} Abonnement} other {{counter} Abonnements}}", "account.follows.empty": "Ce compte ne suit personne présentement.", "account.go_to_profile": "Voir ce profil", "account.hide_reblogs": "Masquer les boosts de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} a demandé à vous suivre", "account.share": "Partager le profil de @{name}", "account.show_reblogs": "Afficher les boosts de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Publication} other {{counter} Publications}}", "account.unblock": "Débloquer @{name}", "account.unblock_domain": "Débloquer le domaine {domain}", "account.unblock_short": "Débloquer", @@ -680,13 +677,10 @@ "server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Comptes actifs mensuellement)", "server_banner.active_users": "comptes actifs", "server_banner.administered_by": "Administré par:", - "server_banner.introduction": "{domain} fait partie du réseau social décentralisé propulsé par {mastodon}.", - "server_banner.learn_more": "En savoir plus", "server_banner.server_stats": "Statistiques du serveur:", "sign_in_banner.create_account": "Créer un compte", "sign_in_banner.sign_in": "Se connecter", "sign_in_banner.sso_redirect": "Se connecter ou s’inscrire", - "sign_in_banner.text": "Identifiez-vous pour suivre des profils ou des hashtags, ajouter des favoris, partager et répondre à des publications. Vous pouvez également interagir depuis votre compte sur un autre serveur.", "status.admin_account": "Ouvrir l’interface de modération pour @{name}", "status.admin_domain": "Ouvrir l’interface de modération pour {domain}", "status.admin_status": "Ouvrir ce message dans l’interface de modération", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 36ec673a47..cd67cda539 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -35,9 +35,7 @@ "account.follow_back": "S'abonner en retour", "account.followers": "Abonné·e·s", "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.", - "account.followers_counter": "{count, plural, one {{counter} Abonné·e} other {{counter} Abonné·e·s}}", "account.following": "Abonnements", - "account.following_counter": "{count, plural, one {{counter} Abonnement} other {{counter} Abonnements}}", "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.", "account.go_to_profile": "Aller au profil", "account.hide_reblogs": "Masquer les partages de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} a demandé à vous suivre", "account.share": "Partager le profil de @{name}", "account.show_reblogs": "Afficher les partages de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Message} other {{counter} Messages}}", "account.unblock": "Débloquer @{name}", "account.unblock_domain": "Débloquer le domaine {domain}", "account.unblock_short": "Débloquer", @@ -680,13 +677,10 @@ "server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Comptes actifs mensuellement)", "server_banner.active_users": "comptes actifs", "server_banner.administered_by": "Administré par :", - "server_banner.introduction": "{domain} fait partie du réseau social décentralisé propulsé par {mastodon}.", - "server_banner.learn_more": "En savoir plus", "server_banner.server_stats": "Statistiques du serveur :", "sign_in_banner.create_account": "Créer un compte", "sign_in_banner.sign_in": "Se connecter", "sign_in_banner.sso_redirect": "Se connecter ou s’inscrire", - "sign_in_banner.text": "Identifiez-vous pour suivre des profils ou des hashtags, ajouter des favoris, partager et répondre à des messages. Vous pouvez également interagir depuis votre compte sur un autre serveur.", "status.admin_account": "Ouvrir l’interface de modération pour @{name}", "status.admin_domain": "Ouvrir l’interface de modération pour {domain}", "status.admin_status": "Ouvrir ce message dans l’interface de modération", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 8048045c5c..d787c16bf3 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -35,9 +35,7 @@ "account.follow_back": "Weromfolgje", "account.followers": "Folgers", "account.followers.empty": "Noch net ien folget dizze brûker.", - "account.followers_counter": "{count, plural, one {{counter} folger} other {{counter} folgers}}", "account.following": "Folgjend", - "account.following_counter": "{count, plural, one {{counter} folgjend} other {{counter} folgjend}}", "account.follows.empty": "Dizze brûker folget noch net ien.", "account.go_to_profile": "Gean nei profyl", "account.hide_reblogs": "Boosts fan @{name} ferstopje", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} hat dy in folchfersyk stjoerd", "account.share": "Profyl fan @{name} diele", "account.show_reblogs": "Boosts fan @{name} toane", - "account.statuses_counter": "{count, plural, one {{counter} berjocht} other {{counter} berjochten}}", "account.unblock": "@{name} deblokkearje", "account.unblock_domain": "Domein {domain} deblokkearje", "account.unblock_short": "Deblokkearje", @@ -655,13 +652,10 @@ "server_banner.about_active_users": "Oantal brûkers yn de ôfrûne 30 dagen (MAU)", "server_banner.active_users": "warbere brûkers", "server_banner.administered_by": "Beheard troch:", - "server_banner.introduction": "{domain} is ûnderdiel fan it desintralisearre sosjale netwurk {mastodon}.", - "server_banner.learn_more": "Mear ynfo", "server_banner.server_stats": "Serverstatistiken:", "sign_in_banner.create_account": "Account registrearje", "sign_in_banner.sign_in": "Oanmelde", "sign_in_banner.sso_redirect": "Oanmelde of Registrearje", - "sign_in_banner.text": "Meld jo oan, om profilen of hashtags te folgjen, berjochten favoryt te meitsjen, te dielen en te beäntwurdzjen of om fan jo account út op in oare server mei oaren ynteraksje te hawwen.", "status.admin_account": "Moderaasje-omjouwing fan @{name} iepenje", "status.admin_domain": "Moderaasje-omjouwing fan {domain} iepenje", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index c71effe06d..edf7618148 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -31,9 +31,7 @@ "account.follow": "Lean", "account.followers": "Leantóirí", "account.followers.empty": "Ní leanann éinne an t-úsáideoir seo fós.", - "account.followers_counter": "{count, plural, one {Leantóir amháin} other {{counter} Leantóir}}", "account.following": "Ag leanúint", - "account.following_counter": "{count, plural, one {Ag leanúint cúntas amháin} other {Ag leanúint {counter} cúntas}}", "account.follows.empty": "Ní leanann an t-úsáideoir seo duine ar bith fós.", "account.go_to_profile": "Téigh go dtí próifíl", "account.hide_reblogs": "Folaigh moltaí ó @{name}", @@ -55,7 +53,6 @@ "account.requested_follow": "D'iarr {name} ort do chuntas a leanúint", "account.share": "Roinn próifíl @{name}", "account.show_reblogs": "Taispeáin moltaí ó @{name}", - "account.statuses_counter": "{count, plural, one {Postáil amháin} other {{counter} Postáil}}", "account.unblock": "Bain bac de @{name}", "account.unblock_domain": "Bain bac den ainm fearainn {domain}", "account.unblock_short": "Bain bac de", @@ -438,7 +435,6 @@ "search_results.statuses": "Postálacha", "search_results.title": "Cuardaigh ar thóir {q}", "server_banner.active_users": "úsáideoirí gníomhacha", - "server_banner.learn_more": "Tuilleadh eolais", "server_banner.server_stats": "Staitisticí freastalaí:", "sign_in_banner.create_account": "Cruthaigh cuntas", "sign_in_banner.sign_in": "Sinigh isteach", diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 9e79793de1..fec025045c 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -35,9 +35,7 @@ "account.follow_back": "Lean air ais", "account.followers": "Luchd-leantainn", "account.followers.empty": "Chan eil neach sam bith a’ leantainn air a’ chleachdaiche seo fhathast.", - "account.followers_counter": "{count, plural, one {{counter} neach-leantainn} two {{counter} neach-leantainn} few {{counter} luchd-leantainn} other {{counter} luchd-leantainn}}", "account.following": "A’ leantainn", - "account.following_counter": "{count, plural, one {A’ leantainn {counter}} two {A’ leantainn {counter}} few {A’ leantainn {counter}} other {A’ leantainn {counter}}}", "account.follows.empty": "Chan eil an cleachdaiche seo a’ leantainn neach sam bith fhathast.", "account.go_to_profile": "Tadhail air a’ phròifil", "account.hide_reblogs": "Falaich na brosnachaidhean o @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "Dh’iarr {name} ’gad leantainn", "account.share": "Co-roinn a’ phròifil aig @{name}", "account.show_reblogs": "Seall na brosnachaidhean o @{name}", - "account.statuses_counter": "{count, plural, one {{counter} phost} two {{counter} phost} few {{counter} postaichean} other {{counter} post}}", "account.unblock": "Dì-bhac @{name}", "account.unblock_domain": "Dì-bhac an àrainn {domain}", "account.unblock_short": "Dì-bhac", @@ -684,13 +681,10 @@ "server_banner.about_active_users": "Daoine a chleachd am frithealaiche seo rè an 30 latha mu dheireadh (Cleachdaichean gnìomhach gach mìos)", "server_banner.active_users": "cleachdaichean gnìomhach", "server_banner.administered_by": "Rianachd le:", - "server_banner.introduction": "Tha {domain} am measg an lìonraidh shòisealta sgaoilte le cumhachd {mastodon}.", - "server_banner.learn_more": "Barrachd fiosrachaidh", "server_banner.server_stats": "Stadastaireachd an fhrithealaiche:", "sign_in_banner.create_account": "Cruthaich cunntas", "sign_in_banner.sign_in": "Clàraich a-steach", "sign_in_banner.sso_redirect": "Clàraich a-steach no clàraich leinn", - "sign_in_banner.text": "Clàraich a-steach a leantainn phròifilean no thagaichean hais, a’ cur postaichean ris na h-annsachdan ’s ’gan co-roinneadh is freagairt dhaibh. ’S urrainn dhut gnìomh a ghabhail le cunntas o fhrithealaiche eile cuideachd.", "status.admin_account": "Fosgail eadar-aghaidh na maorsainneachd dha @{name}", "status.admin_domain": "Fosgail eadar-aghaidh na maorsainneachd dha {domain}", "status.admin_status": "Fosgail am post seo ann an eadar-aghaidh na maorsainneachd", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 0847b8bf0d..03287c7e52 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -35,9 +35,9 @@ "account.follow_back": "Seguir tamén", "account.followers": "Seguidoras", "account.followers.empty": "Aínda ninguén segue esta usuaria.", - "account.followers_counter": "{count, plural, one {{counter} Seguidora} other {{counter} Seguidoras}}", + "account.followers_counter": "{count, plural, one {{counter} seguidora} other {{counter} seguidoras}}", "account.following": "Seguindo", - "account.following_counter": "{count, plural, one {{counter} Seguindo} other {{counter} Seguindo}}", + "account.following_counter": "{count, plural, one {{counter} seguimento} other {{counter} seguimentos}}", "account.follows.empty": "Esta usuaria aínda non segue a ninguén.", "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Agochar promocións de @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} solicitou seguirte", "account.share": "Compartir o perfil de @{name}", "account.show_reblogs": "Amosar compartidos de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Publicación} other {{counter} Publicacións}}", + "account.statuses_counter": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Amosar {domain}", "account.unblock_short": "Desbloquear", @@ -224,7 +224,7 @@ "domain_pill.their_server": "O seu fogar dixital, onde están as súas publicacións.", "domain_pill.their_username": "O seu identificador único no seu servidor. É posible atopar usuarias co mesmo nome de usuaria en diferentes servidores.", "domain_pill.username": "Nome de usuaria", - "domain_pill.whats_in_a_handle": "Que é o alcume?", + "domain_pill.whats_in_a_handle": "As partes do alcume?", "domain_pill.who_they_are": "O alcume dinos quen é esa persoa e onde está, para que poidas interactuar con ela en toda a web social de .", "domain_pill.who_you_are": "Como o teu alcume informa de quen es e onde estás, as persoas poden interactuar contigo desde toda a web social de .", "domain_pill.your_handle": "O teu alcume:", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Este perfil foi agochado pola moderación de {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Máis de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicación} other {{counter} publicacións}}", "lists.account.add": "Engadir á listaxe", "lists.account.remove": "Eliminar da listaxe", "lists.delete": "Eliminar listaxe", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Persoas que usaron este servidor nos últimos 30 días (Usuarias Activas Mensuais)", "server_banner.active_users": "usuarias activas", "server_banner.administered_by": "Administrada por:", - "server_banner.introduction": "{domain} é parte da rede social descentralizada que funciona grazas a {mastodon}.", - "server_banner.learn_more": "Saber máis", + "server_banner.is_one_of_many": "{domain} é un dos moitos servidores Mastodon independentes que podes usar para participar do Fediverso.", "server_banner.server_stats": "Estatísticas do servidor:", "sign_in_banner.create_account": "Crear conta", + "sign_in_banner.follow_anyone": "Sigue a quen queiras no Fediverso e le as publicacións en orde cronolóxica. Sen algoritmos, publicidade nin titulares engañosos.", + "sign_in_banner.mastodon_is": "Mastodon é o mellor xeito de estar ao día do que acontece.", "sign_in_banner.sign_in": "Iniciar sesión", "sign_in_banner.sso_redirect": "Acceder ou Crear conta", - "sign_in_banner.text": "Inicia sesión para seguir perfís ou cancelos, marcar como favorita e responder a publicacións. Tamén podes interactuar coa túa conta noutro servidor.", "status.admin_account": "Abrir interface de moderación para @{name}", "status.admin_domain": "Abrir interface de moderación para {domain}", "status.admin_status": "Abrir esta publicación na interface de moderación", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index dddc318473..8111a56e89 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -35,9 +35,9 @@ "account.follow_back": "לעקוב בחזרה", "account.followers": "עוקבים", "account.followers.empty": "אף אחד לא עוקב אחר המשתמש הזה עדיין.", - "account.followers_counter": "{count, plural,one {עוקב אחד} other {{counter} עוקבים}}", + "account.followers_counter": "{count, plural,one {עוקב אחד} other {{count} עוקבים}}", "account.following": "נעקבים", - "account.following_counter": "{count, plural,one {עוקב אחרי {counter}}other {עוקב אחרי {counter}}}", + "account.following_counter": "{count, plural,one {עוקב אחרי {count}}other {עוקב אחרי {count}}}", "account.follows.empty": "משתמש זה עדיין לא עוקב אחרי אף אחד.", "account.go_to_profile": "מעבר לפרופיל", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ביקשו לעקוב אחריך", "account.share": "שתף את הפרופיל של @{name}", "account.show_reblogs": "הצג הדהודים מאת @{name}", - "account.statuses_counter": "{count, plural, one {הודעה} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", + "account.statuses_counter": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", "account.unblock": "להסיר חסימה ל- @{name}", "account.unblock_domain": "הסירי את החסימה של קהילת {domain}", "account.unblock_short": "הסר חסימה", @@ -415,6 +415,7 @@ "limited_account_hint.title": "פרופיל המשתמש הזה הוסתר על ידי המנחים של {domain}.", "link_preview.author": "מאת {name}", "link_preview.more_from_author": "עוד מאת {name}", + "link_preview.shares": "{count, plural, one {הודעה אחת} two {הודעותיים} many {{count} הודעות} other {{count} הודעות}}", "lists.account.add": "הוסף לרשימה", "lists.account.remove": "הסר מרשימה", "lists.delete": "מחיקת רשימה", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "משתמשים פעילים בשרת ב־30 הימים האחרונים (משתמשים פעילים חודשיים)", "server_banner.active_users": "משתמשים פעילים", "server_banner.administered_by": "מנוהל ע\"י:", - "server_banner.introduction": "{domain} הוא שרת ברשת המבוזרת {mastodon}.", - "server_banner.learn_more": "מידע נוסף", + "server_banner.is_one_of_many": "{domain} הוא שרת אחד משרתי מסטודון עצמאיים רבים שדרגם תוכלו להשתתף בפדיוורס (רשת חברתית מבוזרת).", "server_banner.server_stats": "סטטיסטיקות שרת:", "sign_in_banner.create_account": "יצירת חשבון", + "sign_in_banner.follow_anyone": "תוכלו לעקוב אחרי כל משמתמש בפדיוורס ולקרוא הכל לפי סדר הפרסום בציר הזמן. אין אלגוריתמים, פרסומות, או קליקבייט מטעם בעלי הרשת.", + "sign_in_banner.mastodon_is": "מסטודון הוא הדרך הטובה ביותר לעקוב אחרי מה שקורה.", "sign_in_banner.sign_in": "התחברות", "sign_in_banner.sso_redirect": "התחברות/הרשמה", - "sign_in_banner.text": "יש להתחבר כדי לעקוב אחרי משתמשים או תגיות, לחבב, לשתף ולענות להודעות. ניתן גם לתקשר מהחשבון שלך עם שרת אחר.", "status.admin_account": "פתח/י ממשק ניהול עבור @{name}", "status.admin_domain": "פתיחת ממשק ניהול עבור {domain}", "status.admin_status": "Open this status in the moderation interface", @@ -776,7 +777,7 @@ "timeline_hint.resources.followers": "עוקבים", "timeline_hint.resources.follows": "נעקבים", "timeline_hint.resources.statuses": "הודעות ישנות יותר", - "trends.counter_by_accounts": "{count, plural, one {אדם {count}} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}", + "trends.counter_by_accounts": "{count, plural, one {אדם אחד} other {{count} א.נשים}} {days, plural, one {מאז אתמול} two {ביומיים האחרונים} other {במשך {days} הימים האחרונים}}", "trends.trending_now": "נושאים חמים", "ui.beforeunload": "הטיוטא תאבד אם תעזבו את מסטודון.", "units.short.billion": "{count} מליארד", diff --git a/app/javascript/mastodon/locales/hi.json b/app/javascript/mastodon/locales/hi.json index 5dc99dd76e..89c71207f0 100644 --- a/app/javascript/mastodon/locales/hi.json +++ b/app/javascript/mastodon/locales/hi.json @@ -35,9 +35,7 @@ "account.follow_back": "फॉलो करें", "account.followers": "फॉलोवर", "account.followers.empty": "कोई भी इस यूज़र् को फ़ॉलो नहीं करता है", - "account.followers_counter": "{count, plural, one {{counter} अनुगामी} other {{counter} समर्थक}}", "account.following": "फॉलोइंग", - "account.following_counter": "{count, plural, one {{counter} निम्नलिखित} other {{counter} निम्नलिखित}}", "account.follows.empty": "यह यूज़र् अभी तक किसी को फॉलो नहीं करता है।", "account.go_to_profile": "प्रोफाइल में जाएँ", "account.hide_reblogs": "@{name} के बूस्ट छुपाएं", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} ने आपको फॉलो करने के लिए अनुरोध किया है", "account.share": "@{name} की प्रोफाइल शेयर करे", "account.show_reblogs": "@{name} के बूस्ट दिखाए", - "account.statuses_counter": "{count, plural, one {{counter} भोंपू} other {{counter} भोंपू}}", "account.unblock": "@{name} को अनब्लॉक करें", "account.unblock_domain": "{domain} दिखाए", "account.unblock_short": "अनब्लॉक", @@ -80,6 +77,7 @@ "admin.dashboard.retention.cohort_size": "नये उपयोगकर्ता", "admin.impact_report.instance_accounts": "ये अकाउंट प्रोफाइल मिटा देगा", "admin.impact_report.instance_followers": "हमारे यूजर्स इन फॉलोअर्स को खो देंगे", + "admin.impact_report.instance_follows": "उनके उपयोगकर्ता इतने फ़ॉलोअर खो देंगे", "admin.impact_report.title": "प्रभावकां सारांश", "alert.rate_limited.message": "कृप्या {retry_time, time, medium} के बाद दुबारा कोशिश करें", "alert.rate_limited.title": "सीमित दर", @@ -88,6 +86,7 @@ "announcement.announcement": "घोषणा", "attachments_list.unprocessed": "(असंसाधित)", "audio.hide": "हाईड ऑडियो", + "block_modal.remote_users_caveat": "हम {domain} को आपके निर्णय का सम्मान करने को कहेंगे।हालाकि इसकी आपूर्ति कि प्रत्याभूति नहीं हे। क्योंकि कुछ सर्वर ब्लॉक को अलग तरह से निभा सकते हे। अभी भी सार्वजानिक पोस्ट लोग- इन बगैर के उपयोगकर्ताओं को दिख सकती हैं।", "block_modal.show_less": "कम दिखाएं", "block_modal.show_more": "और दिखाएँ", "block_modal.they_cant_mention": "वे आपको मेंशन या फॉलो नहीं कर सकते", @@ -205,7 +204,12 @@ "dismissable_banner.dismiss": "डिसमिस", "dismissable_banner.explore_links": "इन समाचारों के बारे में लोगों द्वारा इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर अभी बात की जा रही है।", "dismissable_banner.explore_tags": "ये हैशटैग अभी इस पर और डेसेंट्रलीसेड नेटवर्क के अन्य सर्वरों पर लोगों के बीच कर्षण प्राप्त कर रहे हैं।", + "dismissable_banner.public_timeline": "यह ताजा सार्वजनिक पोस्ट है जिसका सामाजिक वेब {domain} के लोगो द्वारा अनुसरण हो रहा हैं।", "domain_block_modal.block": "सर्वर ब्लॉक करें", + "domain_block_modal.block_account_instead": "इसकी जगह यह @{name} रखें", + "domain_block_modal.they_can_interact_with_old_posts": "इस सर्वर की लोग आपकी पूरानी पोस्ट्स का अनुसरण किया जा sakta है।", + "domain_block_modal.they_cant_follow": "इस सर्वर मेसे कोई भी आपका अनुसरण नहीं कर सकता।", + "domain_block_modal.they_wont_know": "उनको पता नहीं चलेगा कि वे अवरोधित किए गए है।", "domain_block_modal.title": "डोमेन ब्लॉक करें", "domain_pill.server": "सर्वर", "domain_pill.username": "यूज़रनेम", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index ec8d62dbba..c8f6f01862 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -35,9 +35,7 @@ "account.follow_back": "Slijedi natrag", "account.followers": "Pratitelji", "account.followers.empty": "Nitko još ne prati korisnika/cu.", - "account.followers_counter": "{count, plural, one {{counter} pratitelj} other {{counter} pratitelja}}", "account.following": "Pratim", - "account.following_counter": "{count, plural, one {{counter} praćeni} few{{counter} praćena} other {{counter} praćenih}}", "account.follows.empty": "Korisnik/ca još ne prati nikoga.", "account.go_to_profile": "Idi na profil", "account.hide_reblogs": "Sakrij boostove od @{name}", @@ -62,7 +60,6 @@ "account.requested_follow": "{name} zatražio/la je praćenje", "account.share": "Podijeli profil @{name}", "account.show_reblogs": "Prikaži boostove od @{name}", - "account.statuses_counter": "{count, plural, one {{counter} toot} other {{counter} toota}}", "account.unblock": "Deblokiraj @{name}", "account.unblock_domain": "Deblokiraj domenu {domain}", "account.unblock_short": "Deblokiraj", @@ -455,8 +452,6 @@ "server_banner.about_active_users": "Popis aktivnih korisnika prošli mjesec", "server_banner.active_users": "aktivni korisnici", "server_banner.administered_by": "Administrator je:", - "server_banner.introduction": "{domain} je dio decentralizirane socijalne mreže koju pokreće {mastodon}.", - "server_banner.learn_more": "Saznaj više", "server_banner.server_stats": "Statistike poslužitelja:", "sign_in_banner.create_account": "Stvori račun", "sign_in_banner.sign_in": "Prijavi se", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index dfb4b539d8..1fcadc8f9c 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -35,9 +35,9 @@ "account.follow_back": "Viszontkövetés", "account.followers": "Követő", "account.followers.empty": "Ezt a felhasználót még senki sem követi.", - "account.followers_counter": "{count, plural, one {{counter} Követő} other {{counter} Követő}}", + "account.followers_counter": "{count, plural, one {{counter} követő} other {{counter} követő}}", "account.following": "Követve", - "account.following_counter": "{count, plural, one {{counter} Követett} other {{counter} Követett}}", + "account.following_counter": "{count, plural, one {{counter} követett} other {{counter} követett}}", "account.follows.empty": "Ez a felhasználó még senkit sem követ.", "account.go_to_profile": "Ugrás a profilhoz", "account.hide_reblogs": "@{name} megtolásainak elrejtése", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} kérte, hogy követhessen", "account.share": "@{name} profiljának megosztása", "account.show_reblogs": "@{name} megtolásainak mutatása", - "account.statuses_counter": "{count, plural, one {{counter} Bejegyzés} other {{counter} Bejegyzés}}", + "account.statuses_counter": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}", "account.unblock": "@{name} letiltásának feloldása", "account.unblock_domain": "{domain} domain tiltásának feloldása", "account.unblock_short": "Tiltás feloldása", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Ezt a profilt {domain} moderátorai elrejtették.", "link_preview.author": "{name} szerint", "link_preview.more_from_author": "Több tőle: {name}", + "link_preview.shares": "{count, plural, one {{counter} bejegyzés} other {{counter} bejegyzés}}", "lists.account.add": "Hozzáadás a listához", "lists.account.remove": "Eltávolítás a listából", "lists.delete": "Lista törlése", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Az elmúlt 30 napban ezt a kiszolgálót használók száma (Havi aktív felhasználók)", "server_banner.active_users": "aktív felhasználó", "server_banner.administered_by": "Adminisztrátor:", - "server_banner.introduction": "{domain} része egy decentralizált közösségi hálónak, melyet a {mastodon} hajt meg.", - "server_banner.learn_more": "Tudj meg többet", + "server_banner.is_one_of_many": "{domain} egy a jelentős, független Mastodon kiszolgálók közül, melyet a fediverzumban való részvételre használhatsz.", "server_banner.server_stats": "Kiszolgálóstatisztika:", "sign_in_banner.create_account": "Fiók létrehozása", + "sign_in_banner.follow_anyone": "Kövess bárkit a fediverzumon keresztül, és láss mindent időrendi sorrendben. Algoritmusok, hirdetések, kattintásvadászat nélkül.", + "sign_in_banner.mastodon_is": "A Mastodon a legjobb módja annak, hogy a történésekkel kapcsolatban naprakész maradj.", "sign_in_banner.sign_in": "Bejelentkezés", "sign_in_banner.sso_redirect": "Bejelentkezés vagy regisztráció", - "sign_in_banner.text": "Jelentkezz be profilok vagy hashtagek követéséhez, kedvencnek jelöléséhez, bejegyzések megosztásához, megválaszolásához. A fiókodból más kiszolgálókon is kommunikálhatsz.", "status.admin_account": "Moderációs felület megnyitása @{name} fiókhoz", "status.admin_domain": "Moderációs felület megnyitása {domain} esetében", "status.admin_status": "Bejegyzés megnyitása a moderációs felületen", diff --git a/app/javascript/mastodon/locales/hy.json b/app/javascript/mastodon/locales/hy.json index 7310104bf9..b4abe9bf09 100644 --- a/app/javascript/mastodon/locales/hy.json +++ b/app/javascript/mastodon/locales/hy.json @@ -28,9 +28,7 @@ "account.follow": "Հետեւել", "account.followers": "Հետեւողներ", "account.followers.empty": "Այս օգտատիրոջը դեռ ոչ մէկ չի հետեւում։", - "account.followers_counter": "{count, plural, one {{counter} Հետեւորդ} other {{counter} Հետեւորդ}}", "account.following": "Հետեւած", - "account.following_counter": "{count, plural, one {{counter} Հետեւած} other {{counter} Հետեւած}}", "account.follows.empty": "Այս օգտատէրը դեռ ոչ մէկի չի հետեւում։", "account.go_to_profile": "Գնալ անձնական հաշիւ", "account.hide_reblogs": "Թաքցնել @{name}֊ի տարածածները", @@ -52,7 +50,6 @@ "account.requested_follow": "{name}-ը ցանկանում է հետեւել քեզ", "account.share": "Կիսուել @{name}֊ի էջով", "account.show_reblogs": "Ցուցադրել @{name}֊ի տարածածները", - "account.statuses_counter": "{count, plural, one {{counter} Գրառում} other {{counter} Գրառումներ}}", "account.unblock": "Ապաարգելափակել @{name}֊ին", "account.unblock_domain": "Ցուցադրել {domain} թաքցուած տիրոյթի գրառումները", "account.unblock_short": "Արգելաբացել", @@ -441,8 +438,6 @@ "search_results.title": "Որոնել {q}-ն", "server_banner.active_users": "ակտիւ մարդիկ", "server_banner.administered_by": "Կառաւարող", - "server_banner.introduction": "{domain}-ը հանդիասնում է ապակենտրոն սոց. ցանցի մաս, ստեղծուած {mastodon}-ով։\n", - "server_banner.learn_more": "Իմանալ աւելին", "server_banner.server_stats": "Սերուերի վիճակը", "sign_in_banner.create_account": "Ստեղծել հաշիւ", "sign_in_banner.sign_in": "Մուտք", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index ed33a45d45..ace6402ee1 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -354,7 +354,7 @@ "home.pending_critical_update.link": "Vider actualisationes", "home.pending_critical_update.title": "Actualisation de securitate critic disponibile!", "home.show_announcements": "Monstrar annuncios", - "interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e salveguarda pro plus tarde.", + "interaction_modal.description.favourite": "Con un conto sur Mastodon, tu pote marcar iste message como favorite pro informar le autor que tu lo apprecia e lo salva pro plus tarde.", "interaction_modal.description.follow": "Con un conto sur Mastodon, tu pote sequer {name} e reciper su messages in tu fluxo de initio.", "interaction_modal.description.reblog": "Con un conto sur Mastodon, tu pote impulsar iste message pro condivider lo con tu proprie sequitores.", "interaction_modal.description.reply": "Con un conto sur Mastodon, tu pote responder a iste message.", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Iste profilo ha essite celate per le moderatores de {domain}.", "link_preview.author": "Per {name}", "link_preview.more_from_author": "Plus de {name}", + "link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}", "lists.account.add": "Adder al lista", "lists.account.remove": "Remover del lista", "lists.delete": "Deler lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Personas que ha usate iste servitor in le ultime 30 dies (usatores active per mense)", "server_banner.active_users": "usatores active", "server_banner.administered_by": "Administrate per:", - "server_banner.introduction": "{domain} face parte del rete social decentralisate actionate per {mastodon}.", - "server_banner.learn_more": "Apprender plus", + "server_banner.is_one_of_many": "{domain} es un de multe servitores independente de Mastodon que tu pote usar pro participar in le fediverso.", "server_banner.server_stats": "Statos del servitor:", "sign_in_banner.create_account": "Crear un conto", + "sign_in_banner.follow_anyone": "Seque quicunque in le fediverso, e tu videra toto in ordine chronologic. Sin algorithmo, sin publicitate, sin titulos de esca.", + "sign_in_banner.mastodon_is": "Mastodon es le melior maniera de sequer lo que passa.", "sign_in_banner.sign_in": "Aperir session", "sign_in_banner.sso_redirect": "Aperir session o crear conto", - "sign_in_banner.text": "Aperi session pro sequer profilos o hashtags, marcar messages como favorite, e condivider e responder a messages. Tu pote etiam interager desde tu conto sur un altere servitor.", "status.admin_account": "Aperir le interfacie de moderation pro @{name}", "status.admin_domain": "Aperir le interfacie de moderation pro {domain}", "status.admin_status": "Aperir iste message in le interfacie de moderation", @@ -763,7 +764,7 @@ "status.unmute_conversation": "Non plus silentiar conversation", "status.unpin": "Disfixar del profilo", "subscribed_languages.lead": "Solmente le messages in le linguas seligite apparera in tu chronologias de initio e de listas post le cambiamento. Selige necun pro reciper messages in tote le linguas.", - "subscribed_languages.save": "Salveguardar le cambiamentos", + "subscribed_languages.save": "Salvar le cambiamentos", "subscribed_languages.target": "Cambiar le linguas subscribite pro {target}", "tabs_bar.home": "Initio", "tabs_bar.notifications": "Notificationes", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 79224c57df..f4e5e1baea 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -35,9 +35,7 @@ "account.follow_back": "Ikuti balik", "account.followers": "Pengikut", "account.followers.empty": "Pengguna ini belum ada pengikut.", - "account.followers_counter": "{count, plural, other {{counter} Pengikut}}", "account.following": "Mengikuti", - "account.following_counter": "{count, plural, other {{counter} Mengikuti}}", "account.follows.empty": "Pengguna ini belum mengikuti siapa pun.", "account.go_to_profile": "Buka profil", "account.hide_reblogs": "Sembunyikan boosts dari @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} ingin mengikuti Anda", "account.share": "Bagikan profil @{name}", "account.show_reblogs": "Tampilkan boost dari @{name}", - "account.statuses_counter": "{count, plural, other {{counter} Kiriman}}", "account.unblock": "Buka blokir @{name}", "account.unblock_domain": "Buka blokir domain {domain}", "account.unblock_short": "Buka blokir", @@ -565,8 +562,6 @@ "server_banner.about_active_users": "Orang menggunakan server ini selama 30 hari terakhir (Pengguna Aktif Bulanan)", "server_banner.active_users": "pengguna aktif", "server_banner.administered_by": "Dikelola oleh:", - "server_banner.introduction": "{domain} adalah bagian dari jaringan sosial terdesentralisasi yang diberdayakan oleh {mastodon}.", - "server_banner.learn_more": "Pelajari lebih lanjut", "server_banner.server_stats": "Statistik server:", "sign_in_banner.create_account": "Buat akun", "sign_in_banner.sign_in": "Masuk", diff --git a/app/javascript/mastodon/locales/ie.json b/app/javascript/mastodon/locales/ie.json index 1921509478..c75788c430 100644 --- a/app/javascript/mastodon/locales/ie.json +++ b/app/javascript/mastodon/locales/ie.json @@ -35,9 +35,7 @@ "account.follow_back": "Sequer reciprocmen", "account.followers": "Sequitores", "account.followers.empty": "Ancor nequi seque ti-ci usator.", - "account.followers_counter": "{count, plural, one {{counter} Sequitor} other {{counter} Sequitor}}", "account.following": "Sequent", - "account.following_counter": "{count, plural, one {{counter} Sequent} other {{counter} Sequent}}", "account.follows.empty": "Ti-ci usator ancor ne seque quemcunc.", "account.go_to_profile": "Ear a profil", "account.hide_reblogs": "Celar boosts de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} ha petit sequer te", "account.share": "Distribuer li profil de @{name}", "account.show_reblogs": "Monstrar boosts de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Posta} other {{counter} Postas}}", "account.unblock": "Desbloccar @{name}", "account.unblock_domain": "Desbloccar dominia {domain}", "account.unblock_short": "Desbloccar", @@ -694,13 +691,10 @@ "server_banner.about_active_users": "Gente usant ti-ci servitor durant li ultim 30 dies (Mensual Activ Usatores)", "server_banner.active_users": "activ usatores", "server_banner.administered_by": "Administrat de:", - "server_banner.introduction": "{domain} es un part del decentralisat social retage constructet sur {mastodon}.", - "server_banner.learn_more": "Aprender plu", "server_banner.server_stats": "Statisticas pri li servitor:", "sign_in_banner.create_account": "Crear un conto", "sign_in_banner.sign_in": "Intrar", "sign_in_banner.sso_redirect": "Intrar o registrar se", - "sign_in_banner.text": "Intrar por sequer profiles o hashtags, favoritisar, partir e responder a postas. Tu posse anc interacter per tui conto che un diferent servitor.", "status.admin_account": "Aperter interfacie de moderation por @{name}", "status.admin_domain": "Aperter interfacie de moderation por {domain}", "status.admin_status": "Aperter ti-ci posta in li interfacie de moderation", diff --git a/app/javascript/mastodon/locales/ig.json b/app/javascript/mastodon/locales/ig.json index 90253743fc..4e3e3997da 100644 --- a/app/javascript/mastodon/locales/ig.json +++ b/app/javascript/mastodon/locales/ig.json @@ -132,7 +132,6 @@ "report_notification.categories.other": "Ọzọ", "search.placeholder": "Chọọ", "server_banner.active_users": "ojiarụ dị ìrè", - "server_banner.learn_more": "Mụtakwuo", "sign_in_banner.sign_in": "Sign in", "status.admin_status": "Open this status in the moderation interface", "status.bookmark": "Kee ebenrụtụakā", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 3382fa1aec..6aa954ae57 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -33,9 +33,7 @@ "account.follow": "Sequar", "account.followers": "Sequanti", "account.followers.empty": "Nulu sequas ca uzanto til nun.", - "account.followers_counter": "{count, plural, one {{counter} Sequanto} other {{counter} Sequanti}}", "account.following": "Sequata", - "account.following_counter": "{count, plural, one {{counter} Sequas} other {{counter} Sequanti}}", "account.follows.empty": "Ca uzanto ne sequa irgu til nun.", "account.go_to_profile": "Irez al profilo", "account.hide_reblogs": "Celez repeti de @{name}", @@ -60,7 +58,6 @@ "account.requested_follow": "{name} demandis sequar tu", "account.share": "Partigez profilo di @{name}", "account.show_reblogs": "Montrez repeti de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Posto} other {{counter} Posti}}", "account.unblock": "Desblokusar @{name}", "account.unblock_domain": "Desblokusar {domain}", "account.unblock_short": "Desblokusar", @@ -582,13 +579,10 @@ "server_banner.about_active_users": "Personi quo uzas ca servilo dum antea 30 dii (monate aktiva uzanti)", "server_banner.active_users": "aktiva uzanti", "server_banner.administered_by": "Administresis da:", - "server_banner.introduction": "{domain} esas parto di necentraligita sociala ret quo povizesas da {mastodon}.", - "server_banner.learn_more": "Lernez plue", "server_banner.server_stats": "Servilstatistiko:", "sign_in_banner.create_account": "Kreez konto", "sign_in_banner.sign_in": "Enirez", "sign_in_banner.sso_redirect": "Enirar o krear konto", - "sign_in_banner.text": "Enirez por sequar profili o hashtagi, favorizar, partigar e respondizar posti. On povas anke interagar de vua konto kun diferanta servilo.", "status.admin_account": "Apertez jerintervizajo por @{name}", "status.admin_domain": "Apertez jerintervizajo por {domain}", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 6ce72b43fc..1a38591b85 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -63,7 +63,7 @@ "account.requested_follow": "{name} hefur beðið um að fylgjast með þér", "account.share": "Deila notandasniði fyrir @{name}", "account.show_reblogs": "Sýna endurbirtingar frá @{name}", - "account.statuses_counter": "{count, plural, one {Færsla: {counter}} other {Færslur: {counter}}}", + "account.statuses_counter": "{count, plural, one {{counter} færsla} other {{counter} færslur}}", "account.unblock": "Aflétta útilokun af @{name}", "account.unblock_domain": "Aflétta útilokun lénsins {domain}", "account.unblock_short": "Hætta að loka á", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Þetta notandasnið hefur verið falið af umsjónarmönnum {domain}.", "link_preview.author": "Eftir {name}", "link_preview.more_from_author": "Meira frá {name}", + "link_preview.shares": "{count, plural, one {{counter} færsla} other {{counter} færslur}}", "lists.account.add": "Bæta á lista", "lists.account.remove": "Fjarlægja af lista", "lists.delete": "Eyða lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Folk sem hefur notað þennan netþjón síðustu 30 daga (virkir notendur í mánuðinum)", "server_banner.active_users": "virkir notendur", "server_banner.administered_by": "Stýrt af:", - "server_banner.introduction": "{domain} er hluti af dreifhýsta samfélagsnetinu sem keyrt er af {mastodon}.", - "server_banner.learn_more": "Kanna nánar", + "server_banner.is_one_of_many": "{domain} er einn af fjölmörgum óháðum Mastodon-þjónum sem þú getur notað til að taka þátt í fediverse-samfélaginu.", "server_banner.server_stats": "Tölfræði þjóns:", "sign_in_banner.create_account": "Búa til notandaaðgang", + "sign_in_banner.follow_anyone": "Fylgstu með hverjum sem er í þessum samtvinnaða heimi og skoðaðu allt í tímaröð. Engin reiknirit, auglýsingar eða smellbeitur.", + "sign_in_banner.mastodon_is": "Mastodon er besta leiðin til að fylgjast með hvað sé í gangi.", "sign_in_banner.sign_in": "Skrá inn", "sign_in_banner.sso_redirect": "Skrá inn eða nýskrá", - "sign_in_banner.text": "Skráðu þig inn til að fylgjast með notendum eða myllumerkjum, svara færslum, deila þeim eða setja í eftirlæti. Þú getur einnig átt í samskiptum á aðgangnum þínum á öðrum netþjónum.", "status.admin_account": "Opna umsjónarviðmót fyrir @{name}", "status.admin_domain": "Opna umsjónarviðmót fyrir @{domain}", "status.admin_status": "Opna þessa færslu í umsjónarviðmótinu", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f66497e0a7..73c4f9ba60 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -35,9 +35,9 @@ "account.follow_back": "Segui a tua volta", "account.followers": "Follower", "account.followers.empty": "Ancora nessuno segue questo utente.", - "account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}", + "account.followers_counter": "{count, plural, one {{counter} seguace} other {{counter} seguaci}}", "account.following": "Seguiti", - "account.following_counter": "{count, plural, one {{counter} Seguiti} other {{counter} Seguiti}}", + "account.following_counter": "{count, plural, one {{counter} segui} other {{counter} segui}}", "account.follows.empty": "Questo utente non segue ancora nessuno.", "account.go_to_profile": "Vai al profilo", "account.hide_reblogs": "Nascondi potenziamenti da @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ha richiesto di seguirti", "account.share": "Condividi il profilo di @{name}", "account.show_reblogs": "Mostra potenziamenti da @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Post}}", + "account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} post}}", "account.unblock": "Sblocca @{name}", "account.unblock_domain": "Sblocca il dominio {domain}", "account.unblock_short": "Sblocca", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Questo profilo è stato nascosto dai moderatori di {domain}.", "link_preview.author": "Di {name}", "link_preview.more_from_author": "Altro da {name}", + "link_preview.shares": "{count, plural, one {{counter} post} other {{counter} post}}", "lists.account.add": "Aggiungi all'elenco", "lists.account.remove": "Rimuovi dall'elenco", "lists.delete": "Elimina elenco", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Persone che hanno utilizzato questo server negli ultimi 30 giorni (Utenti Attivi Mensilmente)", "server_banner.active_users": "utenti attivi", "server_banner.administered_by": "Amministrato da:", - "server_banner.introduction": "{domain} è parte del social network decentralizzato, sviluppato da {mastodon}.", - "server_banner.learn_more": "Scopri di più", + "server_banner.is_one_of_many": "{domain} è uno dei tanti server Mastodon indipendenti che puoi usare per partecipare al fediverso.", "server_banner.server_stats": "Statistiche del server:", "sign_in_banner.create_account": "Crea un profilo", + "sign_in_banner.follow_anyone": "Segui chiunque nel fediverso e vedi tutto in ordine cronologico. Nessun algoritmo, annunci o clickbait in vista.", + "sign_in_banner.mastodon_is": "Mastodon è il modo migliore per tenere il passo con quello che sta accadendo.", "sign_in_banner.sign_in": "Accedi", "sign_in_banner.sso_redirect": "Accedi o Registrati", - "sign_in_banner.text": "Accedi per seguire profili o hashtag, condividere, rispondere e aggiungere post ai preferiti. Puoi anche interagire dal tuo account su un server diverso.", "status.admin_account": "Apri interfaccia di moderazione per @{name}", "status.admin_domain": "Apri l'interfaccia di moderazione per {domain}", "status.admin_status": "Apri questo post nell'interfaccia di moderazione", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index eea06fff59..575c68de03 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -35,9 +35,7 @@ "account.follow_back": "フォローバック", "account.followers": "フォロワー", "account.followers.empty": "まだ誰もフォローしていません。", - "account.followers_counter": "{counter} フォロワー", "account.following": "フォロー中", - "account.following_counter": "{counter} フォロー", "account.follows.empty": "まだ誰もフォローしていません。", "account.go_to_profile": "プロフィールページへ", "account.hide_reblogs": "@{name}さんからのブーストを非表示", @@ -63,7 +61,6 @@ "account.requested_follow": "{name}さんがあなたにフォローリクエストしました", "account.share": "@{name}さんのプロフィールを共有する", "account.show_reblogs": "@{name}さんからのブーストを表示", - "account.statuses_counter": "{counter} 投稿", "account.unblock": "@{name}さんのブロックを解除", "account.unblock_domain": "{domain}のブロックを解除", "account.unblock_short": "ブロック解除", @@ -694,13 +691,10 @@ "server_banner.about_active_users": "過去30日間にこのサーバーを使用している人 (月間アクティブユーザー)", "server_banner.active_users": "人のアクティブユーザー", "server_banner.administered_by": "管理者", - "server_banner.introduction": "{domain}は{mastodon}を使った分散型ソーシャルネットワークの一部です。", - "server_banner.learn_more": "もっと詳しく", "server_banner.server_stats": "サーバーの情報", "sign_in_banner.create_account": "アカウント作成", "sign_in_banner.sign_in": "ログイン", "sign_in_banner.sso_redirect": "ログインまたは登録", - "sign_in_banner.text": "アカウントがあればユーザーやハッシュタグをフォローしたり、投稿のお気に入り登録やブースト、投稿への返信ができます。別のサーバーのユーザーとの交流も可能です。", "status.admin_account": "@{name}さんのモデレーション画面を開く", "status.admin_domain": "{domain}のモデレーション画面を開く", "status.admin_status": "この投稿をモデレーション画面で開く", diff --git a/app/javascript/mastodon/locales/ka.json b/app/javascript/mastodon/locales/ka.json index 7af4dccd86..b2e67e143e 100644 --- a/app/javascript/mastodon/locales/ka.json +++ b/app/javascript/mastodon/locales/ka.json @@ -26,7 +26,6 @@ "account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა", "account.share": "გააზიარე @{name}-ის პროფილი", "account.show_reblogs": "აჩვენე ბუსტები @{name}-სგან", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "განბლოკე @{name}", "account.unblock_domain": "გამოაჩინე {domain}", "account.unendorse": "არ გამოირჩეს პროფილზე", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 9fdf602992..de866cc1bc 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -26,9 +26,7 @@ "account.follow": "Ḍfer", "account.followers": "Imeḍfaren", "account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.", - "account.followers_counter": "{count, plural, one {{count} n umeḍfar} other {{count} n imeḍfaren}}", "account.following": "Yeṭṭafaṛ", - "account.following_counter": "{count, plural, one {{counter} yettwaḍfaren} other {{counter} yettwaḍfaren}}", "account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.", "account.go_to_profile": "Ddu ɣer umaɣnu", "account.hide_reblogs": "Ffer ayen i ibeṭṭu @{name}", @@ -51,7 +49,6 @@ "account.requested_follow": "{name} yessuter ad k·m-yeḍfer", "account.share": "Bḍu amaɣnu n @{name}", "account.show_reblogs": "Ssken-d inebḍa n @{name}", - "account.statuses_counter": "{count, plural, one {{counter} n tsuffeɣt} other {{counter} n tsuffaɣ}}", "account.unblock": "Serreḥ i @{name}", "account.unblock_domain": "Ssken-d {domain}", "account.unblock_short": "Serreḥ", @@ -509,7 +506,6 @@ "search_results.statuses": "Tisuffaɣ", "search_results.title": "Anadi ɣef {q}", "server_banner.administered_by": "Yettwadbel sɣur :", - "server_banner.learn_more": "Issin ugar", "sign_in_banner.create_account": "Snulfu-d amiḍan", "sign_in_banner.sign_in": "Qqen", "sign_in_banner.sso_redirect": "Qqen neɣ jerred", diff --git a/app/javascript/mastodon/locales/kk.json b/app/javascript/mastodon/locales/kk.json index bd0a806cdb..efeee16c65 100644 --- a/app/javascript/mastodon/locales/kk.json +++ b/app/javascript/mastodon/locales/kk.json @@ -31,9 +31,7 @@ "account.follow": "Жазылу", "account.followers": "Жазылушы", "account.followers.empty": "Бұл қолданушыға әлі ешкім жазылмаған.", - "account.followers_counter": "{count, plural, one {{counter} жазылушы} other {{counter} жазылушы}}", "account.following": "Жазылым", - "account.following_counter": "{count, plural, one {{counter} жазылым} other {{counter} жазылым}}", "account.follows.empty": "Бұл қолданушы әлі ешкімге жазылмаған.", "account.go_to_profile": "Профиліне өту", "account.hide_reblogs": "@{name} бустарын жасыру", @@ -52,7 +50,6 @@ "account.requested": "Растауын күтіңіз. Жазылудан бас тарту үшін басыңыз", "account.share": "@{name} профилін бөлісу\"", "account.show_reblogs": "@{name} бөліскендерін көрсету", - "account.statuses_counter": "{count, plural, one {{counter} Пост} other {{counter} Пост}}", "account.unblock": "Бұғаттан шығару @{name}", "account.unblock_domain": "Бұғаттан шығару {domain}", "account.unendorse": "Профильде рекомендемеу", diff --git a/app/javascript/mastodon/locales/kn.json b/app/javascript/mastodon/locales/kn.json index ceb0f8b9b6..24592e37fc 100644 --- a/app/javascript/mastodon/locales/kn.json +++ b/app/javascript/mastodon/locales/kn.json @@ -16,7 +16,6 @@ "account.posts": "ಟೂಟ್‌ಗಳು", "account.posts_with_replies": "Toots and replies", "account.requested": "Awaiting approval", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock_domain": "Unhide {domain}", "account_note.placeholder": "Click to add a note", "alert.unexpected.title": "ಅಯ್ಯೋ!", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index ff62cf5d14..20c1ab3015 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -32,12 +32,12 @@ "account.featured_tags.last_status_never": "게시물 없음", "account.featured_tags.title": "{name} 님의 추천 해시태그", "account.follow": "팔로우", - "account.follow_back": "맞팔로우", + "account.follow_back": "맞팔로우 하기", "account.followers": "팔로워", "account.followers.empty": "아직 아무도 이 사용자를 팔로우하고 있지 않습니다.", - "account.followers_counter": "{counter} 팔로워", + "account.followers_counter": "{count, plural, other {{counter} 팔로워}}", "account.following": "팔로잉", - "account.following_counter": "{counter} 팔로잉", + "account.following_counter": "{count, plural, other {{counter} 팔로잉}}", "account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.", "account.go_to_profile": "프로필로 이동", "account.hide_reblogs": "@{name}의 부스트를 숨기기", @@ -53,7 +53,7 @@ "account.mute_notifications_short": "알림 뮤트", "account.mute_short": "뮤트", "account.muted": "뮤트됨", - "account.mutual": "상호 팔로우", + "account.mutual": "맞팔로우 중", "account.no_bio": "제공된 설명이 없습니다.", "account.open_original_page": "원본 페이지 열기", "account.posts": "게시물", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다", "account.share": "@{name}의 프로필 공유", "account.show_reblogs": "@{name}의 부스트 보기", - "account.statuses_counter": "{counter} 게시물", + "account.statuses_counter": "{count, plural, other {{counter} 게시물}}", "account.unblock": "차단 해제", "account.unblock_domain": "도메인 {domain} 차단 해제", "account.unblock_short": "차단 해제", @@ -415,6 +415,7 @@ "limited_account_hint.title": "이 프로필은 {domain}의 중재자에 의해 숨겨진 상태입니다.", "link_preview.author": "{name}", "link_preview.more_from_author": "{name} 프로필 보기", + "link_preview.shares": "{count, plural, other {{counter} 개의 게시물}}", "lists.account.add": "리스트에 추가", "lists.account.remove": "리스트에서 제거", "lists.delete": "리스트 삭제", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "30일 동안 이 서버를 사용한 사람들 (월간 활성 이용자)", "server_banner.active_users": "활성 사용자", "server_banner.administered_by": "관리자:", - "server_banner.introduction": "{domain}은 마스토돈으로 운영되는 탈중앙화 된 소셜 네트워크의 일부입니다.", - "server_banner.learn_more": "더 알아보기", + "server_banner.is_one_of_many": "{domain}은 페디버스를 통해 참여할 수 있는 많은 마스토돈 서버들 중 하나입니다", "server_banner.server_stats": "서버 통계:", "sign_in_banner.create_account": "계정 생성", + "sign_in_banner.follow_anyone": "페디버스를 통해 누구든지 팔로우하고 시간순으로 게시물을 받아보세요. 알고리즘도, 광고도, 클릭을 유도하는 것들도 없습니다.", + "sign_in_banner.mastodon_is": "마스토돈은 무엇이 일어나는지 받아보는 가장 좋은 수단입니다.", "sign_in_banner.sign_in": "로그인", "sign_in_banner.sso_redirect": "로그인 또는 가입하기", - "sign_in_banner.text": "로그인을 통해 프로필이나 해시태그를 팔로우하거나 마음에 들어하거나 공유하고 답글을 달 수 있습니다. 다른 서버에 있는 본인의 계정을 통해 참여할 수도 있습니다.", "status.admin_account": "@{name}에 대한 중재 화면 열기", "status.admin_domain": "{domain}에 대한 중재 화면 열기", "status.admin_status": "중재 화면에서 이 게시물 열기", diff --git a/app/javascript/mastodon/locales/ku.json b/app/javascript/mastodon/locales/ku.json index c78861b60c..5248cdfa51 100644 --- a/app/javascript/mastodon/locales/ku.json +++ b/app/javascript/mastodon/locales/ku.json @@ -32,9 +32,7 @@ "account.follow": "Bişopîne", "account.followers": "Şopîner", "account.followers.empty": "Kesekî hin ev bikarhêner neşopandiye.", - "account.followers_counter": "{count, plural, one {{counter} Şopîner} other {{counter} Şopîner}}", "account.following": "Dişopîne", - "account.following_counter": "{count, plural, one {{counter} Dişopîne} other {{counter} Dişopîne}}", "account.follows.empty": "Ev bikarhêner hin kesekî heya niha neşopandiye.", "account.go_to_profile": "Biçe bo profîlê", "account.hide_reblogs": "Bilindkirinên ji @{name} veşêre", @@ -56,7 +54,6 @@ "account.requested_follow": "{name} dixwaze te bişopîne", "account.share": "Profîla @{name} parve bike", "account.show_reblogs": "Bilindkirinên ji @{name} nîşan bike", - "account.statuses_counter": "{count, plural,one {{counter} Şandî}other {{counter} Şandî}}", "account.unblock": "Astengê li ser @{name} rake", "account.unblock_domain": "Astengê li ser navperê {domain} rake", "account.unblock_short": "Astengiyê rake", @@ -492,8 +489,6 @@ "server_banner.about_active_users": "Kesên ku di van 30 rojên dawî de vê rajekarê bi kar tînin (Bikarhênerên Çalak ên Mehane)", "server_banner.active_users": "bikarhênerên çalak", "server_banner.administered_by": "Tê bi rêvebirin ji aliyê:", - "server_banner.introduction": "{domain} beşek ji tora civakî ya nenavendî ye bi hêzdariya {mastodon}.", - "server_banner.learn_more": "Bêtir fêr bibe", "server_banner.server_stats": "Amarên rajekar:", "sign_in_banner.create_account": "Ajimêr biafirîne", "sign_in_banner.sign_in": "Têkeve", diff --git a/app/javascript/mastodon/locales/kw.json b/app/javascript/mastodon/locales/kw.json index 794cbd9ede..1afcf645cf 100644 --- a/app/javascript/mastodon/locales/kw.json +++ b/app/javascript/mastodon/locales/kw.json @@ -17,8 +17,6 @@ "account.follow": "Holya", "account.followers": "Holyoryon", "account.followers.empty": "Ny wra nagonan holya'n devnydhyer ma hwath.", - "account.followers_counter": "{count, plural, one {{counter} Holyer} other {{counter} Holyer}}", - "account.following_counter": "{count, plural, one {Ow holya {counter}} other {Ow holya {counter}}}", "account.follows.empty": "Ny wra'n devnydhyer ma holya nagonan hwath.", "account.hide_reblogs": "Kudha kenerthow a @{name}", "account.link_verified_on": "Perghenogeth an kolm ma a veu checkys dhe {date}", @@ -33,7 +31,6 @@ "account.requested": "Ow kortos komendyans. Klyckyewgh dhe hedhi govyn holya", "account.share": "Kevrenna profil @{name}", "account.show_reblogs": "Diskwedhes kenerthow a @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Tout} other {{counter} Tout}}", "account.unblock": "Anlettya @{name}", "account.unblock_domain": "Anlettya gorfarth {domain}", "account.unendorse": "Na wra diskwedhes yn profil", diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json index 4bee0efed4..aa209fcc00 100644 --- a/app/javascript/mastodon/locales/la.json +++ b/app/javascript/mastodon/locales/la.json @@ -14,12 +14,9 @@ "account.edit_profile": "Recolere notionem", "account.featured_tags.last_status_never": "Nulla contributa", "account.featured_tags.title": "Hashtag notātī {name}", - "account.followers_counter": "{count, plural, one {{counter} Sectator} other {{counter} Sectatores}}", - "account.following_counter": "{count, plural, one {{counter} Sequens} other {{counter} Sequentes}}", "account.moved_to": "{name} significavit eum suam rationem novam nunc esse:", "account.muted": "Confutatus", "account.requested_follow": "{name} postulavit ut te sequeretur", - "account.statuses_counter": "{count, plural, one {{counter} Nuntius} other {{counter} Nuntii}}", "account.unblock_short": "Solvere impedimentum", "account_note.placeholder": "Click to add a note", "admin.dashboard.retention.average": "Mediocritas", @@ -220,8 +217,6 @@ "search_results.all": "Omnis", "server_banner.active_users": "Usūrāriī āctīvī", "server_banner.administered_by": "Administratur:", - "server_banner.introduction": "{domain} pars est de rete sociali decentralizato a {mastodon} propulsato.", - "server_banner.learn_more": "Discere plura", "sign_in_banner.sign_in": "Sign in", "status.admin_status": "Open this status in the moderation interface", "status.block": "Impedire @{name}", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index 72299bb861..292f00818c 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -35,9 +35,7 @@ "account.follow_back": "Sige tamyen", "account.followers": "Suivantes", "account.followers.empty": "Por agora dingun no sige a este utilizador.", - "account.followers_counter": "{count, plural, one {{counter} suivante} other {{counter} suivantes}}", "account.following": "Sigiendo", - "account.following_counter": "{count, plural, other {Sigiendo a {counter}}}", "account.follows.empty": "Este utilizador ainda no sige a dingun.", "account.go_to_profile": "Va al profil", "account.hide_reblogs": "Eskonde repartajasyones de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} tiene solisitado segirte", "account.share": "Partaja el profil de @{name}", "account.show_reblogs": "Amostra repartajasyones de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} publikasyon} other {{counter} publikasyones}}", "account.unblock": "Dezbloka a @{name}", "account.unblock_domain": "Dezbloka domeno {domain}", "account.unblock_short": "Dezbloka", @@ -666,13 +663,10 @@ "server_banner.about_active_users": "Utilizadores aktivos en este sirvidor durante los ultimos 30 diyas (utilizadores aktivos mensuales)", "server_banner.active_users": "utilizadores aktivos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} es parte de la red sosyala desentralizada liderada por {mastodon}.", - "server_banner.learn_more": "Ambezate mas", "server_banner.server_stats": "Estatistikas del sirvidor:", "sign_in_banner.create_account": "Kriya kuento", "sign_in_banner.sign_in": "Konektate", "sign_in_banner.sso_redirect": "Konektate o enrejistrate", - "sign_in_banner.text": "Konektate para segir prefiles o etiketas, partajar publikasyones, arispondir a eyas i markar ke te plazen. Puedes tambyen enteraktuar dizde tu kuento en un sirvidor desferente.", "status.admin_account": "Avre la enterfaz de moderasyon para @{name}", "status.admin_domain": "Avre la enterfaz de moderasyon para @{domain}", "status.admin_status": "Avre esto en la enterfaz de moderasyon", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 307230036c..bb69b73399 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -1,18 +1,18 @@ { "about.blocks": "Prižiūrimi serveriai", "about.contact": "Kontaktai:", - "about.disclaimer": "Mastodon – tai nemokama atvirojo kodo programinė įranga ir Mastodon gGmbH prekės ženklas.", + "about.disclaimer": "„Mastodon“ – tai nemokama atvirojo kodo programinė įranga ir „Mastodon“ gGmbH prekės ženklas.", "about.domain_blocks.no_reason_available": "Priežastis nepateikta", - "about.domain_blocks.preamble": "Mastodon paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.", + "about.domain_blocks.preamble": "„Mastodon“ paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.", "about.domain_blocks.silenced.explanation": "Paprastai nematysi profilių ir turinio iš šio serverio, nebent jį aiškiai ieškosi arba pasirinksi jį sekdamas (-a).", "about.domain_blocks.silenced.title": "Ribota", "about.domain_blocks.suspended.explanation": "Jokie duomenys iš šio serverio nebus apdorojami, saugomi ar keičiami, todėl bet kokia sąveika ar bendravimas su šio serverio naudotojais bus neįmanomas.", - "about.domain_blocks.suspended.title": "Uždrausta", + "about.domain_blocks.suspended.title": "Pristabdyta", "about.not_available": "Ši informacija nebuvo pateikta šiame serveryje.", - "about.powered_by": "Decentralizuota socialinė medija, kurią valdo {mastodon}", + "about.powered_by": "Decentralizuota socialinė medija, veikianti pagal „{mastodon}“", "about.rules": "Serverio taisyklės", "account.account_note_header": "Pastaba", - "account.add_or_remove_from_list": "Pridėti arba ištrinti iš sąrašų", + "account.add_or_remove_from_list": "Pridėti arba pašalinti iš sąrašų", "account.badges.bot": "Automatizuotas", "account.badges.group": "Grupė", "account.block": "Blokuoti @{name}", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Šį profilį paslėpė {domain} prižiūrėtojai.", "link_preview.author": "Sukūrė {name}", "link_preview.more_from_author": "Daugiau iš {name}", + "link_preview.shares": "{count, plural, one {{counter} įrašas} few {{counter} įrašai} many {{counter} įrašo} other {{counter} įrašų}}", "lists.account.add": "Pridėti į sąrašą", "lists.account.remove": "Pašalinti iš sąrašo", "lists.delete": "Ištrinti sąrašą", @@ -690,13 +691,13 @@ "server_banner.about_active_users": "Žmonės, kurie naudojosi šiuo serveriu per pastarąsias 30 dienų (mėnesio aktyvūs naudotojai)", "server_banner.active_users": "aktyvūs naudotojai", "server_banner.administered_by": "Administruoja:", - "server_banner.introduction": "{domain} – decentralizuoto socialinio tinklo dalis, kurį palaiko {mastodon}.", - "server_banner.learn_more": "Sužinoti daugiau", + "server_banner.is_one_of_many": "{domain} – tai vienas iš daugelio nepriklausomų „Mastodon“ serverių, kuriuos gali naudoti fediverse.", "server_banner.server_stats": "Serverio statistika:", "sign_in_banner.create_account": "Sukurti paskyrą", + "sign_in_banner.follow_anyone": "Sek bet kurį asmenį visoje fediverse ir žiūrėk viską chronologine tvarka. Jokių algoritmų, reklamų ar paspaudimų.", + "sign_in_banner.mastodon_is": "„Mastodon“ – tai geriausias būdas sekti, kas vyksta.", "sign_in_banner.sign_in": "Prisijungimas", "sign_in_banner.sso_redirect": "Prisijungti arba užsiregistruoti", - "sign_in_banner.text": "Prisijunk, kad galėtum sekti profilius arba saitažodžius, mėgsti, bendrinti ir atsakyti į įrašus. Taip pat gali bendrauti iš savo paskyros kitame serveryje.", "status.admin_account": "Atidaryti prižiūrėjimo sąsają @{name}", "status.admin_domain": "Atidaryti prižiūrėjimo sąsają {domain}", "status.admin_status": "Atidaryti šį įrašą prižiūrėjimo sąsajoje", diff --git a/app/javascript/mastodon/locales/lv.json b/app/javascript/mastodon/locales/lv.json index efc45c9c08..041072c6ad 100644 --- a/app/javascript/mastodon/locales/lv.json +++ b/app/javascript/mastodon/locales/lv.json @@ -35,9 +35,7 @@ "account.follow_back": "Sekot atpakaļ", "account.followers": "Sekotāji", "account.followers.empty": "Šim lietotājam vēl nav sekotāju.", - "account.followers_counter": "{count, plural, zero {{counter} sekotāju} one {{counter} sekotājs} other {{counter} sekotāji}}", "account.following": "Seko", - "account.following_counter": "{count, plural, zero{{counter} sekojamo} one {{counter} sekojamais} other {{counter} sekojamie}}", "account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.", "account.go_to_profile": "Doties uz profilu", "account.hide_reblogs": "Paslēpt @{name} pastiprinātos ierakstus", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} nosūtīja Tev sekošanas pieprasījumu", "account.share": "Dalīties ar @{name} profilu", "account.show_reblogs": "Parādīt @{name} pastiprinātos ierakstus", - "account.statuses_counter": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}}", "account.unblock": "Atbloķēt @{name}", "account.unblock_domain": "Atbloķēt domēnu {domain}", "account.unblock_short": "Atbloķēt", @@ -642,13 +639,10 @@ "server_banner.about_active_users": "Cilvēki, kas izmantojuši šo serveri pēdējo 30 dienu laikā (aktīvie lietotāji mēnesī)", "server_banner.active_users": "aktīvi lietotāji", "server_banner.administered_by": "Pārvalda:", - "server_banner.introduction": "{domain} ir daļa no decentralizētā sociālā tīkla, ko nodrošina {mastodon}.", - "server_banner.learn_more": "Uzzināt vairāk", "server_banner.server_stats": "Servera statistika:", "sign_in_banner.create_account": "Izveidot kontu", "sign_in_banner.sign_in": "Pieteikties", "sign_in_banner.sso_redirect": "Piesakies vai Reģistrējies", - "sign_in_banner.text": "Jāpiesakās, lai sekotu profiliem vai tēmturiem, pievienotu izlasei, kopīgotu ierakstus un atbildētu uz tiem. Vari arī mijiedarboties ar savu kontu citā serverī.", "status.admin_account": "Atvērt @{name} moderēšanas saskarni", "status.admin_domain": "Atvērt {domain} moderēšanas saskarni", "status.admin_status": "Atvērt šo ziņu moderācijas saskarnē", diff --git a/app/javascript/mastodon/locales/mk.json b/app/javascript/mastodon/locales/mk.json index d8a470ed47..a09ad98ebf 100644 --- a/app/javascript/mastodon/locales/mk.json +++ b/app/javascript/mastodon/locales/mk.json @@ -38,7 +38,6 @@ "account.requested": "Се чека одобрување. Кликни за да одкажиш барање за следење", "account.share": "Сподели @{name} профил", "account.show_reblogs": "Прикажи бустови од @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "Одблокирај @{name}", "account.unblock_domain": "Прикажи {domain}", "account.unendorse": "Не прикажувај на профил", diff --git a/app/javascript/mastodon/locales/ml.json b/app/javascript/mastodon/locales/ml.json index 8fb4e818db..d9caccef34 100644 --- a/app/javascript/mastodon/locales/ml.json +++ b/app/javascript/mastodon/locales/ml.json @@ -22,9 +22,7 @@ "account.follow": "പിന്തുടരുക", "account.followers": "പിന്തുടരുന്നവർ", "account.followers.empty": "ഈ ഉപയോക്താവിനെ ആരും ഇതുവരെ പിന്തുടരുന്നില്ല.", - "account.followers_counter": "{count, plural, one {{counter} പിന്തുടരുന്നവർ} other {{counter} പിന്തുടരുന്നവർ}}", "account.following": "പിന്തുടരുന്നു", - "account.following_counter": "{count, plural, one {{counter} പിന്തുടരുന്നു} other {{counter} പിന്തുടരുന്നു}}", "account.follows.empty": "ഈ ഉപയോക്താവ് ആരേയും ഇതുവരെ പിന്തുടരുന്നില്ല.", "account.go_to_profile": "പ്രൊഫൈലിലേക്ക് പോകാം", "account.hide_reblogs": "@{name} ബൂസ്റ്റ് ചെയ്തവ മറയ്കുക", @@ -42,7 +40,6 @@ "account.requested": "അനുവാദത്തിനായി കാത്തിരിക്കുന്നു. പിന്തുടരാനുള്ള അപേക്ഷ റദ്ദാക്കുവാൻ ഞെക്കുക", "account.share": "@{name} ന്റെ പ്രൊഫൈൽ പങ്കിടുക", "account.show_reblogs": "@{name} ൽ നിന്നുള്ള ബൂസ്റ്റുകൾ കാണിക്കുക", - "account.statuses_counter": "{count, plural, one {{counter} ടൂട്ട്} other {{counter} ടൂട്ടുകൾ}}", "account.unblock": "@{name} തടഞ്ഞത് മാറ്റുക", "account.unblock_domain": "{domain} എന്ന മേഖല വെളിപ്പെടുത്തുക", "account.unblock_short": "അൺബ്ലോക്കു ചെയ്യുക", diff --git a/app/javascript/mastodon/locales/mr.json b/app/javascript/mastodon/locales/mr.json index c07294d90a..2757b96f94 100644 --- a/app/javascript/mastodon/locales/mr.json +++ b/app/javascript/mastodon/locales/mr.json @@ -35,9 +35,7 @@ "account.follow_back": "आपणही अनुसरण करा", "account.followers": "अनुयायी", "account.followers.empty": "ह्या वापरकर्त्याचा आतापर्यंत कोणी अनुयायी नाही.", - "account.followers_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.following": "अनुसरण", - "account.following_counter": "{count, plural, one {{counter} following} other {{counter} following}}", "account.follows.empty": "हा वापरकर्ता अजूनपर्यंत कोणाचा अनुयायी नाही.", "account.go_to_profile": "प्रोफाइल वर जा", "account.hide_reblogs": "@{name} पासून सर्व बूस्ट लपवा", @@ -59,7 +57,6 @@ "account.requested_follow": "{name} ने आपल्याला फॉलो करण्याची रिक्वेस्ट केली आहे", "account.share": "@{name} चे प्रोफाइल शेअर करा", "account.show_reblogs": "{name}चे सर्व बुस्ट्स दाखवा", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "@{name} ला ब्लॉक करा", "account.unblock_domain": "उघड करा {domain}", "account.unblock_short": "अनब्लॉक करा", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index 8fe043c5dc..88c093bdee 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -35,9 +35,7 @@ "account.follow_back": "Ikut balik", "account.followers": "Pengikut", "account.followers.empty": "Belum ada yang mengikuti pengguna ini.", - "account.followers_counter": "{count, plural, one {{counter} Pengikut} other {{counter} Pengikut}}", "account.following": "Mengikuti", - "account.following_counter": "{count, plural, one {{counter} Diikuti} other {{counter} Diikuti}}", "account.follows.empty": "Pengguna ini belum mengikuti sesiapa.", "account.go_to_profile": "Pergi ke profil", "account.hide_reblogs": "Sembunyikan galakan daripada @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} has requested to follow you", "account.share": "Kongsi profil @{name}", "account.show_reblogs": "Tunjukkan galakan daripada @{name}", - "account.statuses_counter": "{count, plural, other {{counter} kiriman}}", "account.unblock": "Nyahsekat @{name}", "account.unblock_domain": "Nyahsekat domain {domain}", "account.unblock_short": "Nyahsekat", @@ -604,13 +601,10 @@ "server_banner.about_active_users": "Pengguna pelayan ini sepanjang 30 hari yang lalu (Pengguna Aktif Bulanan)", "server_banner.active_users": "pengguna aktif", "server_banner.administered_by": "Ditadbir oleh:", - "server_banner.introduction": "{domain} ialah sebahagian daripada rangkaian sosial terpencar dikuasakan oleh {mastodon}.", - "server_banner.learn_more": "Maklumat lanjut", "server_banner.server_stats": "Statistik pelayan:", "sign_in_banner.create_account": "Cipta akaun", "sign_in_banner.sign_in": "Daftar masuk", "sign_in_banner.sso_redirect": "Log masuk atau mendaftar", - "sign_in_banner.text": "Log masuk untuk mengikuti profil atau hashtag, kegemaran, kongsi dan balas pos. Anda juga boleh berinteraksi daripada akaun anda pada server lain.", "status.admin_account": "Buka antara muka penyederhanaan untuk @{name}", "status.admin_domain": "antara muka penyederhanaan", "status.admin_status": "Buka hantaran ini dalam antara muka penyederhanaan", diff --git a/app/javascript/mastodon/locales/my.json b/app/javascript/mastodon/locales/my.json index 23eaa8564b..46c8d18069 100644 --- a/app/javascript/mastodon/locales/my.json +++ b/app/javascript/mastodon/locales/my.json @@ -34,9 +34,7 @@ "account.follow": "စောင့်ကြည့်", "account.followers": "စောင့်ကြည့်သူများ", "account.followers.empty": "ဤသူကို စောင့်ကြည့်သူ မရှိသေးပါ။", - "account.followers_counter": "{count, plural, one {စောင့်ကြည့်သူ {counter}} other {စောင့်ကြည့်သူများ {counter}}}", "account.following": "စောင့်ကြည့်နေသည်", - "account.following_counter": "{count, plural, one {စောင့်ကြည့်ထားသူ {counter}} other {စောင့်ကြည့်ထားသူများ {counter}}}", "account.follows.empty": "ဤသူသည် မည်သူ့ကိုမျှ စောင့်ကြည့်ခြင်း မရှိသေးပါ။", "account.go_to_profile": "ပရိုဖိုင်းသို့ သွားရန်", "account.hide_reblogs": "@{name} ၏ မျှဝေမှုကို ဝှက်ထားရန်", @@ -61,7 +59,6 @@ "account.requested_follow": "{name} က သင့်ကို စောင့်ကြည့်ရန် တောင်းဆိုထားသည်", "account.share": "{name}၏ပရိုဖိုင်ကိုမျှဝေပါ", "account.show_reblogs": "@{name} မှ မျှ၀ေမှုများကို ပြပါ\n", - "account.statuses_counter": "{count, plural, one {{counter} ပိုစ့်များ} other {{counter} ပိုစ့်များ}}", "account.unblock": "{name} ကို ဘလော့ဖြုတ်မည်", "account.unblock_domain": " {domain} ဒိုမိန်းကိုပြန်ဖွင့်မည်", "account.unblock_short": "ဘလော့ဖြုတ်ရန်", @@ -582,13 +579,10 @@ "server_banner.about_active_users": "ပြီးခဲ့သည့် ရက်ပေါင်း ၃၀ အတွင်း ဤဆာဗာကို အသုံးပြုသူများ (လအလိုက် လက်ရှိအသုံးပြုသူများ)", "server_banner.active_users": "လက်ရှိအသုံးပြုသူများ", "server_banner.administered_by": "မှ စီမံခန့်ခွဲသည် -", - "server_banner.introduction": "{domain} သည် {mastodon} မှ ပံ့ပိုးပေးထားသော ဗဟိုချုပ်ကိုင်မှုမရှိသည့် လူမှုကွန်ရက်တစ်ခုဖြစ်သည်။", - "server_banner.learn_more": "ပိုမိုသိရှိရန်", "server_banner.server_stats": "ဆာဗာအား လက်ရှိအသုံးပြုသူများ -", "sign_in_banner.create_account": "အကောင့်ဖန်တီးမည်", "sign_in_banner.sign_in": "အကောင့်ဝင်မည်", "sign_in_banner.sso_redirect": "အကောင့်ဝင်ပါ သို့မဟုတ် မှတ်ပုံတင်ပါ", - "sign_in_banner.text": "ပရိုဖိုင်များ သို့မဟုတ် hashtag များ၊ favorite၊ ပို့စ်မျှဝေမှုများနှင့် ပြန်ကြားစာများအသုံးပြုရန်အတွက် အကောင့်ဝင်ပါ။ အခြားဆာဗာပေါ်ရှိ သင့်အကောင့်မှလည်း အပြန်အလှန်ဖလှယ်နိုင်ပါသည်။", "status.admin_account": "@{name} အတွက် စိစစ်ခြင်းကြားခံနယ်ကို ဖွင့်ပါ", "status.admin_domain": "{domain} အတွက် စိစစ်ခြင်းကြားခံနယ်ကို ဖွင့်ပါ", "status.admin_status": "Open this status in the moderation interface", diff --git a/app/javascript/mastodon/locales/ne.json b/app/javascript/mastodon/locales/ne.json index 500261a34b..ca23a1f781 100644 --- a/app/javascript/mastodon/locales/ne.json +++ b/app/javascript/mastodon/locales/ne.json @@ -39,7 +39,6 @@ "account.requested_follow": "{name} ले तपाईंलाई फलो गर्न अनुरोध गर्नुभएको छ", "account.share": "@{name} को प्रोफाइल सेयर गर्नुहोस्", "account.show_reblogs": "@{name} को बूस्टहरू देखाउनुहोस्", - "account.statuses_counter": "{count, plural, one {{counter} पोस्ट} other {{counter} पोस्टहरू}}", "account.unblock": "@{name} लाई अनब्लक गर्नुहोस्", "account.unblock_domain": "{domain} डोमेनलाई अनब्लक गर्नुहोस्", "account.unblock_short": "अनब्लक गर्नुहोस्", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index bf081ad588..8246d8dfd2 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -415,6 +415,7 @@ "limited_account_hint.title": "Dit profiel is door de moderatoren van {domain} verborgen.", "link_preview.author": "Door {name}", "link_preview.more_from_author": "Meer van {name}", + "link_preview.shares": "{count, plural, one {{counter} bericht} other {{counter} berichten}}", "lists.account.add": "Aan lijst toevoegen", "lists.account.remove": "Uit lijst verwijderen", "lists.delete": "Lijst verwijderen", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Aantal gebruikers tijdens de afgelopen 30 dagen (MAU)", "server_banner.active_users": "actieve gebruikers", "server_banner.administered_by": "Beheerd door:", - "server_banner.introduction": "{domain} is onderdeel van het decentrale sociale netwerk {mastodon}.", - "server_banner.learn_more": "Meer leren", + "server_banner.is_one_of_many": "{domain} is een van de vele onafhankelijke Mastodon-servers die je kunt gebruiken om deel te nemen aan de fediverse.", "server_banner.server_stats": "Serverstats:", "sign_in_banner.create_account": "Registreren", + "sign_in_banner.follow_anyone": "Volg iedereen in de fediverse en zie het allemaal in chronologische volgorde. Geen algoritmes, advertenties of clickbaits.", + "sign_in_banner.mastodon_is": "Mastodon is de beste manier om wat er gebeurt bij te houden.", "sign_in_banner.sign_in": "Inloggen", "sign_in_banner.sso_redirect": "Inloggen of Registreren", - "sign_in_banner.text": "Wanneer je een account op deze server hebt, kun je inloggen om mensen of hashtags te volgen, op berichten te reageren of om deze te delen. Wanneer je een account op een andere server hebt, kun je daar inloggen en daar ook interactie met mensen op deze server hebben.", "status.admin_account": "Moderatie-omgeving van @{name} openen", "status.admin_domain": "Moderatie-omgeving van {domain} openen", "status.admin_status": "Dit bericht in de moderatie-omgeving tonen", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 3711cc0ae3..0fb0edf0a0 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -35,9 +35,9 @@ "account.follow_back": "Fylg tilbake", "account.followers": "Fylgjarar", "account.followers.empty": "Ingen fylgjer denne brukaren enno.", - "account.followers_counter": "{count, plural, one {{counter} fylgjar} other {{counter} fylgjarar}}", + "account.followers_counter": "{count, plural, one {{counter} følgjar} other {{counter} følgjarar}}", "account.following": "Fylgjer", - "account.following_counter": "{count, plural, one {Fylgjer {counter}} other {Fylgjer {counter}}}", + "account.following_counter": "{count, plural, one {{counter} følgjer} other {{counter} følgjer}}", "account.follows.empty": "Denne brukaren fylgjer ikkje nokon enno.", "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Gøym framhevingar frå @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} har bedt om å få fylgja deg", "account.share": "Del @{name} sin profil", "account.show_reblogs": "Vis framhevingar frå @{name}", - "account.statuses_counter": "{count, plural, one {{counter} tut} other {{counter} tut}}", + "account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}", "account.unblock": "Stopp blokkering av @{name}", "account.unblock_domain": "Stopp blokkering av domenet {domain}", "account.unblock_short": "Stopp blokkering", @@ -414,6 +414,8 @@ "limited_account_hint.action": "Vis profilen likevel", "limited_account_hint.title": "Denne profilen er skjult av moderatorane på {domain}.", "link_preview.author": "Av {name}", + "link_preview.more_from_author": "Meir frå {name}", + "link_preview.shares": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}}", "lists.account.add": "Legg til i liste", "lists.account.remove": "Fjern frå liste", "lists.delete": "Slett liste", @@ -694,13 +696,13 @@ "server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)", "server_banner.active_users": "aktive brukarar", "server_banner.administered_by": "Administrert av:", - "server_banner.introduction": "{domain} er del av det desentraliserte sosiale nettverket drive av {mastodon}.", - "server_banner.learn_more": "Lær meir", + "server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-serverane du kan bruke til å delta i Fødiverset.", "server_banner.server_stats": "Tenarstatistikk:", "sign_in_banner.create_account": "Opprett konto", + "sign_in_banner.follow_anyone": "Følg kven som helst på tvers av Fødiverset og sjå alt i kronologisk rekkjefølgje. Ingen algoritmar, reklamar eller clickbait i sikte.", + "sign_in_banner.mastodon_is": "Mastodon er den beste måten å følgje med på det som skjer.", "sign_in_banner.sign_in": "Logg inn", "sign_in_banner.sso_redirect": "Logg inn eller registrer deg", - "sign_in_banner.text": "Logg inn for å fylgja profilar eller emneknaggar, og for å lika, dela og svara på innlegg. Du kan òg samhandla med aktivitet på denne tenaren frå kontoar på andre tenarar.", "status.admin_account": "Opne moderasjonsgrensesnitt for @{name}", "status.admin_domain": "Opna moderatorgrensesnittet for {domain}", "status.admin_status": "Opne denne statusen i moderasjonsgrensesnittet", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 7f93ff0465..2bda373404 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -35,9 +35,7 @@ "account.follow_back": "Følg tilbake", "account.followers": "Følgere", "account.followers.empty": "Ingen følger denne brukeren ennå.", - "account.followers_counter": "{count, plural, one {{counter} følger} other {{counter} følgere}}", "account.following": "Følger", - "account.following_counter": "{count, plural, one {{counter} som følges} other {{counter} som følges}}", "account.follows.empty": "Denne brukeren følger ikke noen enda.", "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul fremhevinger fra @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} har bedt om å få følge deg", "account.share": "Del @{name} sin profil", "account.show_reblogs": "Vis fremhevinger fra @{name}", - "account.statuses_counter": "{count, plural, one {{counter} innlegg} other {{counter} innlegg}}", "account.unblock": "Opphev blokkering av @{name}", "account.unblock_domain": "Opphev blokkering av {domain}", "account.unblock_short": "Opphev blokkering", @@ -606,13 +603,10 @@ "server_banner.about_active_users": "Personer som har brukt denne serveren i løpet av de siste 30 dagene (aktive brukere månedlig)", "server_banner.active_users": "aktive brukere", "server_banner.administered_by": "Administrert av:", - "server_banner.introduction": "{domain} er en del av det desentraliserte sosiale nettverket drevet av {mastodon}.", - "server_banner.learn_more": "Finn ut mer", "server_banner.server_stats": "Serverstatistikk:", "sign_in_banner.create_account": "Opprett konto", "sign_in_banner.sign_in": "Logg inn", "sign_in_banner.sso_redirect": "Logg inn eller registrer deg", - "sign_in_banner.text": "Logg inn for å følge profiler eller emneknagger, favorittmarkere, dele og svare på innlegg. Du kan også samhandle fra din konto på en annen server.", "status.admin_account": "Åpne moderatorgrensesnittet for @{name}", "status.admin_domain": "Åpne moderatorgrensesnittet for {domain}", "status.admin_status": "Åpne denne statusen i moderatorgrensesnittet", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 3c32fed0ef..d977eed4af 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -32,9 +32,7 @@ "account.follow_back": "Sègre en retorn", "account.followers": "Seguidors", "account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.", - "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}", "account.following": "Abonat", - "account.following_counter": "{count, plural, one {{counter} Abonaments} other {{counter} Abonaments}}", "account.follows.empty": "Aqueste utilizaire sèc pas degun pel moment.", "account.go_to_profile": "Anar al perfil", "account.hide_reblogs": "Rescondre los partatges de @{name}", @@ -60,7 +58,6 @@ "account.requested_follow": "{name} a demandat a vos sègre", "account.share": "Partejar lo perfil a @{name}", "account.show_reblogs": "Mostrar los partatges de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Tut} other {{counter} Tuts}}", "account.unblock": "Desblocar @{name}", "account.unblock_domain": "Desblocar {domain}", "account.unblock_short": "Desblocat", @@ -499,8 +496,6 @@ "search_results.title": "Recèrca : {q}", "server_banner.active_users": "utilizaires actius", "server_banner.administered_by": "Administrat per :", - "server_banner.introduction": "{domain} fa part del malhum social descentralizat propulsat per {mastodon}.", - "server_banner.learn_more": "Ne saber mai", "server_banner.server_stats": "Estatisticas del servidor :", "sign_in_banner.create_account": "Crear un compte", "sign_in_banner.sign_in": "Se connectar", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index c693c24721..3828ff887e 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -25,9 +25,7 @@ "account.follow_back": "ਵਾਪਸ ਫਾਲ਼ੋ ਕਰੋ", "account.followers": "ਫ਼ਾਲੋਅਰ", "account.followers.empty": "ਇਸ ਵਰਤੋਂਕਾਰ ਨੂੰ ਹਾਲੇ ਕੋਈ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।", - "account.followers_counter": "{count, plural, one {{counter} ਫ਼ਾਲੋਅਰ} other {{counter} ਫ਼ਾਲੋਅਰ}}", "account.following": "ਫ਼ਾਲੋ ਕੀਤਾ", - "account.following_counter": "{count, plural, one {{counter} ਨੂੰ ਫ਼ਾਲੋ} other {{counter} ਨੂੰ ਫ਼ਾਲੋ}}", "account.follows.empty": "ਇਹ ਵਰਤੋਂਕਾਰ ਹਾਲੇ ਕਿਸੇ ਨੂੰ ਫ਼ਾਲੋ ਨਹੀਂ ਕਰਦਾ ਹੈ।", "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ", "account.media": "ਮੀਡੀਆ", @@ -41,7 +39,6 @@ "account.requested": "ਮਨਜ਼ੂਰੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ। ਫ਼ਾਲੋ ਬੇਨਤੀਆਂ ਨੂੰ ਰੱਦ ਕਰਨ ਲਈ ਕਲਿੱਕ ਕਰੋ", "account.requested_follow": "{name} ਨੇ ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਨ ਦੀ ਬੇਨਤੀ ਕੀਤੀ ਹੈ", "account.share": "{name} ਦਾ ਪਰੋਫ਼ਾਇਲ ਸਾਂਝਾ ਕਰੋ", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "@{name} ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ", "account.unblock_domain": "{domain} ਡੋਮੇਨ ਤੋਂ ਪਾਬੰਦੀ ਹਟਾਓ", "account.unblock_short": "ਪਾਬੰਦੀ ਹਟਾਓ", @@ -311,7 +308,6 @@ "search_results.see_all": "ਸਭ ਵੇਖੋ", "search_results.statuses": "ਪੋਸਟਾਂ", "search_results.title": "{q} ਲਈ ਖੋਜ", - "server_banner.learn_more": "ਹੋਰ ਜਾਣੋ", "sign_in_banner.create_account": "ਖਾਤਾ ਬਣਾਓ", "sign_in_banner.sign_in": "ਲਾਗਇਨ", "sign_in_banner.sso_redirect": "ਲਾਗਇਨ ਜਾਂ ਰਜਿਸਟਰ ਕਰੋ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 6f67e8f74f..ddfe1d4fbc 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -415,6 +415,7 @@ "limited_account_hint.title": "Ten profil został ukryty przez moderatorów {domain}.", "link_preview.author": "{name}", "link_preview.more_from_author": "Więcej od {name}", + "link_preview.shares": "{count, plural, one {{counter} wpis} few {{counter} wpisy} many {{counter} wpisów} other {{counter} wpisów}}", "lists.account.add": "Dodaj do listy", "lists.account.remove": "Usunąć z listy", "lists.delete": "Usuń listę", @@ -694,13 +695,13 @@ "server_banner.about_active_users": "Osoby korzystające z tego serwera w ciągu ostatnich 30 dni (Miesięcznie aktywni użytkownicy)", "server_banner.active_users": "aktywni użytkownicy", "server_banner.administered_by": "Zarządzana przez:", - "server_banner.introduction": "{domain} jest częścią zdecentralizowanej sieci społecznościowej wspieranej przez {mastodon}.", - "server_banner.learn_more": "Dowiedz się więcej", + "server_banner.is_one_of_many": "{domain} jest jedną z wielu niezależnych serwerów Mastodon, których możesz użyć by uczestniczyć w fediwersum.", "server_banner.server_stats": "Statystyki serwera:", "sign_in_banner.create_account": "Załóż konto", + "sign_in_banner.follow_anyone": "Obserwuj kogokolwiek z fediwersum w kolejności chronologicznej. Bez algorytmów ani reklam.", + "sign_in_banner.mastodon_is": "Mastodon to najlepszy sposób nadążania za bieżącymi zdarzeniami.", "sign_in_banner.sign_in": "Zaloguj się", "sign_in_banner.sso_redirect": "Zaloguj/zarejestruj się", - "sign_in_banner.text": "Zaloguj się, aby obserwować profile lub hashtagi, polubić, udostępnić oraz odpowiedzieć na posty. Możesz również wejść w interakcję z konta na innym serwerze.", "status.admin_account": "Otwórz interfejs moderacyjny dla @{name}", "status.admin_domain": "Otwórz interfejs moderacyjny dla {domain}", "status.admin_status": "Otwórz ten wpis w interfejsie moderacyjnym", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 3c8f3cf416..34d0ba36e6 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -35,9 +35,7 @@ "account.follow_back": "Seguir de volta", "account.followers": "Seguidores", "account.followers.empty": "Nada aqui.", - "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.following": "Seguindo", - "account.following_counter": "{count, plural, one {segue {counter}} other {segue {counter}}}", "account.follows.empty": "Nada aqui.", "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Ocultar boosts de @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} quer te seguir", "account.share": "Compartilhar perfil de @{name}", "account.show_reblogs": "Mostrar boosts de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear domínio {domain}", "account.unblock_short": "Desbloquear", @@ -415,6 +412,7 @@ "limited_account_hint.title": "Este perfil foi ocultado pelos moderadores do {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Mais de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicação} other {{counter} publicações}}", "lists.account.add": "Adicionar à lista", "lists.account.remove": "Remover da lista", "lists.delete": "Excluir lista", @@ -695,13 +693,13 @@ "server_banner.about_active_users": "Pessoas usando este servidor durante os últimos 30 dias (Usuários ativos mensalmente)", "server_banner.active_users": "usuários ativos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} faz parte da rede social descentralizada desenvolvida por {mastodon}.", - "server_banner.learn_more": "Saiba mais", + "server_banner.is_one_of_many": "{domain} é um dos muitos servidores Mastodon independentes que você pode usar para participar do fediverso.", "server_banner.server_stats": "Estatísticas do servidor:", "sign_in_banner.create_account": "Criar conta", + "sign_in_banner.follow_anyone": "Siga alguém pelo fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait à vista.", + "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está acontecendo.", "sign_in_banner.sign_in": "Entrar", "sign_in_banner.sso_redirect": "Entrar ou Registrar-se", - "sign_in_banner.text": "Identifique-se para seguir perfis ou 'hashtags', favoritar, compartilhar e responder publicações. Você também pode interagir a partir da sua conta em um servidor diferente.", "status.admin_account": "Abrir interface de moderação para @{name}", "status.admin_domain": "Abrir interface de moderação para {domain}", "status.admin_status": "Abrir este toot na interface de moderação", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index c389d4f4fc..6a6feca309 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -37,7 +37,7 @@ "account.followers.empty": "Ainda ninguém segue este utilizador.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", "account.following": "A seguir", - "account.following_counter": "{count, plural, other {A seguir {counter}}}", + "account.following_counter": "{count, plural, one {A seguir {counter}} other {A seguir {counter}}}", "account.follows.empty": "Este utilizador ainda não segue ninguém.", "account.go_to_profile": "Ir para o perfil", "account.hide_reblogs": "Esconder partilhas de @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} pediu para segui-lo", "account.share": "Partilhar o perfil @{name}", "account.show_reblogs": "Mostrar partilhas de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", + "account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}", "account.unblock": "Desbloquear @{name}", "account.unblock_domain": "Desbloquear o domínio {domain}", "account.unblock_short": "Desbloquear", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Este perfil foi ocultado pelos moderadores de {domain}.", "link_preview.author": "Por {name}", "link_preview.more_from_author": "Mais de {name}", + "link_preview.shares": "{count, plural, one {{counter} publicação} other {{counter} publicações}}", "lists.account.add": "Adicionar à lista", "lists.account.remove": "Remover da lista", "lists.delete": "Eliminar lista", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Pessoas que utilizaram este servidor nos últimos 30 dias (Utilizadores Ativos Mensais)", "server_banner.active_users": "utilizadores ativos", "server_banner.administered_by": "Administrado por:", - "server_banner.introduction": "{domain} faz parte da rede social descentralizada baseada no {mastodon}.", - "server_banner.learn_more": "Saber mais", + "server_banner.is_one_of_many": "{domain} é um dos muitos servidores Mastodon independentes que pode utilizar para participar no fediverso.", "server_banner.server_stats": "Estatísticas do servidor:", "sign_in_banner.create_account": "Criar conta", + "sign_in_banner.follow_anyone": "Siga alguém no fediverso e veja tudo em ordem cronológica. Sem algoritmos, anúncios ou clickbait à vista.", + "sign_in_banner.mastodon_is": "O Mastodon é a melhor maneira de acompanhar o que está a acontecer.", "sign_in_banner.sign_in": "Iniciar Sessão", "sign_in_banner.sso_redirect": "Inicie Sessão ou Registe-se", - "sign_in_banner.text": "Inicie sessão para seguir perfis ou etiquetas, assinale como favorito, partilhe ou responda a publicações. Pode ainda interagir através da sua conta noutro servidor.", "status.admin_account": "Abrir a interface de moderação para @{name}", "status.admin_domain": "Abrir interface de moderação para {domain}", "status.admin_status": "Abrir esta publicação na interface de moderação", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 0aef0ebd96..35abf1b021 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -35,9 +35,7 @@ "account.follow_back": "Urmăreşte înapoi", "account.followers": "Urmăritori", "account.followers.empty": "Acest utilizator nu are încă urmăritori.", - "account.followers_counter": "{count, plural, one {Un abonat} few {{counter} abonați} other {{counter} de abonați}}", "account.following": "Urmăriți", - "account.following_counter": "{count, plural, one {Un abonament} few {{counter} abonamente} other {{counter} de abonamente}}", "account.follows.empty": "Momentan acest utilizator nu are niciun abonament.", "account.go_to_profile": "Mergi la profil", "account.hide_reblogs": "Ascunde distribuirile de la @{name}", @@ -62,7 +60,6 @@ "account.requested_follow": "{name} A cerut să vă urmărească", "account.share": "Distribuie profilul lui @{name}", "account.show_reblogs": "Afișează distribuirile de la @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "Deblochează pe @{name}", "account.unblock_domain": "Deblochează domeniul {domain}", "account.unblock_short": "Deblochează", @@ -550,8 +547,6 @@ "server_banner.about_active_users": "Persoane care au folosit acest server în ultimele 30 de zile (Utilizatori Lunari Activi)", "server_banner.active_users": "utilizatori activi", "server_banner.administered_by": "Administrat de:", - "server_banner.introduction": "{domain} face parte din rețeaua socială descentralizată alimentată de {mastodon}.", - "server_banner.learn_more": "Află mai multe", "server_banner.server_stats": "Statisticile serverului:", "sign_in_banner.create_account": "Creează-ți un cont", "sign_in_banner.sign_in": "Conectează-te", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 07a41385a2..97a1f0b09c 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -35,9 +35,7 @@ "account.follow_back": "Подписаться в ответ", "account.followers": "Подписчики", "account.followers.empty": "На этого пользователя пока никто не подписан.", - "account.followers_counter": "{count, plural, one {{counter} подписчик} many {{counter} подписчиков} other {{counter} подписчика}}", "account.following": "Подписки", - "account.following_counter": "{count, plural, one {{counter} подписка} many {{counter} подписок} other {{counter} подписки}}", "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", "account.go_to_profile": "Перейти к профилю", "account.hide_reblogs": "Скрыть продвижения от @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} отправил(а) вам запрос на подписку", "account.share": "Поделиться профилем @{name}", "account.show_reblogs": "Показывать продвижения от @{name}", - "account.statuses_counter": "{count, plural, one {{counter} пост} many {{counter} постов} other {{counter} поста}}", "account.unblock": "Разблокировать @{name}", "account.unblock_domain": "Разблокировать {domain}", "account.unblock_short": "Разблокировать", @@ -297,6 +294,7 @@ "filter_modal.select_filter.subtitle": "Используйте существующую категорию или создайте новую", "filter_modal.select_filter.title": "Фильтровать этот пост", "filter_modal.title.status": "Фильтровать пост", + "filtered_notifications_banner.mentions": "{count, plural, one {упоминание} other {упоминания}}", "filtered_notifications_banner.pending_requests": "Уведомления от {count, plural, =0 {никого} one {# человека} other {# других людей, с кем вы можете быть знакомы}}", "filtered_notifications_banner.title": "Отфильтрованные уведомления", "firehose.all": "Все", @@ -307,6 +305,8 @@ "follow_requests.unlocked_explanation": "Хотя ваша учетная запись не закрыта, команда {domain} подумала, что вы захотите просмотреть запросы от этих учетных записей вручную.", "follow_suggestions.curated_suggestion": "Выбор администрации", "follow_suggestions.dismiss": "Больше не показывать", + "follow_suggestions.featured_longer": "Отобранные командой {domain} вручную", + "follow_suggestions.friends_of_friends_longer": "Популярно среди людей, на которых вы подписаны", "follow_suggestions.hints.featured": "Этот профиль был вручную выбран командой {domain}.", "follow_suggestions.hints.friends_of_friends": "Этот профиль популярен среди людей, на которых вы подписаны.", "follow_suggestions.hints.most_followed": "Этот профиль один из самых отслеживаемых на {domain}.", @@ -314,6 +314,8 @@ "follow_suggestions.hints.similar_to_recently_followed": "Этот профиль похож на другие профили, на которые вы подписывались в последнее время.", "follow_suggestions.personalized_suggestion": "Персонализированное предложение", "follow_suggestions.popular_suggestion": "Популярное предложение", + "follow_suggestions.popular_suggestion_longer": "Популярное на {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Похоже на профили, на которые вы недавно подписались", "follow_suggestions.view_all": "Посмотреть все", "follow_suggestions.who_to_follow": "На кого подписаться", "followed_tags": "Отслеживаемые хэштеги", @@ -409,6 +411,8 @@ "limited_account_hint.action": "Все равно показать профиль", "limited_account_hint.title": "Этот профиль был скрыт модераторами {domain}.", "link_preview.author": "Автор: {name}", + "link_preview.more_from_author": "Больше от {name}", + "link_preview.shares": "{count, plural, one {{counter} пост} other {{counter} посты}}", "lists.account.add": "Добавить в список", "lists.account.remove": "Убрать из списка", "lists.delete": "Удалить список", @@ -468,7 +472,15 @@ "notification.follow": "{name} подписался (-лась) на вас", "notification.follow_request": "{name} отправил запрос на подписку", "notification.mention": "{name} упомянул(а) вас", + "notification.moderation-warning.learn_more": "Узнать больше", + "notification.moderation_warning": "Вы получили предупреждение от модерации", "notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.", + "notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.", + "notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые из ваших сообщений были отмечены как деликатные.", + "notification.moderation_warning.action_none": "Ваша учётная запись получила предупреждение от модерации.", + "notification.moderation_warning.action_sensitive": "С этого момента ваши сообщения будут помечены как деликатные.", + "notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.", + "notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.", "notification.own_poll": "Ваш опрос закончился", "notification.poll": "Опрос, в котором вы приняли участие, завершился", "notification.reblog": "{name} продвинул(а) ваш пост", @@ -489,6 +501,8 @@ "notifications.column_settings.admin.sign_up": "Новые регистрации:", "notifications.column_settings.alert": "Уведомления на рабочем столе", "notifications.column_settings.favourite": "Избранные:", + "notifications.column_settings.filter_bar.advanced": "Отображать все категории", + "notifications.column_settings.filter_bar.category": "Панель сортировки", "notifications.column_settings.follow": "У вас новый подписчик:", "notifications.column_settings.follow_request": "Новые запросы на подписку:", "notifications.column_settings.mention": "Вас упомянули в посте:", @@ -514,7 +528,14 @@ "notifications.permission_denied": "Уведомления на рабочем столе недоступны, так как вы запретили их отправку в браузере. Проверьте настройки для сайта, чтобы включить их обратно.", "notifications.permission_denied_alert": "Уведомления на рабочем столе недоступны, так как вы ранее отклонили запрос на их отправку.", "notifications.permission_required": "Чтобы включить уведомления на рабочем столе, необходимо разрешить их в браузере.", + "notifications.policy.filter_new_accounts.hint": "Создано в течение последних {days, plural, one {один день} few {# дней} many {# дней} other {# дня}}", "notifications.policy.filter_new_accounts_title": "Новые учётные записи", + "notifications.policy.filter_not_followers_title": "Люди, не подписанные на вас", + "notifications.policy.filter_not_following_hint": "Пока вы не одобрите их вручную", + "notifications.policy.filter_not_following_title": "Люди, на которых вы не подписаны", + "notifications.policy.filter_private_mentions_hint": "Фильтруется, если только это не ответ на ваше собственное упоминание или если вы подписаны на отправителя", + "notifications.policy.filter_private_mentions_title": "Нежелательные личные упоминания", + "notifications.policy.title": "Фильтровать уведомления от…", "notifications_permission_banner.enable": "Включить уведомления", "notifications_permission_banner.how_to_control": "Получайте уведомления даже когда Mastodon закрыт, включив уведомления на рабочем столе. А чтобы лишний шум не отвлекал, вы можете настроить какие уведомления вы хотите получать, нажав на кнопку {icon} выше.", "notifications_permission_banner.title": "Будьте в курсе происходящего", @@ -671,13 +692,10 @@ "server_banner.about_active_users": "Люди, заходившие на этот сервер за последние 30 дней (ежемесячные активные пользователи)", "server_banner.active_users": "активные пользователи", "server_banner.administered_by": "Управляется:", - "server_banner.introduction": "{domain} является частью децентрализованной социальной сети, основанной на {mastodon}.", - "server_banner.learn_more": "Узнать больше", "server_banner.server_stats": "Статистика сервера:", "sign_in_banner.create_account": "Создать учётную запись", "sign_in_banner.sign_in": "Войти", "sign_in_banner.sso_redirect": "Войдите или Зарегистрируйтесь", - "sign_in_banner.text": "Войдите, чтобы отслеживать профили, хэштеги или избранное, делиться сообщениями и отвечать на них. Вы также можете взаимодействовать с вашей учётной записью на другом сервере.", "status.admin_account": "Открыть интерфейс модератора для @{name}", "status.admin_domain": "Открыть интерфейс модерации {domain}", "status.admin_status": "Открыть этот пост в интерфейсе модератора", @@ -691,6 +709,7 @@ "status.direct": "Лично упоминать @{name}", "status.direct_indicator": "Личные упоминания", "status.edit": "Изменить", + "status.edited": "Дата последнего изменения: {date}", "status.edited_x_times": "{count, plural, one {{count} изменение} many {{count} изменений} other {{count} изменения}}", "status.embed": "Встроить на свой сайт", "status.favourite": "Избранное", diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json index 99aa46bc89..c3880a6b03 100644 --- a/app/javascript/mastodon/locales/sa.json +++ b/app/javascript/mastodon/locales/sa.json @@ -32,9 +32,7 @@ "account.follow": "अनुस्रियताम्", "account.followers": "अनुसर्तारः", "account.followers.empty": "नाऽनुसर्तारो वर्तन्ते", - "account.followers_counter": "{count, plural, one {{counter} अनुसर्ता} two {{counter} अनुसर्तारौ} other {{counter} अनुसर्तारः}}", "account.following": "अनुसरति", - "account.following_counter": "{count, plural, one {{counter} अनुसृतः} two {{counter} अनुसृतौ} other {{counter} अनुसृताः}}", "account.follows.empty": "न कोऽप्यनुसृतो वर्तते", "account.go_to_profile": "प्रोफायिलं गच्छ", "account.hide_reblogs": "@{name} मित्रस्य प्रकाशनानि छिद्यन्ताम्", @@ -56,7 +54,6 @@ "account.requested_follow": "{name} त्वामनुसर्तुमयाचीत्", "account.share": "@{name} मित्रस्य विवरणं विभाज्यताम्", "account.show_reblogs": "@{name} मित्रस्य प्रकाशनानि दृश्यन्ताम्", - "account.statuses_counter": "{count, plural, one {{counter} पत्रम्} two{{counter} पत्रे} other {{counter} पत्राणि}}", "account.unblock": "निषेधता नश्यताम् @{name}", "account.unblock_domain": "प्रदेशनिषेधता नश्यताम् {domain}", "account.unblock_short": "अनवरुन्धि", @@ -499,8 +496,6 @@ "server_banner.about_active_users": "विगतेषु ३० दिनेषु सर्वरमिममुपयुज्यमाणा जनाः (मासिकसक्रियोपभोक्तारः)", "server_banner.active_users": "सक्रियोपभोक्तारः", "server_banner.administered_by": "इत्यनेन अधिकृतः : ", - "server_banner.introduction": "{domain} {mastodon} इत्यनेन सामर्थितो विकेन्द्रीयसामाजिकजालकर्मणोंऽशोऽस्ति।", - "server_banner.learn_more": "अधिकं ज्ञायताम्", "server_banner.server_stats": "सर्वरः स्थितिविषयकानि :", "sign_in_banner.create_account": "समयं संसृज", "sign_in_banner.sign_in": "सम्प्रवेशं कुरु", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index 1a5f2ef0f3..8955573737 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -26,9 +26,7 @@ "account.follow": "Sighi", "account.followers": "Sighiduras", "account.followers.empty": "Nemos sighit ancora custa persone.", - "account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}", "account.following": "Sighende", - "account.following_counter": "{count, plural, one {Sighende a {counter}} other {Sighende a {counter}}}", "account.follows.empty": "Custa persone non sighit ancora a nemos.", "account.hide_reblogs": "Cua is cumpartziduras de @{name}", "account.in_memoriam": "In memoriam.", @@ -47,7 +45,6 @@ "account.requested_follow": "{name} at dimandadu de ti sighire", "account.share": "Cumpartzi su profilu de @{name}", "account.show_reblogs": "Ammustra is cumpartziduras de @{name}", - "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "account.unblock": "Isbloca a @{name}", "account.unblock_domain": "Isbloca su domìniu {domain}", "account.unendorse": "Non cussiges in su profilu", @@ -375,7 +372,6 @@ "search_results.hashtags": "Etichetas", "search_results.statuses": "Publicatziones", "server_banner.administered_by": "Amministradu dae:", - "server_banner.learn_more": "Àteras informatziones", "server_banner.server_stats": "Istatìsticas de su serbidore:", "sign_in_banner.sign_in": "Sign in", "status.admin_account": "Aberi s'interfache de moderatzione pro @{name}", diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json index ba62c11f71..397f63fed4 100644 --- a/app/javascript/mastodon/locales/sco.json +++ b/app/javascript/mastodon/locales/sco.json @@ -31,9 +31,7 @@ "account.follow": "Follae", "account.followers": "Follaers", "account.followers.empty": "Naebody follaes this uiser yit.", - "account.followers_counter": "{count, plural, one {{counter} Follaer} other {{counter} Follaers}}", "account.following": "Follaein", - "account.following_counter": "{count, plural, one {{counter} Follaein} other {{counter} Follaein}}", "account.follows.empty": "This uiser disnae follae oniebody yit.", "account.go_to_profile": "Gang tae profile", "account.hide_reblogs": "Dinnae shaw heezes fae @{name}", @@ -53,7 +51,6 @@ "account.requested": "Haudin fir approval. Chap tae cancel follae request", "account.share": "Share @{name}'s profile", "account.show_reblogs": "Shaw heezes frae @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Posts}}", "account.unblock": "Undingie @{name}", "account.unblock_domain": "Undingie domain {domain}", "account.unblock_short": "Undingie", @@ -471,8 +468,6 @@ "server_banner.about_active_users": "Fowk uisin this server in the last 30 days (Monthly Active Uisers)", "server_banner.active_users": "active uisers", "server_banner.administered_by": "Administert bi:", - "server_banner.introduction": "{domain} is pairt o the decentralized social network pooery bi {mastodon}.", - "server_banner.learn_more": "Lairn mair", "server_banner.server_stats": "Server stats:", "sign_in_banner.create_account": "Mak accoont", "sign_in_banner.sign_in": "Sign in", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index ccbface05a..fbfdfaa659 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -23,9 +23,7 @@ "account.follow": "අනුගමනය", "account.followers": "අනුගාමිකයින්", "account.followers.empty": "කිසිවෙක් අනුගමනය කර නැත.", - "account.followers_counter": "{count, plural, one {අනුගාමිකයින් {counter}} other {අනුගාමිකයින් {counter}}}", "account.following": "අනුගමන", - "account.following_counter": "{count, plural, one {අනුගමන {counter}} other {අනුගමන {counter}}}", "account.follows.empty": "තවමත් කිසිවෙක් අනුගමනය නොකරයි.", "account.go_to_profile": "පැතිකඩට යන්න", "account.joined_short": "එක් වූ දිනය", @@ -39,7 +37,6 @@ "account.posts_with_replies": "ලිපි සහ පිළිතුරු", "account.report": "@{name} වාර්තා කරන්න", "account.share": "@{name} ගේ පැතිකඩ බෙදාගන්න", - "account.statuses_counter": "{count, plural, one {ලිපි {counter}} other {ලිපි {counter}}}", "account.unblock": "@{name} අනවහිර කරන්න", "account.unblock_domain": "{domain} වසම අනවහිර කරන්න", "account.unblock_short": "අනවහිර", @@ -395,7 +392,6 @@ "search_results.statuses": "ලිපි", "search_results.title": "{q} සොයන්න", "server_banner.active_users": "සක්‍රිය පරිශ්‍රීලකයින්", - "server_banner.learn_more": "තව දැනගන්න", "sign_in_banner.create_account": "ගිණුමක් සාදන්න", "sign_in_banner.sign_in": "පිවිසෙන්න", "status.admin_status": "මෙම ලිපිය මැදිහත්කරණ අතුරුමුහුණතෙහි අරින්න", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index 9b5be21f9d..ed877f7667 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -35,9 +35,7 @@ "account.follow_back": "Sledovať späť", "account.followers": "Sledovatelia", "account.followers.empty": "Tento účet ešte nikto nesleduje.", - "account.followers_counter": "{count, plural, one {{counter} sledujúci účet} few {{counter} sledujúce účty} many {{counter} sledujúcich účtov} other {{counter} sledujúcich účtov}}", "account.following": "Sledovaný účet", - "account.following_counter": "{count, plural, one {{counter} sledovaný účet} few {{counter} sledované účty} many {{counter} sledovaných účtov} other {{counter} sledovaných účtov}}", "account.follows.empty": "Tento účet ešte nikoho nesleduje.", "account.go_to_profile": "Prejsť na profil", "account.hide_reblogs": "Skryť zdieľania od @{name}", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} vás chce sledovať", "account.share": "Zdieľaj profil @{name}", "account.show_reblogs": "Zobrazovať zdieľania od @{name}", - "account.statuses_counter": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}}", "account.unblock": "Odblokovať @{name}", "account.unblock_domain": "Odblokovať doménu {domain}", "account.unblock_short": "Odblokovať", @@ -91,7 +88,10 @@ "audio.hide": "Skryť zvuk", "block_modal.show_less": "Zobraziť menej", "block_modal.show_more": "Zobraziť viac", + "block_modal.they_cant_mention": "Nemôžu ťa spomenúť, alebo nasledovať.", + "block_modal.they_will_know": "Môžu vidieť, že sú zablokovaní/ý.", "block_modal.title": "Blokovať užívateľa?", + "block_modal.you_wont_see_mentions": "Neuvidíš príspevky, ktoré ich spomínajú.", "boost_modal.combo": "Nabudúce môžete preskočiť stlačením {combo}", "bundle_column_error.copy_stacktrace": "Kopírovať chybovú hlášku", "bundle_column_error.error.body": "Požadovanú stránku nebolo možné vykresliť. Môže to byť spôsobené chybou v našom kóde alebo problémom s kompatibilitou prehliadača.", @@ -656,13 +656,10 @@ "server_banner.about_active_users": "Ľudia používajúci tento server za posledných 30 dní (aktívni používatelia za mesiac)", "server_banner.active_users": "Aktívne účty", "server_banner.administered_by": "Správa servera:", - "server_banner.introduction": "{domain} je súčasťou decentralizovanej sociálnej siete využívajúcej technológiu {mastodon}.", - "server_banner.learn_more": "Viac informácií", "server_banner.server_stats": "Štatistiky servera:", "sign_in_banner.create_account": "Vytvoriť účet", "sign_in_banner.sign_in": "Prihlásiť sa", "sign_in_banner.sso_redirect": "Prihlásenie alebo registrácia", - "sign_in_banner.text": "Prihláste sa, aby ste mohli sledovať profily alebo hashtagy, hviezdičkovať, zdieľať a odpovedať na príspevky. Môžete tiež komunikovať zo svojho účtu na inom serveri.", "status.admin_account": "Moderovať @{name}", "status.admin_domain": "Moderovať {domain}", "status.admin_status": "Moderovať príspevok", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index a8cce3202c..2a3d74a80f 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -35,9 +35,9 @@ "account.follow_back": "Sledi nazaj", "account.followers": "Sledilci", "account.followers.empty": "Nihče ne sledi temu uporabniku.", - "account.followers_counter": "{count, plural, one {ima {counter} sledilca} two {ima {counter} sledilca} few {ima {counter} sledilce} other {ima {counter} sledilcev}}", + "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}", "account.following": "Sledim", - "account.following_counter": "{count, plural, one {sledi {count} osebi} two {sledi {count} osebama} few {sledi {count} osebam} other {sledi {count} osebam}}", + "account.following_counter": "{count, plural, one {{counter} sleden} two {{counter} sledena} few {{counter} sledeni} other {{counter} sledenih}}", "account.follows.empty": "Ta uporabnik še ne sledi nikomur.", "account.go_to_profile": "Pojdi na profil", "account.hide_reblogs": "Skrij izpostavitve od @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} vam želi slediti", "account.share": "Deli profil osebe @{name}", "account.show_reblogs": "Pokaži izpostavitve osebe @{name}", - "account.statuses_counter": "{count, plural, one {{count} objava} two {{count} objavi} few {{count} objave} other {{count} objav}}", + "account.statuses_counter": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}", "account.unblock": "Odblokiraj @{name}", "account.unblock_domain": "Odblokiraj domeno {domain}", "account.unblock_short": "Odblokiraj", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.", "link_preview.author": "Avtor_ica {name}", "link_preview.more_from_author": "Več od {name}", + "link_preview.shares": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}", "lists.account.add": "Dodaj na seznam", "lists.account.remove": "Odstrani s seznama", "lists.delete": "Izbriši seznam", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Osebe, ki so uporabljale ta strežnik zadnjih 30 dni (dejavni uporabniki meseca)", "server_banner.active_users": "dejavnih uporabnikov", "server_banner.administered_by": "Upravlja:", - "server_banner.introduction": "{domain} je del decentraliziranega družbenega omrežja, ki ga poganja {mastodon}.", - "server_banner.learn_more": "Več o tem", + "server_banner.is_one_of_many": "{domain} je en izmed mnogih neodvisnih strežnikov Mastodon, ki ga lahko uporabljate za sodelovanje v fediverzumu.", "server_banner.server_stats": "Statistika strežnika:", "sign_in_banner.create_account": "Ustvari račun", + "sign_in_banner.follow_anyone": "Sledite komurkoli iz fediverzuma in vidite vse objave v časovnem vrstnem redu. Brez skritih algoritmov ter brez oglasov in vab za klikanje na vidiku.", + "sign_in_banner.mastodon_is": "Mastodon je najboljši način, da ste na tekočem z dogajanjem.", "sign_in_banner.sign_in": "Prijava", "sign_in_banner.sso_redirect": "Prijavite ali registrirajte se", - "sign_in_banner.text": "Prijavite se, da sledite profilom ali ključnikom, dodajate med priljubljene, delite z drugimi ter odgovarjate na objave. V interakciji ste lahko tudi iz svojega računa na drugem strežniku.", "status.admin_account": "Odpri vmesnik za moderiranje za @{name}", "status.admin_domain": "Odpri vmesnik za moderiranje za {domain}", "status.admin_status": "Odpri to objavo v vmesniku za moderiranje", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 6b3c5fbd90..96b7b3fefc 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -35,9 +35,9 @@ "account.follow_back": "Ndiqe gjithashtu", "account.followers": "Ndjekës", "account.followers.empty": "Këtë përdorues ende s’e ndjek kush.", - "account.followers_counter": "{count, plural, one {{counter} Ndjekës} other {{counter} Ndjekës}}", + "account.followers_counter": "{count, plural, one {{counter} ndjekës} other {{counter} ndjekës}}", "account.following": "Ndjekje", - "account.following_counter": "{count, plural, one {{counter} i Ndjekur} other {{counter} të Ndjekur}}", + "account.following_counter": "{count, plural, one {{counter} i ndjekur} other {{counter} të ndjekur}}", "account.follows.empty": "Ky përdorues ende s’ndjek kënd.", "account.go_to_profile": "Kalo te profili", "account.hide_reblogs": "Fshih përforcime nga @{name}", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} ka kërkuar t’ju ndjekë", "account.share": "Ndajeni profilin e @{name} me të tjerët", "account.show_reblogs": "Shfaq përforcime nga @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Mesazh} other {{counter} Mesazhe}}", + "account.statuses_counter": "{count, plural, one {{counter} postim} other {{counter} postime}}", "account.unblock": "Zhbllokoje @{name}", "account.unblock_domain": "Zhblloko përkatësinë {domain}", "account.unblock_short": "Zhbllokoje", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Ky profil është fshehur nga moderatorët e {domain}.", "link_preview.author": "Nga {name}", "link_preview.more_from_author": "Më tepër nga {name}", + "link_preview.shares": "{count, plural, one {{counter} post} other {{counter} postime}}", "lists.account.add": "Shto në listë", "lists.account.remove": "Hiqe nga lista", "lists.delete": "Fshije listën", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Persona që përdorin këtë shërbyes gjatë 30 ditëve të fundit (Përdorues Mujorë Aktivë)", "server_banner.active_users": "përdorues aktivë", "server_banner.administered_by": "Administruar nga:", - "server_banner.introduction": "{domain} është pjesë e rrjetit shoqëror të decentralizuar të ngritur mbi {mastodon}.", - "server_banner.learn_more": "Mësoni më tepër", + "server_banner.is_one_of_many": "{domain} është një nga mjaft shërbyes të pavarur Mastodon te të cilët mund të merrni pjesë në Fedivers.", "server_banner.server_stats": "Statistika shërbyesi:", "sign_in_banner.create_account": "Krijoni llogari", + "sign_in_banner.follow_anyone": "Ndiqni këdo në Fedivers dhe shihni gjithçka në rend kohor. Pa algortime, apo marifete.", + "sign_in_banner.mastodon_is": "Mastodon-i është rruga më e mirë për të ndjekur se ç’ndodh.", "sign_in_banner.sign_in": "Hyni", "sign_in_banner.sso_redirect": "Bëni hyrjen, ose Regjistrohuni", - "sign_in_banner.text": "Që të ndiqni profile ose hashtagë, t’u vini shenjë si të parapëlqyer, të ndani me të tjerë dhe t’i ripostoni në postime, bëni hyrjen në llogari. Mundeni edhe të ndërveproni që nga llogaria juaj në një shërbyes tjetër.", "status.admin_account": "Hap ndërfaqe moderimi për @{name}", "status.admin_domain": "Hap ndërfaqe moderimi për {domain}", "status.admin_status": "Hape këtë mesazh te ndërfaqja e moderimit", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index a78b9ff5b4..71b69d428a 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -63,7 +63,7 @@ "account.requested_follow": "{name} je zatražio da vas prati", "account.share": "Podeli profil korisnika @{name}", "account.show_reblogs": "Prikaži podržavanja od korisnika @{name}", - "account.statuses_counter": "{count, plural, one {{counter} objavio} few {{counter} objavio} other {{counter} objavio}}", + "account.statuses_counter": "{count, plural, one {{counter} objava} few {{counter} objave} other {{counter} objava}}", "account.unblock": "Odblokiraj korisnika @{name}", "account.unblock_domain": "Odblokiraj domen {domain}", "account.unblock_short": "Odblokiraj", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Ovaj profil su sakrili moderatori {domain}.", "link_preview.author": "Po {name}", "link_preview.more_from_author": "Više od {name}", + "link_preview.shares": "{count, plural, one {{counter} objava} few {{counter} objave} other {{counter} objava}}", "lists.account.add": "Dodaj na listu", "lists.account.remove": "Ukloni sa liste", "lists.delete": "Izbriši listu", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Ljudi koji su koristili ovaj server u prethodnih 30 dana (mesečno aktivnih korisnika)", "server_banner.active_users": "aktivnih korisnika", "server_banner.administered_by": "Administrira:", - "server_banner.introduction": "{domain} je deo decentralizovane društvene mreže koju pokreće {mastodon}.", - "server_banner.learn_more": "Saznajte više", + "server_banner.is_one_of_many": "{domain} je jedan od mnogih nezavisnih Mastodon servera koje možete koristiti za učešće u fediverzumu.", "server_banner.server_stats": "Statistike servera:", "sign_in_banner.create_account": "Napravite nalog", + "sign_in_banner.follow_anyone": "Pratite bilo koga širom fediverzuma i pogledajte sve hronološkim redom. Nema algoritama, reklama ili mamaca za klikove na vidiku.", + "sign_in_banner.mastodon_is": "Mastodon je najbolji način da budete u toku sa onim što se dešava.", "sign_in_banner.sign_in": "Prijavite se", "sign_in_banner.sso_redirect": "Prijavite se ili se registrujte", - "sign_in_banner.text": "Prijavite se da biste pratili profile ili heš oznake, označili objave kao omiljene, delili i odgovarali na njih. Takođe možete komunicirati sa svog naloga na drugom serveru.", "status.admin_account": "Otvori moderatorsko okruženje za @{name}", "status.admin_domain": "Otvori moderatorsko okruženje za {domain}", "status.admin_status": "Otvori ovu objavu u moderatorskom okruženju", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 5c14faea85..2c4649f9d0 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -63,7 +63,7 @@ "account.requested_follow": "{name} је затражио да вас прати", "account.share": "Подели профил корисника @{name}", "account.show_reblogs": "Прикажи подржавања од корисника @{name}", - "account.statuses_counter": "{count, plural, one {{counter} објавио} few {{counter} објавио} other {{counter} објавио}}", + "account.statuses_counter": "{count, plural, one {{counter} објава} few {{counter} објаве} other {{counter} објава}}", "account.unblock": "Одблокирај корисника @{name}", "account.unblock_domain": "Одблокирај домен {domain}", "account.unblock_short": "Одблокирај", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Овај профил су сакрили модератори {domain}.", "link_preview.author": "По {name}", "link_preview.more_from_author": "Више од {name}", + "link_preview.shares": "{count, plural, one {{counter} објава} few {{counter} објаве} other {{counter} објава}}", "lists.account.add": "Додај на листу", "lists.account.remove": "Уклони са листе", "lists.delete": "Избриши листу", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Људи који су користили овај сервер у претходних 30 дана (месечно активних корисника)", "server_banner.active_users": "активних корисника", "server_banner.administered_by": "Администрира:", - "server_banner.introduction": "{domain} је део децентрализоване друштвене мреже коју покреће {mastodon}.", - "server_banner.learn_more": "Сазнајте више", + "server_banner.is_one_of_many": "{domain} је један од многих независних Mastodon сервера које можете користити за учешће у федиверзуму.", "server_banner.server_stats": "Статистике сервера:", "sign_in_banner.create_account": "Направите налог", + "sign_in_banner.follow_anyone": "Пратите било кога широм федиверзума и погледајте све хронолошким редом. Нема алгоритама, реклама или мамаца за кликове на видику.", + "sign_in_banner.mastodon_is": "Mastodon је најбољи начин да будете у току са оним што се дешава.", "sign_in_banner.sign_in": "Пријавите се", "sign_in_banner.sso_redirect": "Пријавите се или се региструјте", - "sign_in_banner.text": "Пријавите се да бисте пратили профиле или хеш ознаке, означили објаве као омиљене, делили и одговарали на њих. Такође можете комуницирати са свог налога на другом серверу.", "status.admin_account": "Отвори модераторско окружење за @{name}", "status.admin_domain": "Отвори модераторско окружење за {domain}", "status.admin_status": "Отвори ову објаву у модераторском окружењу", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index a1d478b7ad..1833a2cfde 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -37,7 +37,6 @@ "account.followers.empty": "Ingen följer denna användare än.", "account.followers_counter": "{count, plural, one {{counter} följare} other {{counter} följare}}", "account.following": "Följer", - "account.following_counter": "{count, plural, one {{counter} följd} other {{counter} följda}}", "account.follows.empty": "Denna användare följer inte någon än.", "account.go_to_profile": "Gå till profilen", "account.hide_reblogs": "Dölj boostar från @{name}", @@ -415,6 +414,7 @@ "limited_account_hint.title": "Denna profil har dolts av {domain}s moderatorer.", "link_preview.author": "Av {name}", "link_preview.more_from_author": "Mer från {name}", + "link_preview.shares": "{count, plural, one {{counter} inlägg} other {{counter} inlägg}}", "lists.account.add": "Lägg till i lista", "lists.account.remove": "Ta bort från lista", "lists.delete": "Radera lista", @@ -695,13 +695,10 @@ "server_banner.about_active_users": "Personer som använt denna server de senaste 30 dagarna (månatligt aktiva användare)", "server_banner.active_users": "aktiva användare", "server_banner.administered_by": "Administrerad av:", - "server_banner.introduction": "{domain} är en del av det decentraliserade sociala nätverket som drivs av {mastodon}.", - "server_banner.learn_more": "Lär dig mer", "server_banner.server_stats": "Serverstatistik:", "sign_in_banner.create_account": "Skapa konto", "sign_in_banner.sign_in": "Logga in", "sign_in_banner.sso_redirect": "Logga in eller registrera dig", - "sign_in_banner.text": "Logga in för att följa profiler eller hashtaggar, favoritmarkera, dela och svara på inlägg. Du kan också interagera med ditt konto på en annan server.", "status.admin_account": "Öppet modereringsgränssnitt för @{name}", "status.admin_domain": "Öppet modereringsgränssnitt för @{domain}", "status.admin_status": "Öppna detta inlägg i modereringsgränssnittet", diff --git a/app/javascript/mastodon/locales/szl.json b/app/javascript/mastodon/locales/szl.json index 43cfc78d5b..34d086eb48 100644 --- a/app/javascript/mastodon/locales/szl.json +++ b/app/javascript/mastodon/locales/szl.json @@ -23,7 +23,6 @@ "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.requested": "Awaiting approval", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account_note.placeholder": "Click to add a note", "column.pins": "Pinned toot", "community.column_settings.media_only": "Media only", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index ac0984293a..d44ac424f4 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -24,9 +24,7 @@ "account.follow_back": "பின்தொடரு", "account.followers": "பின்தொடர்பவர்கள்", "account.followers.empty": "இதுவரை யாரும் இந்த பயனரைப் பின்தொடரவில்லை.", - "account.followers_counter": "{count, plural, one {{counter} வாசகர்} other {{counter} வாசகர்கள்}}", "account.following": "பின்தொடரும்", - "account.following_counter": "{count, plural,one {{counter} சந்தா} other {{counter} சந்தாக்கள்}}", "account.follows.empty": "இந்த பயனர் இதுவரை யாரையும் பின்தொடரவில்லை.", "account.go_to_profile": "சுயவிவரத்திற்குச் செல்லவும்", "account.hide_reblogs": "இருந்து ஊக்கியாக மறை @{name}", @@ -45,7 +43,6 @@ "account.requested": "ஒப்புதலுக்காகக் காத்திருக்கிறது. பின்தொடரும் கோரிக்கையை நீக்க அழுத்தவும்", "account.share": "@{name} உடைய விவரத்தை பகிர்", "account.show_reblogs": "காட்டு boosts இருந்து @{name}", - "account.statuses_counter": "{count, plural, one {{counter} டூட்} other {{counter} டூட்டுகள்}}", "account.unblock": "@{name} மீது தடை நீக்குக", "account.unblock_domain": "{domain} ஐ காண்பி", "account.unblock_short": "தடையை நீக்கு", diff --git a/app/javascript/mastodon/locales/tai.json b/app/javascript/mastodon/locales/tai.json index 825cfb93bd..cad6e8eaa5 100644 --- a/app/javascript/mastodon/locales/tai.json +++ b/app/javascript/mastodon/locales/tai.json @@ -9,7 +9,6 @@ "account.posts": "Huah-siann", "account.posts_with_replies": "Huah-siann kah huê-ìng", "account.requested": "Tán-thāi phue-tsún", - "account.statuses_counter": "{count, plural, one {{counter} Huah-siann} other {{counter} Huah-siann}}", "account_note.placeholder": "Tiám tsi̍t-ē ka-thiam pī-tsù", "column.pins": "Tah thâu-tsîng ê huah-siann", "community.column_settings.media_only": "Kan-na muî-thé", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 284102c381..c06472561f 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -25,7 +25,6 @@ "account.requested": "ఆమోదం కోసం వేచి ఉంది. అభ్యర్థనను రద్దు చేయడానికి క్లిక్ చేయండి", "account.share": "@{name} యొక్క ప్రొఫైల్ను పంచుకోండి", "account.show_reblogs": "@{name}నుంచి బూస్ట్ లను చూపించు", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "@{name}పై బ్లాక్ ను తొలగించు", "account.unblock_domain": "{domain}ను దాచవద్దు", "account.unendorse": "ప్రొఫైల్లో చూపించవద్దు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index e16e393575..64abb394bf 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -415,6 +415,7 @@ "limited_account_hint.title": "มีการซ่อนโปรไฟล์นี้โดยผู้กลั่นกรองของ {domain}", "link_preview.author": "โดย {name}", "link_preview.more_from_author": "เพิ่มเติมจาก {name}", + "link_preview.shares": "{count, plural, other {{counter} โพสต์}}", "lists.account.add": "เพิ่มไปยังรายการ", "lists.account.remove": "เอาออกจากรายการ", "lists.delete": "ลบรายการ", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "ผู้คนที่ใช้เซิร์ฟเวอร์นี้ในระหว่าง 30 วันที่ผ่านมา (ผู้ใช้ที่ใช้งานอยู่รายเดือน)", "server_banner.active_users": "ผู้ใช้ที่ใช้งานอยู่", "server_banner.administered_by": "ดูแลโดย:", - "server_banner.introduction": "{domain} เป็นส่วนหนึ่งของเครือข่ายสังคมแบบกระจายศูนย์ที่ขับเคลื่อนโดย {mastodon}", - "server_banner.learn_more": "เรียนรู้เพิ่มเติม", + "server_banner.is_one_of_many": "{domain} เป็นหนึ่งในเซิร์ฟเวอร์ Mastodon อิสระจำนวนมากที่คุณสามารถใช้เพื่อมีส่วนร่วมในจักรวาลสหพันธ์", "server_banner.server_stats": "สถิติเซิร์ฟเวอร์:", "sign_in_banner.create_account": "สร้างบัญชี", + "sign_in_banner.follow_anyone": "ติดตามใครก็ตามทั่วทั้งจักรวาลสหพันธ์และดูจักรวาลสหพันธ์ทั้งหมดตามลำดับเวลา ไม่มีอัลกอริทึม, โฆษณา หรือคลิกเบตอยู่ในสายตา", + "sign_in_banner.mastodon_is": "Mastodon เป็นวิธีที่ดีที่สุดที่จะติดตามสิ่งที่กำลังเกิดขึ้น", "sign_in_banner.sign_in": "เข้าสู่ระบบ", "sign_in_banner.sso_redirect": "เข้าสู่ระบบหรือลงทะเบียน", - "sign_in_banner.text": "เข้าสู่ระบบเพื่อติดตามโปรไฟล์หรือแฮชแท็ก ชื่นชอบ แชร์ และตอบกลับโพสต์ คุณยังสามารถโต้ตอบจากบัญชีของคุณในเซิร์ฟเวอร์อื่น", "status.admin_account": "เปิดส่วนติดต่อการกลั่นกรองสำหรับ @{name}", "status.admin_domain": "เปิดส่วนติดต่อการกลั่นกรองสำหรับ {domain}", "status.admin_status": "เปิดโพสต์นี้ในส่วนติดต่อการกลั่นกรอง", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index c0f8d083ad..ac39a3fd7b 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -35,9 +35,9 @@ "account.follow_back": "Geri takip et", "account.followers": "Takipçi", "account.followers.empty": "Henüz kimse bu kullanıcıyı takip etmiyor.", - "account.followers_counter": "{count, plural, one {{counter} Takipçi} other {{counter} Takipçi}}", + "account.followers_counter": "{count, plural, one {{counter} takipçi} other {{counter} takipçi}}", "account.following": "Takip Ediliyor", - "account.following_counter": "{count, plural, one {{counter} Takip Edilen} other {{counter} Takip Edilen}}", + "account.following_counter": "{count, plural, one {{counter} takip edilen} other {{counter} takip edilen}}", "account.follows.empty": "Bu kullanıcı henüz kimseyi takip etmiyor.", "account.go_to_profile": "Profile git", "account.hide_reblogs": "@{name} kişisinin boostlarını gizle", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} size takip isteği gönderdi", "account.share": "@{name} adlı kişinin profilini paylaş", "account.show_reblogs": "@{name} kişisinin yeniden paylaşımlarını göster", - "account.statuses_counter": "{count, plural, one {{counter} Gönderi} other {{counter} Gönderi}}", + "account.statuses_counter": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}", "account.unblock": "@{name} adlı kişinin engelini kaldır", "account.unblock_domain": "{domain} alan adının engelini kaldır", "account.unblock_short": "Engeli kaldır", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Bu profil {domain} moderatörleri tarafından gizlendi.", "link_preview.author": "Yazar: {name}", "link_preview.more_from_author": "{name} kişisinden daha fazlası", + "link_preview.shares": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}", "lists.account.add": "Listeye ekle", "lists.account.remove": "Listeden kaldır", "lists.delete": "Listeyi sil", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Bu sunucuyu son 30 günde kullanan insanlar (Aylık Etkin Kullanıcılar)", "server_banner.active_users": "etkin kullanıcılar", "server_banner.administered_by": "Yönetici:", - "server_banner.introduction": "{domain}, {mastodon} destekli merkeziyetsiz sosyal ağın bir parçasıdır.", - "server_banner.learn_more": "Daha fazlasını öğrenin", + "server_banner.is_one_of_many": "{domain} fediverse katılımı için kullanabileceğiniz birçok bağımsız Mastodon sunucusundan biridir.", "server_banner.server_stats": "Sunucu istatistikleri:", "sign_in_banner.create_account": "Hesap oluştur", + "sign_in_banner.follow_anyone": "Fediverse çapında herhangi bir kimseyi takip edin ve tümünü kronolojik sırada görüntüleyin. Algoritma, reklam veya tıklama tuzağı yok.", + "sign_in_banner.mastodon_is": "Neler olup bittiğini izlemenin en iyi aracı Mastodon'dur.", "sign_in_banner.sign_in": "Giriş yap", "sign_in_banner.sso_redirect": "Giriş yap veya kaydol", - "sign_in_banner.text": "Profilleri ve hashtagleri takip etmek, gönderileri favorilerine eklemek, paylaşmak ve yanıtlamak için giriş yap. Farklı bir sunucudaki hesabınla da etkileşimde bulunabilirsin.", "status.admin_account": "@{name} için denetim arayüzünü açın", "status.admin_domain": "{domain} için denetim arayüzünü açın", "status.admin_status": "Denetim arayüzünde bu gönderiyi açın", diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json index 9a402472d9..baba3190dc 100644 --- a/app/javascript/mastodon/locales/tt.json +++ b/app/javascript/mastodon/locales/tt.json @@ -31,9 +31,7 @@ "account.follow": "Язылу", "account.followers": "Язылучы", "account.followers.empty": "Әле беркем дә язылмаган.", - "account.followers_counter": "{count, plural,one {{counter} язылучы} other {{counter} язылучы}}", "account.following": "Язылулар", - "account.following_counter": "{count, plural, one {{counter} язылу} other {{counter} язылу}}", "account.follows.empty": "Беркемгә дә язылмаган әле.", "account.go_to_profile": "Профильгә күчү", "account.hide_reblogs": "Скрывать көчен нче @{name}", @@ -55,7 +53,6 @@ "account.requested_follow": "{name} Сезгә язылу соравын җиберде", "account.share": "@{name} профиле белән уртаклашу", "account.show_reblogs": "Күрсәтергә көчәйтү нче @{name}", - "account.statuses_counter": "{count, plural, one {{counter} язма} other {{counter} язма}}", "account.unblock": "@{name} бикләвен чыгу", "account.unblock_domain": "{domain} бикләвен чыгу", "account.unblock_short": "Бикләүне чыгу", @@ -407,7 +404,6 @@ "search_results.statuses": "Язмалар", "search_results.title": "{q} өчен эзләү", "server_banner.administered_by": "Идарә итүче:", - "server_banner.learn_more": "Күбрәк белү", "server_banner.server_stats": "Сервер статистикасы:", "sign_in_banner.create_account": "Аккаунтны ясау", "sign_in_banner.sign_in": "Керү", diff --git a/app/javascript/mastodon/locales/ug.json b/app/javascript/mastodon/locales/ug.json index 4120d4483b..e3dd0e6b11 100644 --- a/app/javascript/mastodon/locales/ug.json +++ b/app/javascript/mastodon/locales/ug.json @@ -6,7 +6,6 @@ "account.posts": "Toots", "account.posts_with_replies": "Toots and replies", "account.requested": "Awaiting approval", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account_note.placeholder": "Click to add a note", "column.pins": "Pinned toot", "community.column_settings.media_only": "Media only", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index f750fce040..150b808f89 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -63,7 +63,7 @@ "account.requested_follow": "{name} надсилає запит на стеження", "account.share": "Поділитися профілем @{name}", "account.show_reblogs": "Показати поширення від @{name}", - "account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} дописи}}", + "account.statuses_counter": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}", "account.unblock": "Розблокувати @{name}", "account.unblock_domain": "Розблокувати {domain}", "account.unblock_short": "Розблокувати", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Цей профіль сховали модератори {domain}.", "link_preview.author": "Від {name}", "link_preview.more_from_author": "Більше від {name}", + "link_preview.shares": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}", "lists.account.add": "Додати до списку", "lists.account.remove": "Вилучити зі списку", "lists.delete": "Видалити список", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Люди, які використовують цей сервер протягом останніх 30 днів (Щомісячні Активні Користувачі)", "server_banner.active_users": "активні користувачі", "server_banner.administered_by": "Адміністратор:", - "server_banner.introduction": "{domain} є частиною децентралізованої соціальної мережі від {mastodon}.", - "server_banner.learn_more": "Дізнайтесь більше", + "server_banner.is_one_of_many": "{domain} - один з багатьох незалежних серверів Mastodon, які ви можете використати, щоб брати участь у федівері.", "server_banner.server_stats": "Статистика сервера:", "sign_in_banner.create_account": "Створити обліковий запис", + "sign_in_banner.follow_anyone": "Слідкуйте за ким завгодно у всьому fediverse і дивіться все це в хронологічному порядку. Немає алгоритмів, реклами чи наживок для натискань при перегляді.", + "sign_in_banner.mastodon_is": "Мастодон - найкращий спосіб продовжувати свою справу.", "sign_in_banner.sign_in": "Увійти", "sign_in_banner.sso_redirect": "Увійдіть або зареєструйтесь", - "sign_in_banner.text": "Увійдіть, щоб слідкувати за профілями або хештегами, вподобаними, ділитися і відповідати на дописи. Ви також можете взаємодіяти з вашого облікового запису на іншому сервері.", "status.admin_account": "Відкрити інтерфейс модерації для @{name}", "status.admin_domain": "Відкрити інтерфейс модерації для {domain}", "status.admin_status": "Відкрити цей допис в інтерфейсі модерації", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index 37f156c28f..cf53eb6fe8 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -26,11 +26,10 @@ "account.featured_tags.last_status_never": "کوئی مراسلہ نہیں", "account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز", "account.follow": "پیروی کریں", + "account.follow_back": "اکاؤنٹ کو فالو بیک ", "account.followers": "پیروکار", - "account.followers.empty": "\"ہنوز اس صارف کی کوئی پیروی نہیں کرتا\".", - "account.followers_counter": "{count, plural,one {{counter} پیروکار} other {{counter} پیروکار}}", + "account.followers.empty": "ہنوز اس صارف کی کوئی پیروی نہیں کرتا.", "account.following": "فالو کر رہے ہیں", - "account.following_counter": "{count, plural, one {{counter} پیروی کر رہے ہیں} other {{counter} پیروی کر رہے ہیں}}", "account.follows.empty": "\"یہ صارف ہنوز کسی کی پیروی نہیں کرتا ہے\".", "account.go_to_profile": "پروفائل پر جائیں", "account.hide_reblogs": "@{name} سے فروغ چھپائیں", @@ -46,6 +45,7 @@ "account.mute_notifications_short": "نوٹیفیکیشنز کو خاموش کریں", "account.mute_short": "خاموش", "account.muted": "خاموش کردہ", + "account.mutual": "میوچول اکاؤنٹ", "account.no_bio": "کوئی تفصیل نہیں دی گئی۔", "account.open_original_page": "اصل صفحہ کھولیں", "account.posts": "ٹوٹ", @@ -55,7 +55,6 @@ "account.requested_follow": "{name} آپ کو فالو کرنا چھاتا ہے۔", "account.share": "@{name} کے مشخص کو بانٹیں", "account.show_reblogs": "@{name} کی افزائشات کو دکھائیں", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unblock": "@{name} کو بحال کریں", "account.unblock_domain": "{domain} کو نہ چھپائیں", "account.unblock_short": "بلاک ختم کریں", @@ -64,7 +63,8 @@ "account.unmute": "@{name} کو با آواز کریں", "account.unmute_notifications_short": "نوٹیفیکیشنز کو خاموش نہ کریں", "account.unmute_short": "کو خاموش نہ کریں", - "account_note.placeholder": "Click to add a note", + "admin.dashboard.daily_retention": "ایڈمن ڈیش بورڈ کو ڈیلی چیک ان کریں", + "admin.dashboard.monthly_retention": "ایڈمن کیش بورڈ کو منتھلی چیک ان کریں", "admin.dashboard.retention.average": "اوسط", "admin.dashboard.retention.cohort_size": "نئے یسرز", "alert.rate_limited.message": "\"{retry_time, time, medium} کے بعد کوشش کریں\".", diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json index 77892914a4..4824b1d332 100644 --- a/app/javascript/mastodon/locales/uz.json +++ b/app/javascript/mastodon/locales/uz.json @@ -31,9 +31,7 @@ "account.follow": "Obuna bo‘lish", "account.followers": "Obunachilar", "account.followers.empty": "Bu foydalanuvchini hali hech kim kuzatmaydi.", - "account.followers_counter": "{count, plural, one {{counter} Muxlis} other {{counter} Muxlislar}}", "account.following": "Kuzatish", - "account.following_counter": "{count, plural, one {{counter} ga Muxlis} other {{counter} larga muxlis}}", "account.follows.empty": "Bu foydalanuvchi hali hech kimni kuzatmagan.", "account.go_to_profile": "Profilga o'tish", "account.hide_reblogs": "@{name} dan boostlarni yashirish", @@ -54,7 +52,6 @@ "account.requested_follow": "{name} sizni kuzatishni soʻradi", "account.share": "@{name} profilini ulashing", "account.show_reblogs": "@{name} dan bootlarni ko'rsatish", - "account.statuses_counter": "{count, plural, one {{counter} Post} other {{counter} Postlar}}", "account.unblock": "@{name} ni blokdan chiqarish", "account.unblock_domain": "{domain} domenini blokdan chiqarish", "account.unblock_short": "Blokdan chiqarish", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 56b2f7e52c..70932d10be 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -35,9 +35,9 @@ "account.follow_back": "Theo dõi lại", "account.followers": "Người theo dõi", "account.followers.empty": "Chưa có người theo dõi nào.", - "account.followers_counter": "{count, plural, one {{counter} Người theo dõi} other {{counter} Người theo dõi}}", + "account.followers_counter": "{count, plural, other {{counter} người theo dõi}}", "account.following": "Đang theo dõi", - "account.following_counter": "{count, plural, one {{counter} Theo dõi} other {{counter} Theo dõi}}", + "account.following_counter": "{count, plural, other {{counter} đang theo dõi}}", "account.follows.empty": "Người này chưa theo dõi ai.", "account.go_to_profile": "Xem hồ sơ", "account.hide_reblogs": "Ẩn tút @{name} đăng lại", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} yêu cầu theo dõi bạn", "account.share": "Chia sẻ @{name}", "account.show_reblogs": "Hiện tút do @{name} đăng lại", - "account.statuses_counter": "{count, plural, one {{counter} Tút} other {{counter} Tút}}", + "account.statuses_counter": "{count, plural, other {{counter} tút}}", "account.unblock": "Bỏ chặn @{name}", "account.unblock_domain": "Bỏ ẩn {domain}", "account.unblock_short": "Bỏ chặn", @@ -415,6 +415,7 @@ "limited_account_hint.title": "Người này đã bị ẩn bởi quản trị viên của {domain}.", "link_preview.author": "Bởi {name}", "link_preview.more_from_author": "Thêm từ {name}", + "link_preview.shares": "{count, plural, other {{counter} lượt chia sẻ}}", "lists.account.add": "Thêm vào danh sách", "lists.account.remove": "Xóa khỏi danh sách", "lists.delete": "Xóa danh sách", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "Những người ở máy chủ này trong 30 ngày qua (MAU)", "server_banner.active_users": "người hoạt động", "server_banner.administered_by": "Vận hành:", - "server_banner.introduction": "{domain} là một phần của mạng xã hội liên hợp {mastodon}.", - "server_banner.learn_more": "Tìm hiểu", + "server_banner.is_one_of_many": "{domain} là một trong nhiều máy chủ Mastodon độc lập mà bạn có thể sử dụng để tham gia vào Fediverse.", "server_banner.server_stats": "Thống kê:", "sign_in_banner.create_account": "Đăng ký", + "sign_in_banner.follow_anyone": "Theo dõi bất kỳ ai trên Fediverse và đọc tút theo thứ tự thời gian. Không thuật toán, quảng cáo hoặc clickbait.", + "sign_in_banner.mastodon_is": "Mastodon là cách tốt nhất để nắm bắt những gì đang xảy ra.", "sign_in_banner.sign_in": "Đăng nhập", "sign_in_banner.sso_redirect": "Đăng nhập", - "sign_in_banner.text": "Đăng nhập để theo dõi người hoặc hashtag, thích, chia sẻ và trả lời tút. Bạn cũng có thể tương tác từ tài khoản của mình trên một máy chủ khác.", "status.admin_account": "Mở giao diện quản trị @{name}", "status.admin_domain": "Mở giao diện quản trị @{domain}", "status.admin_status": "Mở tút này trong giao diện quản trị", diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json index 1d3a22108c..b42bb7589c 100644 --- a/app/javascript/mastodon/locales/zgh.json +++ b/app/javascript/mastodon/locales/zgh.json @@ -19,7 +19,6 @@ "account.posts_with_replies": "Toots and replies", "account.requested": "Awaiting approval", "account.share": "ⴱⴹⵓ ⵉⴼⵔⵙ ⵏ @{name}", - "account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}", "account.unfollow": "ⴽⴽⵙ ⴰⴹⴼⴼⵓⵕ", "account_note.placeholder": "Click to add a note", "bundle_column_error.retry": "ⴰⵍⵙ ⴰⵔⵎ", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 0f8bcae6f8..f2accae0d0 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -35,9 +35,9 @@ "account.follow_back": "回关", "account.followers": "关注者", "account.followers.empty": "目前无人关注此用户。", - "account.followers_counter": "被 {counter} 人关注", + "account.followers_counter": "{count, plural, other {{counter} 关注者}}", "account.following": "正在关注", - "account.following_counter": "正在关注 {counter} 人", + "account.following_counter": "{count, plural, other {{counter} 关注}}", "account.follows.empty": "此用户目前未关注任何人。", "account.go_to_profile": "前往个人资料页", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", @@ -63,7 +63,7 @@ "account.requested_follow": "{name} 已经向你发送了关注请求", "account.share": "分享 @{name} 的个人资料页", "account.show_reblogs": "显示来自 @{name} 的转嘟", - "account.statuses_counter": "{counter} 条嘟文", + "account.statuses_counter": "{count, plural, other {{counter} 嘟文}}", "account.unblock": "取消屏蔽 @{name}", "account.unblock_domain": "取消屏蔽 {domain} 域名", "account.unblock_short": "取消屏蔽", @@ -415,6 +415,7 @@ "limited_account_hint.title": "此账号资料已被 {domain} 管理员隐藏。", "link_preview.author": "由 {name}", "link_preview.more_from_author": "查看 {name} 的更多内容", + "link_preview.shares": "{count, plural, other {{counter} 条嘟文}}", "lists.account.add": "添加到列表", "lists.account.remove": "从列表中移除", "lists.delete": "删除列表", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "过去 30 天内使用此服务器的人(每月活跃用户)", "server_banner.active_users": "活跃用户", "server_banner.administered_by": "本站管理员:", - "server_banner.introduction": "{domain} 是由 {mastodon} 驱动的去中心化社交网络的一部分。", - "server_banner.learn_more": "详细了解", + "server_banner.is_one_of_many": "{domain} 是可用于参与联邦宇宙的众多独立 Mastodon 服务器之一。", "server_banner.server_stats": "服务器统计数据:", "sign_in_banner.create_account": "创建账户", + "sign_in_banner.follow_anyone": "关注联邦宇宙中的任何人,并按时间顺序查看所有内容。没有算法、广告或诱导链接。", + "sign_in_banner.mastodon_is": "Mastodon 是了解最新动态的最佳途径。", "sign_in_banner.sign_in": "登录", "sign_in_banner.sso_redirect": "登录或注册", - "sign_in_banner.text": "登录关注用户和话题标签,喜欢、分享和回复嘟文。您还可以与其他服务器上的用户进行互动。", "status.admin_account": "打开 @{name} 的管理界面", "status.admin_domain": "打开 {domain} 的管理界面", "status.admin_status": "打开此帖的管理界面", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index eaa5dabe93..09a497e889 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -35,9 +35,7 @@ "account.follow_back": "追蹤對方", "account.followers": "追蹤者", "account.followers.empty": "尚未有人追蹤這位使用者。", - "account.followers_counter": "有 {count, plural,one {{counter} 個} other {{counter} 個}}追蹤者", "account.following": "正在追蹤", - "account.following_counter": "正在追蹤 {count, plural,one {{counter}}other {{counter} 人}}", "account.follows.empty": "這位使用者尚未追蹤任何人。", "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏 @{name} 的轉推", @@ -63,7 +61,6 @@ "account.requested_follow": "{name} 要求追蹤你", "account.share": "分享 @{name} 的個人檔案", "account.show_reblogs": "顯示 @{name} 的轉推", - "account.statuses_counter": "{count, plural,one {{counter} 篇}other {{counter} 篇}}帖文", "account.unblock": "解除封鎖 @{name}", "account.unblock_domain": "解除封鎖網域 {domain}", "account.unblock_short": "解除封鎖", @@ -693,13 +690,10 @@ "server_banner.about_active_users": "在最近 30 天內內使用此伺服器的人 (月活躍用戶)", "server_banner.active_users": "活躍用戶", "server_banner.administered_by": "管理者:", - "server_banner.introduction": "{domain} 是由 {mastodon} 提供之去中心化社交網絡的一部份。", - "server_banner.learn_more": "了解更多", "server_banner.server_stats": "伺服器統計:", "sign_in_banner.create_account": "建立帳號", "sign_in_banner.sign_in": "登入", "sign_in_banner.sso_redirect": "登入或註冊", - "sign_in_banner.text": "登入以追蹤個人檔案、主題標籤,或最愛、分享和回覆帖文。你也可以從其他伺服器上的帳號進行互動。", "status.admin_account": "開啟 @{name} 的管理介面", "status.admin_domain": "打開 {domain} 管理介面", "status.admin_status": "在管理介面開啟這篇文章", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 1d20034db8..04469a971d 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -35,9 +35,9 @@ "account.follow_back": "跟隨回去", "account.followers": "跟隨者", "account.followers.empty": "尚未有人跟隨這位使用者。", - "account.followers_counter": "被 {count, plural, other {{counter} 人}}跟隨", + "account.followers_counter": "被 {count, plural, other {{count} 人}}跟隨", "account.following": "跟隨中", - "account.following_counter": "正在跟隨 {count,plural,other {{counter} 人}}", + "account.following_counter": "正在跟隨 {count,plural,other {{count} 人}}", "account.follows.empty": "這位使用者尚未跟隨任何人。", "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏來自 @{name} 的轉嘟", @@ -62,8 +62,8 @@ "account.requested": "正在等候審核。按一下以取消跟隨請求", "account.requested_follow": "{name} 要求跟隨您", "account.share": "分享 @{name} 的個人檔案", - "account.show_reblogs": "顯示來自 @{name} 的嘟文", - "account.statuses_counter": "{count, plural,one {{counter} 則}other {{counter} 則}}嘟文", + "account.show_reblogs": "顯示來自 @{name} 的轉嘟", + "account.statuses_counter": "{count, plural, other {{count} 則嘟文}}", "account.unblock": "解除封鎖 @{name}", "account.unblock_domain": "解除封鎖網域 {domain}", "account.unblock_short": "解除封鎖", @@ -415,6 +415,7 @@ "limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。", "link_preview.author": "來自 {name}", "link_preview.more_from_author": "來自 {name} 之更多內容", + "link_preview.shares": "{count, plural, other {{count} 則嘟文}}", "lists.account.add": "新增至列表", "lists.account.remove": "自列表中移除", "lists.delete": "刪除列表", @@ -695,13 +696,13 @@ "server_banner.about_active_users": "最近三十日內使用此伺服器的人(月活躍使用者)", "server_banner.active_users": "活躍使用者", "server_banner.administered_by": "管理者:", - "server_banner.introduction": "{domain} 是由 {mastodon} 提供之去中心化社群網路一部分。", - "server_banner.learn_more": "了解更多", + "server_banner.is_one_of_many": "{domain} 為許多獨立的 Mastodon 伺服器之一,您能透過該伺服器參與聯邦宇宙。", "server_banner.server_stats": "伺服器統計:", "sign_in_banner.create_account": "新增帳號", + "sign_in_banner.follow_anyone": "跟隨聯邦宇宙中的任何人,並且以時間順序瀏覽所有內容。沒有演算法、廣告、或騙點擊連結。", + "sign_in_banner.mastodon_is": "Mastodon 是跟上時代潮流的最佳工具!", "sign_in_banner.sign_in": "登入", "sign_in_banner.sso_redirect": "登入或註冊", - "sign_in_banner.text": "登入以跟隨個人檔案與主題標籤,或收藏、分享及回覆嘟文。您也可以使用您的帳號於其他伺服器進行互動。", "status.admin_account": "開啟 @{name} 的管理介面", "status.admin_domain": "開啟 {domain} 的管理介面", "status.admin_status": "於管理介面開啟此嘟文", diff --git a/app/javascript/mastodon/models/notification_policy.ts b/app/javascript/mastodon/models/notification_policy.ts new file mode 100644 index 0000000000..eb65403292 --- /dev/null +++ b/app/javascript/mastodon/models/notification_policy.ts @@ -0,0 +1,3 @@ +import type { NotificationPolicyJSON } from 'mastodon/api_types/notification_policies'; + +export type NotificationPolicy = NotificationPolicyJSON; // No changes from the API type diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts index 7907fc34f8..3900df4e38 100644 --- a/app/javascript/mastodon/models/status.ts +++ b/app/javascript/mastodon/models/status.ts @@ -1,4 +1,12 @@ +import type { RecordOf } from 'immutable'; + +import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses'; + export type { StatusVisibility } from 'mastodon/api_types/statuses'; // Temporary until we type it correctly export type Status = Immutable.Map; + +type CardShape = Required; + +export type Card = RecordOf; diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 97218e9f75..9f66c09631 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; + import { COMPOSE_MOUNT, COMPOSE_UNMOUNT, @@ -51,7 +53,6 @@ import { } from '../actions/compose'; import { REDRAFT } from '../actions/statuses'; import { STORE_HYDRATE } from '../actions/store'; -import { TIMELINE_DELETE } from '../actions/timelines'; import { me } from '../initial_state'; import { unescapeHTML } from '../utils/html'; import { uuid } from '../uuid'; @@ -446,10 +447,10 @@ export default function compose(state = initialState, action) { return updateSuggestionTags(state, action.token); case COMPOSE_TAG_HISTORY_UPDATE: return state.set('tagHistory', fromJS(action.tags)); - case TIMELINE_DELETE: - if (action.id === state.get('in_reply_to')) { + case timelineDelete.type: + if (action.payload.statusId === state.get('in_reply_to')) { return state.set('in_reply_to', null); - } else if (action.id === state.get('id')) { + } else if (action.payload.statusId === state.get('id')) { return state.set('id', null); } else { return state; diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index f7d7419a4e..b2c6f3f1ab 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -1,11 +1,13 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; + import { blockAccountSuccess, muteAccountSuccess, } from '../actions/accounts'; import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; -import { TIMELINE_DELETE, TIMELINE_UPDATE } from '../actions/timelines'; +import { TIMELINE_UPDATE } from '../actions/timelines'; import { compareId } from '../compare_id'; const initialState = ImmutableMap({ @@ -97,8 +99,8 @@ export default function replies(state = initialState, action) { return filterContexts(state, action.payload.relationship, action.payload.statuses); case CONTEXT_FETCH_SUCCESS: return normalizeContext(state, action.id, action.ancestors, action.descendants); - case TIMELINE_DELETE: - return deleteFromContexts(state, [action.id]); + case timelineDelete.type: + return deleteFromContexts(state, [action.payload.statusId]); case TIMELINE_UPDATE: return updateContext(state, action.status); default: diff --git a/app/javascript/mastodon/reducers/modal.ts b/app/javascript/mastodon/reducers/modal.ts index 368f26542c..ca85eb8c7f 100644 --- a/app/javascript/mastodon/reducers/modal.ts +++ b/app/javascript/mastodon/reducers/modal.ts @@ -1,10 +1,11 @@ import type { Reducer } from '@reduxjs/toolkit'; import { Record as ImmutableRecord, Stack } from 'immutable'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; + import { COMPOSE_UPLOAD_CHANGE_SUCCESS } from '../actions/compose'; import type { ModalType } from '../actions/modal'; import { openModal, closeModal } from '../actions/modal'; -import { TIMELINE_DELETE } from '../actions/timelines'; export type ModalProps = Record; interface Modal { @@ -72,10 +73,10 @@ export const modalReducer: Reducer = (state = initialState, action) => { // TODO: type those actions else if (action.type === COMPOSE_UPLOAD_CHANGE_SUCCESS) return popModal(state, { modalType: 'FOCAL_POINT', ignoreFocus: false }); - else if (action.type === TIMELINE_DELETE) + else if (timelineDelete.match(action)) return state.update('stack', (stack) => stack.filterNot( - (modal) => modal.get('modalProps').statusId === action.id, + (modal) => modal.get('modalProps').statusId === action.payload.statusId, ), ); else return state; diff --git a/app/javascript/mastodon/reducers/notification_policy.js b/app/javascript/mastodon/reducers/notification_policy.js deleted file mode 100644 index 8edb4d12a1..0000000000 --- a/app/javascript/mastodon/reducers/notification_policy.js +++ /dev/null @@ -1,12 +0,0 @@ -import { fromJS } from 'immutable'; - -import { NOTIFICATION_POLICY_FETCH_SUCCESS } from 'mastodon/actions/notifications'; - -export const notificationPolicyReducer = (state = null, action) => { - switch(action.type) { - case NOTIFICATION_POLICY_FETCH_SUCCESS: - return fromJS(action.policy); - default: - return state; - } -}; diff --git a/app/javascript/mastodon/reducers/notification_policy.ts b/app/javascript/mastodon/reducers/notification_policy.ts new file mode 100644 index 0000000000..ab111066cc --- /dev/null +++ b/app/javascript/mastodon/reducers/notification_policy.ts @@ -0,0 +1,18 @@ +import { createReducer, isAnyOf } from '@reduxjs/toolkit'; + +import { + fetchNotificationPolicy, + updateNotificationsPolicy, +} from 'mastodon/actions/notification_policies'; +import type { NotificationPolicy } from 'mastodon/models/notification_policy'; + +export const notificationPolicyReducer = + createReducer(null, (builder) => { + builder.addMatcher( + isAnyOf( + fetchNotificationPolicy.fulfilled, + updateNotificationsPolicy.fulfilled, + ), + (_state, action) => action.payload, + ); + }); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 64cddcb666..79aa5651ff 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -1,6 +1,7 @@ import { fromJS, Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { blockDomainSuccess } from 'mastodon/actions/domain_blocks'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; import { authorizeFollowRequestSuccess, @@ -30,7 +31,7 @@ import { NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, } from '../actions/notifications'; -import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines'; +import { disconnectTimeline } from '../actions/timelines'; import { compareId } from '../compare_id'; const initialState = ImmutableMap({ @@ -291,11 +292,11 @@ export default function notifications(state = initialState, action) { return filterNotifications(state, [action.payload.id], 'follow_request'); case NOTIFICATIONS_CLEAR: return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); - case TIMELINE_DELETE: - return deleteByStatus(state, action.id); - case TIMELINE_DISCONNECT: - return action.timeline === 'home' ? - state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : + case timelineDelete.type: + return deleteByStatus(state, action.payload.statusId); + case disconnectTimeline.type: + return action.payload.timeline === 'home' ? + state.update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; case NOTIFICATIONS_MARK_AS_READ: const lastNotification = state.get('items').find(item => item !== null); diff --git a/app/javascript/mastodon/reducers/picture_in_picture.ts b/app/javascript/mastodon/reducers/picture_in_picture.ts index 0feddcb706..10d4f1fae5 100644 --- a/app/javascript/mastodon/reducers/picture_in_picture.ts +++ b/app/javascript/mastodon/reducers/picture_in_picture.ts @@ -4,8 +4,7 @@ import { deployPictureInPictureAction, removePictureInPicture, } from 'mastodon/actions/picture_in_picture'; - -import { TIMELINE_DELETE } from '../actions/timelines'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; export interface PIPMediaProps { src: string; @@ -49,8 +48,9 @@ export const pictureInPictureReducer: Reducer = ( ...action.payload.props, }; else if (removePictureInPicture.match(action)) return initialState; - else if (action.type === TIMELINE_DELETE) - if (state.type && state.statusId === action.id) return initialState; + else if (timelineDelete.match(action)) + if (state.type && state.statusId === action.payload.statusId) + return initialState; return state; }; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 16bbd490c2..039272e44e 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; + import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { normalizeStatusTranslation } from '../actions/importer/normalizer'; import { @@ -32,7 +34,6 @@ import { STATUS_FETCH_REQUEST, STATUS_FETCH_FAIL, } from '../actions/statuses'; -import { TIMELINE_DELETE } from '../actions/timelines'; const importStatus = (state, status) => state.set(status.id, fromJS(status)); @@ -156,8 +157,8 @@ export default function statuses(state = initialState, action) { }); case STATUS_COLLAPSE: return state.setIn([action.id, 'collapsed'], action.isCollapsed); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references); + case timelineDelete.type: + return deleteStatus(state, action.payload.statusId, action.payload.references); case STATUS_TRANSLATE_SUCCESS: return statusTranslateSuccess(state, action.id, action.translation); case STATUS_TRANSLATE_UNDO: diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 4c9ab98a82..b07281ab87 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -1,5 +1,7 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; +import { timelineDelete } from 'mastodon/actions/timelines_typed'; + import { blockAccountSuccess, muteAccountSuccess, @@ -7,19 +9,18 @@ import { } from '../actions/accounts'; import { TIMELINE_UPDATE, - TIMELINE_DELETE, TIMELINE_CLEAR, TIMELINE_EXPAND_SUCCESS, TIMELINE_EXPAND_REQUEST, TIMELINE_EXPAND_FAIL, TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, - TIMELINE_DISCONNECT, TIMELINE_LOAD_PENDING, TIMELINE_MARK_AS_PARTIAL, TIMELINE_INSERT, TIMELINE_GAP, TIMELINE_SUGGESTIONS, + disconnectTimeline, } from '../actions/timelines'; import { compareId } from '../compare_id'; @@ -158,7 +159,7 @@ const filterTimelines = (state, relationship, statuses) => { return; } - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); + references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')).valueSeq().toJSON(); state = deleteStatus(state, status.get('id'), references, relationship.id); }); @@ -201,8 +202,8 @@ export default function timelines(state = initialState, action) { return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); - case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.references, action.reblogOf); + case timelineDelete.type: + return deleteStatus(state, action.payload.statusId, action.payload.references, action.payload.reblogOf); case TIMELINE_CLEAR: return clearTimeline(state, action.timeline); case blockAccountSuccess.type: @@ -214,11 +215,11 @@ export default function timelines(state = initialState, action) { return updateTop(state, action.timeline, action.top); case TIMELINE_CONNECT: return state.update(action.timeline, initialTimeline, map => reconnectTimeline(map, action.usePendingItems)); - case TIMELINE_DISCONNECT: + case disconnectTimeline.type: return state.update( - action.timeline, + action.payload.timeline, initialTimeline, - map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), + map => map.set('online', false).update(action.payload.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(TIMELINE_GAP) : items), ); case TIMELINE_MARK_AS_PARTIAL: return state.update( diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 2f17fed5fd..7a4c04c5c7 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -1,12 +1,8 @@ import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { - DIRECTORY_FETCH_REQUEST, - DIRECTORY_FETCH_SUCCESS, - DIRECTORY_FETCH_FAIL, - DIRECTORY_EXPAND_REQUEST, - DIRECTORY_EXPAND_SUCCESS, - DIRECTORY_EXPAND_FAIL, + expandDirectory, + fetchDirectory } from 'mastodon/actions/directory'; import { FEATURED_TAGS_FETCH_REQUEST, @@ -117,6 +113,7 @@ const normalizeFeaturedTags = (state, path, featuredTags, accountId) => { })); }; +/** @type {import('@reduxjs/toolkit').Reducer} */ export default function userLists(state = initialState, action) { switch(action.type) { case FOLLOWERS_FETCH_SUCCESS: @@ -194,16 +191,6 @@ export default function userLists(state = initialState, action) { case MUTES_FETCH_FAIL: case MUTES_EXPAND_FAIL: return state.setIn(['mutes', 'isLoading'], false); - case DIRECTORY_FETCH_SUCCESS: - return normalizeList(state, ['directory'], action.accounts, action.next); - case DIRECTORY_EXPAND_SUCCESS: - return appendToList(state, ['directory'], action.accounts, action.next); - case DIRECTORY_FETCH_REQUEST: - case DIRECTORY_EXPAND_REQUEST: - return state.setIn(['directory', 'isLoading'], true); - case DIRECTORY_FETCH_FAIL: - case DIRECTORY_EXPAND_FAIL: - return state.setIn(['directory', 'isLoading'], false); case FEATURED_TAGS_FETCH_SUCCESS: return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id); case FEATURED_TAGS_FETCH_REQUEST: @@ -211,6 +198,17 @@ export default function userLists(state = initialState, action) { case FEATURED_TAGS_FETCH_FAIL: return state.setIn(['featured_tags', action.id, 'isLoading'], false); default: - return state; + if(fetchDirectory.fulfilled.match(action)) + return normalizeList(state, ['directory'], action.payload.accounts, undefined); + else if( expandDirectory.fulfilled.match(action)) + return appendToList(state, ['directory'], action.payload.accounts, undefined); + else if(fetchDirectory.pending.match(action) || + expandDirectory.pending.match(action)) + return state.setIn(['directory', 'isLoading'], true); + else if(fetchDirectory.rejected.match(action) || + expandDirectory.rejected.match(action)) + return state.setIn(['directory', 'isLoading'], false); + else + return state; } } diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts index dae37e6225..e5820149db 100644 --- a/app/javascript/mastodon/store/typed_functions.ts +++ b/app/javascript/mastodon/store/typed_functions.ts @@ -82,13 +82,19 @@ export function createThunk( const discardLoadDataInPayload = Symbol('discardLoadDataInPayload'); type DiscardLoadData = typeof discardLoadDataInPayload; -type OnData = ( +type OnData = ( data: LoadDataResult, api: AppThunkApi & { + actionArg: ActionArg; discardLoadData: DiscardLoadData; }, ) => ReturnedData | DiscardLoadData | Promise; +type LoadData = ( + args: Args, + api: AppThunkApi, +) => Promise; + type ArgsType = Record | undefined; // Overload when there is no `onData` method, the payload is the `onData` result @@ -101,18 +107,18 @@ export function createDataLoadingThunk( // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty export function createDataLoadingThunk( name: string, - loadData: (args: Args) => Promise, + loadData: LoadData, onDataOrThunkOptions?: | AppThunkOptions - | OnData, + | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; // Overload when the `onData` method returns nothing, then the mayload is the `onData` result export function createDataLoadingThunk( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; @@ -123,8 +129,10 @@ export function createDataLoadingThunk< Returned, >( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: + | AppThunkOptions + | OnData, thunkOptions?: AppThunkOptions, ): ReturnType>; @@ -159,11 +167,13 @@ export function createDataLoadingThunk< Returned, >( name: string, - loadData: (args: Args) => Promise, - onDataOrThunkOptions?: AppThunkOptions | OnData, + loadData: LoadData, + onDataOrThunkOptions?: + | AppThunkOptions + | OnData, maybeThunkOptions?: AppThunkOptions, ) { - let onData: OnData | undefined; + let onData: OnData | undefined; let thunkOptions: AppThunkOptions | undefined; if (typeof onDataOrThunkOptions === 'function') onData = onDataOrThunkOptions; @@ -177,7 +187,10 @@ export function createDataLoadingThunk< return createThunk( name, async (arg, { getState, dispatch }) => { - const data = await loadData(arg); + const data = await loadData(arg, { + dispatch, + getState, + }); if (!onData) return data as Returned; @@ -185,6 +198,7 @@ export function createDataLoadingThunk< dispatch, getState, discardLoadData: discardLoadDataInPayload, + actionArg: arg, }); // if there is no return in `onData`, we return the `onData` result diff --git a/app/javascript/mastodon/test_helpers.tsx b/app/javascript/mastodon/test_helpers.tsx index 93b5a8453a..f405090730 100644 --- a/app/javascript/mastodon/test_helpers.tsx +++ b/app/javascript/mastodon/test_helpers.tsx @@ -2,6 +2,7 @@ import { IntlProvider } from 'react-intl'; import { MemoryRouter } from 'react-router'; +import type { RenderOptions } from '@testing-library/react'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as rtlRender } from '@testing-library/react'; @@ -9,7 +10,11 @@ import { IdentityContext } from './identity_context'; function render( ui: React.ReactElement, - { locale = 'en', signedIn = true, ...renderOptions } = {}, + { + locale = 'en', + signedIn = true, + ...renderOptions + }: RenderOptions & { locale?: string; signedIn?: boolean } = {}, ) { const fakeIdentity = { signedIn: signedIn, diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss index 09a75a834b..9f571b3f26 100644 --- a/app/javascript/styles/mastodon-light/variables.scss +++ b/app/javascript/styles/mastodon-light/variables.scss @@ -56,11 +56,13 @@ $account-background-color: $white !default; $emojis-requiring-inversion: 'chains'; -.theme-mastodon-light { +body { --dropdown-border-color: #d9e1e8; --dropdown-background-color: #fff; + --modal-border-color: #d9e1e8; + --modal-background-color: var(--background-color-tint); --background-border-color: #d9e1e8; --background-color: #fff; - --background-color-tint: rgba(255, 255, 255, 90%); + --background-color-tint: rgba(255, 255, 255, 80%); --background-filter: blur(10px); } diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index b3bce309de..1a7d70e610 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -120,8 +120,27 @@ text-decoration: none; } - &:disabled { - opacity: 0.5; + &.button--destructive { + &:active, + &:focus, + &:hover { + border-color: $ui-button-destructive-focus-background-color; + color: $ui-button-destructive-focus-background-color; + } + } + + &:disabled, + &.disabled { + opacity: 0.7; + border-color: $ui-primary-color; + color: $ui-primary-color; + + &:active, + &:focus, + &:hover { + border-color: $ui-primary-color; + color: $ui-primary-color; + } } } @@ -903,9 +922,15 @@ body > [data-popper-placement] { padding: 10px; p { + font-size: 15px; + line-height: 22px; color: $darker-text-color; margin-bottom: 20px; + strong { + font-weight: 700; + } + a { color: $secondary-text-color; text-decoration: none; @@ -1412,10 +1437,15 @@ body > [data-popper-placement] { .audio-player, .attachment-list, .picture-in-picture-placeholder, + .more-from-author, .status-card, .hashtag-bar { margin-inline-start: $thread-margin; - width: calc(100% - ($thread-margin)); + width: calc(100% - $thread-margin); + } + + .more-from-author { + width: calc(100% - $thread-margin + 2px); } .status__content__read-more-button { @@ -2424,7 +2454,7 @@ a.account__display-name { } .dropdown-animation { - animation: dropdown 150ms cubic-bezier(0.1, 0.7, 0.1, 1); + animation: dropdown 250ms cubic-bezier(0.1, 0.7, 0.1, 1); @keyframes dropdown { from { @@ -4144,6 +4174,13 @@ a.status-card { border-end-start-radius: 0; } +.status-card.bottomless .status-card__image, +.status-card.bottomless .status-card__image-image, +.status-card.bottomless .status-card__image-preview { + border-end-end-radius: 0; + border-end-start-radius: 0; +} + .status-card.expanded > a { width: 100%; } @@ -8803,43 +8840,80 @@ noscript { display: flex; align-items: center; color: $primary-text-color; - text-decoration: none; - padding: 15px; + padding: 16px; border-bottom: 1px solid var(--background-border-color); - gap: 15px; + gap: 16px; &:last-child { border-bottom: 0; } - &:hover, - &:active, - &:focus { - color: $highlight-text-color; - - .story__details__publisher, - .story__details__shared { - color: $highlight-text-color; - } - } - &__details { flex: 1 1 auto; &__publisher { color: $darker-text-color; margin-bottom: 8px; + font-size: 14px; + line-height: 20px; } &__title { + display: block; font-size: 19px; line-height: 24px; font-weight: 500; margin-bottom: 8px; + text-decoration: none; + color: $primary-text-color; + + &:hover, + &:active, + &:focus { + color: $highlight-text-color; + } } &__shared { + display: flex; + align-items: center; color: $darker-text-color; + gap: 8px; + justify-content: space-between; + font-size: 14px; + line-height: 20px; + + & > span { + display: flex; + align-items: center; + gap: 4px; + } + + &__pill { + background: var(--surface-variant-background-color); + border-radius: 4px; + color: inherit; + text-decoration: none; + padding: 4px 12px; + font-size: 12px; + font-weight: 500; + line-height: 16px; + } + + &__author-link { + display: inline-flex; + align-items: center; + gap: 4px; + color: $primary-text-color; + font-weight: 500; + text-decoration: none; + + &:hover, + &:active, + &:focus { + color: $highlight-text-color; + } + } } strong { @@ -8904,14 +8978,14 @@ noscript { } .server-banner { - padding: 20px 0; - &__introduction { + font-size: 15px; + line-height: 22px; color: $darker-text-color; margin-bottom: 20px; strong { - font-weight: 600; + font-weight: 700; } a { @@ -8939,6 +9013,9 @@ noscript { } &__description { + font-size: 15px; + line-height: 22px; + color: $darker-text-color; margin-bottom: 20px; } @@ -9910,14 +9987,14 @@ noscript { color: inherit; text-decoration: none; padding: 4px 12px; - background: $ui-base-color; + background: var(--surface-variant-background-color); border-radius: 4px; font-weight: 500; &:hover, &:focus, &:active { - background: lighten($ui-base-color, 4%); + background: var(--surface-variant-active-background-color); } } @@ -10248,6 +10325,7 @@ noscript { } .more-from-author { + box-sizing: border-box; font-size: 14px; color: $darker-text-color; background: var(--surface-background-color); @@ -10285,3 +10363,158 @@ noscript { } } } + +.hover-card-controller[data-popper-reference-hidden='true'] { + opacity: 0; + pointer-events: none; +} + +.hover-card { + box-shadow: var(--dropdown-shadow); + background: var(--modal-background-color); + backdrop-filter: var(--background-filter); + border: 1px solid var(--modal-border-color); + border-radius: 8px; + padding: 16px; + width: 270px; + display: flex; + flex-direction: column; + gap: 12px; + + &--loading { + position: relative; + min-height: 100px; + } + + &__name { + display: flex; + gap: 12px; + text-decoration: none; + color: inherit; + } + + &__number { + font-size: 15px; + line-height: 22px; + color: $secondary-text-color; + + strong { + font-weight: 700; + } + } + + &__text-row { + display: flex; + flex-direction: column; + gap: 8px; + } + + &__bio { + color: $secondary-text-color; + font-size: 14px; + line-height: 20px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + max-height: 2 * 20px; + overflow: hidden; + + p { + margin-bottom: 0; + } + + a { + color: inherit; + text-decoration: underline; + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } + } + + .display-name { + font-size: 15px; + line-height: 22px; + + bdi { + font-weight: 500; + color: $primary-text-color; + } + + &__account { + display: block; + color: $dark-text-color; + } + } + + .account-fields { + color: $secondary-text-color; + font-size: 14px; + line-height: 20px; + + a { + color: inherit; + text-decoration: none; + + &:focus, + &:hover, + &:active { + text-decoration: underline; + } + } + + dl { + display: flex; + align-items: center; + gap: 4px; + + dt { + flex: 0 0 auto; + color: $dark-text-color; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + dd { + flex: 1 1 auto; + font-weight: 500; + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + text-align: end; + } + + &.verified { + dd { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + overflow: hidden; + white-space: nowrap; + color: $valid-value-color; + + & > span { + overflow: hidden; + text-overflow: ellipsis; + } + + a { + font-weight: 500; + } + + .icon { + width: 16px; + height: 16px; + } + } + } + } + } +} diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f6ec44fb53..26bb2bee14 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -613,9 +613,10 @@ code { font-family: inherit; pointer-events: none; cursor: default; - max-width: 140px; + max-width: 50%; white-space: nowrap; overflow: hidden; + text-overflow: ellipsis; &::after { content: ''; diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index 7a4fbf960a..1059fc5437 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -106,4 +106,6 @@ $font-monospace: 'mastodon-font-monospace' !default; --background-color: #{darken($ui-base-color, 8%)}; --background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)}; --surface-background-color: #{darken($ui-base-color, 4%)}; + --surface-variant-background-color: #{$ui-base-color}; + --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)}; } diff --git a/app/lib/access_grant_extension.rb b/app/lib/access_grant_extension.rb new file mode 100644 index 0000000000..bf8f5ae25a --- /dev/null +++ b/app/lib/access_grant_extension.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module AccessGrantExtension + extend ActiveSupport::Concern + + included do + scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') } + scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) } + end +end diff --git a/app/lib/access_token_extension.rb b/app/lib/access_token_extension.rb index 4e9585dd1e..6e06f988a5 100644 --- a/app/lib/access_token_extension.rb +++ b/app/lib/access_token_extension.rb @@ -9,6 +9,10 @@ module AccessTokenExtension has_many :web_push_subscriptions, class_name: 'Web::PushSubscription', inverse_of: :access_token after_commit :push_to_streaming_api + + scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') } + scope :not_revoked, -> { where(revoked_at: nil) } + scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) } end def revoke(clock = Time) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index fbba95157d..c384505232 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -104,7 +104,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def find_existing_status status = status_from_uri(object_uri) status ||= Status.find_by(uri: @object['atomUri']) if @object['atomUri'].present? - status + status if status&.account_id == @account.id end def process_status_params diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index 9dd0d393f9..a260a66e2a 100644 --- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb +++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb @@ -10,7 +10,7 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim protected def perform_query - [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version].compact + [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, imagemagick_version, ffmpeg_version].compact end def mastodon_version @@ -28,8 +28,8 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim { key: 'ruby', human_key: 'Ruby', - value: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}", - human_value: RUBY_DESCRIPTION, + value: RUBY_DESCRIPTION, + human_value: "#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}", } end @@ -82,6 +82,34 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim } end + def imagemagick_version + return if Rails.configuration.x.use_vips + + version = `convert -version`.match(/Version: ImageMagick ([\d\.]+)/)[1] + + { + key: 'imagemagick', + human_key: 'ImageMagick', + value: version, + human_value: version, + } + rescue Errno::ENOENT + nil + end + + def ffmpeg_version + version = `ffmpeg -version`.match(/ffmpeg version ([\d\.]+)/)[1] + + { + key: 'ffmpeg', + human_key: 'FFmpeg', + value: version, + human_value: version, + } + rescue Errno::ENOENT + nil + end + def redis_info @redis_info ||= if redis.is_a?(Redis::Namespace) redis.redis.info diff --git a/app/lib/admin/metrics/measure/active_users_measure.rb b/app/lib/admin/metrics/measure/active_users_measure.rb index e6f09d4bcf..c085ced629 100644 --- a/app/lib/admin/metrics/measure/active_users_measure.rb +++ b/app/lib/admin/metrics/measure/active_users_measure.rb @@ -22,12 +22,4 @@ class Admin::Metrics::Measure::ActiveUsersMeasure < Admin::Metrics::Measure::Bas def activity_tracker @activity_tracker ||= ActivityTracker.new('activity:logins', :unique) end - - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end end diff --git a/app/lib/admin/metrics/measure/base_measure.rb b/app/lib/admin/metrics/measure/base_measure.rb index e33a6c494f..8b7fe39b55 100644 --- a/app/lib/admin/metrics/measure/base_measure.rb +++ b/app/lib/admin/metrics/measure/base_measure.rb @@ -86,11 +86,11 @@ class Admin::Metrics::Measure::BaseMeasure end def time_period - (@start_at..@end_at) + (@start_at.to_date..@end_at.to_date) end def previous_time_period - ((@start_at - length_of_period)..(@end_at - length_of_period)) + ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) end def length_of_period diff --git a/app/lib/admin/metrics/measure/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb index 3d081fdd90..889a5e6f05 100644 --- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb @@ -43,19 +43,11 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure SELECT count(*) FROM new_accounts ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb index 378c6754d9..fa934c6b96 100644 --- a/app/lib/admin/metrics/measure/instance_followers_measure.rb +++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb @@ -44,19 +44,11 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur SELECT count(*) FROM new_followers ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb index e213348fbc..3f3ab73fc9 100644 --- a/app/lib/admin/metrics/measure/instance_follows_measure.rb +++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb @@ -44,19 +44,11 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure: SELECT count(*) FROM new_follows ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb index 1d2dbbe414..996ca52e0b 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -53,19 +53,11 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics: SELECT COALESCE(SUM(size), 0) FROM new_media_attachments ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb index 9da3d53e34..ae1bb6e68d 100644 --- a/app/lib/admin/metrics/measure/instance_reports_measure.rb +++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb @@ -44,19 +44,11 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure: SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index b918a30a57..324d427b18 100644 --- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb +++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb @@ -45,7 +45,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure SELECT count(*) FROM new_statuses ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end @@ -58,14 +58,6 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure Mastodon::Snowflake.id_at(@end_at.end_of_day, with_random: false) end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:domain, :include_subdomains) end diff --git a/app/lib/admin/metrics/measure/interactions_measure.rb b/app/lib/admin/metrics/measure/interactions_measure.rb index 7a2b7e0fac..f4b4836b4a 100644 --- a/app/lib/admin/metrics/measure/interactions_measure.rb +++ b/app/lib/admin/metrics/measure/interactions_measure.rb @@ -22,12 +22,4 @@ class Admin::Metrics::Measure::InteractionsMeasure < Admin::Metrics::Measure::Ba def activity_tracker @activity_tracker ||= ActivityTracker.new('activity:interactions', :basic) end - - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end end diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb index 6837c14c82..32057154d6 100644 --- a/app/lib/admin/metrics/measure/new_users_measure.rb +++ b/app/lib/admin/metrics/measure/new_users_measure.rb @@ -32,7 +32,7 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe SELECT count(*) FROM new_users ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end diff --git a/app/lib/admin/metrics/measure/opened_reports_measure.rb b/app/lib/admin/metrics/measure/opened_reports_measure.rb index c395c46341..47de38bbe6 100644 --- a/app/lib/admin/metrics/measure/opened_reports_measure.rb +++ b/app/lib/admin/metrics/measure/opened_reports_measure.rb @@ -32,7 +32,7 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B SELECT count(*) FROM new_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end diff --git a/app/lib/admin/metrics/measure/query_helper.rb b/app/lib/admin/metrics/measure/query_helper.rb index 969065f73f..47cfc63e5c 100644 --- a/app/lib/admin/metrics/measure/query_helper.rb +++ b/app/lib/admin/metrics/measure/query_helper.rb @@ -15,6 +15,14 @@ module Admin::Metrics::Measure::QueryHelper ActiveRecord::Base.sanitize_sql_array(sql_array) end + def generated_series_days + Arel.sql( + <<~SQL.squish + SELECT generate_series(:start_at::timestamp, :end_at::timestamp, '1 day')::date AS period + SQL + ) + end + def account_domain_sql(include_subdomains) if include_subdomains "accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || :domain::text))" diff --git a/app/lib/admin/metrics/measure/resolved_reports_measure.rb b/app/lib/admin/metrics/measure/resolved_reports_measure.rb index 780db75a10..ecfd779c86 100644 --- a/app/lib/admin/metrics/measure/resolved_reports_measure.rb +++ b/app/lib/admin/metrics/measure/resolved_reports_measure.rb @@ -32,7 +32,7 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure: SELECT count(*) FROM resolved_reports ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) AS axis SQL end diff --git a/app/lib/admin/metrics/measure/tag_accounts_measure.rb b/app/lib/admin/metrics/measure/tag_accounts_measure.rb index 8f4512efe7..906277b7d5 100644 --- a/app/lib/admin/metrics/measure/tag_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/tag_accounts_measure.rb @@ -27,14 +27,6 @@ class Admin::Metrics::Measure::TagAccountsMeasure < Admin::Metrics::Measure::Bas @tag ||= Tag.find(params[:id]) end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:id) end diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index f273d739d0..5db1076062 100644 --- a/app/lib/admin/metrics/measure/tag_servers_measure.rb +++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb @@ -40,7 +40,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base SELECT COUNT(*) FROM tag_servers ) AS value FROM ( - SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period + #{generated_series_days} ) as axis SQL end diff --git a/app/lib/admin/metrics/measure/tag_uses_measure.rb b/app/lib/admin/metrics/measure/tag_uses_measure.rb index bce86b89f1..2be96a02b3 100644 --- a/app/lib/admin/metrics/measure/tag_uses_measure.rb +++ b/app/lib/admin/metrics/measure/tag_uses_measure.rb @@ -27,14 +27,6 @@ class Admin::Metrics::Measure::TagUsesMeasure < Admin::Metrics::Measure::BaseMea @tag ||= Tag.find(params[:id]) end - def time_period - (@start_at.to_date..@end_at.to_date) - end - - def previous_time_period - ((@start_at.to_date - length_of_period)..(@end_at.to_date - length_of_period)) - end - def params @params.permit(:id) end diff --git a/app/lib/application_extension.rb b/app/lib/application_extension.rb index 2fea1057cb..d7aaeba5bd 100644 --- a/app/lib/application_extension.rb +++ b/app/lib/application_extension.rb @@ -16,7 +16,7 @@ module ApplicationExtension # dependent: delete_all, which means the ActiveRecord callback in # AccessTokenExtension is not run, so instead we manually announce to # streaming that these tokens are being deleted. - before_destroy :push_to_streaming_api, prepend: true + before_destroy :close_streaming_sessions, prepend: true end def confirmation_redirect_uri @@ -29,10 +29,12 @@ module ApplicationExtension redirect_uri.split end - def push_to_streaming_api + def close_streaming_sessions(resource_owner = nil) # TODO: #28793 Combine into a single topic payload = Oj.dump(event: :kill) - access_tokens.in_batches do |tokens| + scope = access_tokens + scope = scope.where(resource_owner_id: resource_owner.id) unless resource_owner.nil? + scope.in_batches do |tokens| redis.pipelined do |pipeline| tokens.ids.each do |id| pipeline.publish("timeline:access_token:#{id}", payload) diff --git a/app/lib/cache_buster.rb b/app/lib/cache_buster.rb index 554f2ba95d..d3395f8f0a 100644 --- a/app/lib/cache_buster.rb +++ b/app/lib/cache_buster.rb @@ -2,13 +2,8 @@ class CacheBuster def initialize(options = {}) - Rails.application.deprecators[:mastodon].warn('Default values for the cache buster secret header name and values will be removed in Mastodon 4.3. Please set them explicitely if you rely on those.') unless options[:http_method] || (options[:secret] && options[:secret_header]) - - @secret_header = options[:secret_header] || - (options[:http_method] ? nil : 'Secret-Header') - @secret = options[:secret] || - (options[:http_method] ? nil : 'True') - + @secret_header = options[:secret_header] + @secret = options[:secret] @http_method = options[:http_method] || 'GET' end diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb index 9090773ae9..7e647a7587 100644 --- a/app/lib/extractor.rb +++ b/app/lib/extractor.rb @@ -86,10 +86,6 @@ module Extractor possible_entries end - def extract_cashtags_with_indices(_text) - [] - end - def extract_extra_uris_with_indices(text) return [] unless text&.index(':') diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index e4801b53a9..5a1710c52d 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -470,10 +470,7 @@ class FeedManager check_for_blocks = status.active_mentions.pluck(:account_id) check_for_blocks.push(status.in_reply_to_account) if status.reply? && !status.in_reply_to_account_id.nil? - should_filter = blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) - should_filter ||= status.account.silenced? && !Follow.exists?(account_id: receiver_id, target_account_id: status.account_id) # Filter if the account is silenced and I'm not following them - - should_filter + blocks_or_mutes?(receiver_id, check_for_blocks, :mentions) # Filter if it's from someone I blocked, in reply to someone I blocked, or mentioning someone I blocked (or muted) end # Check if status should not be added to the linear direct message feed diff --git a/app/lib/link_details_extractor.rb b/app/lib/link_details_extractor.rb index 2e49d3fb4f..dbfdd33fcc 100644 --- a/app/lib/link_details_extractor.rb +++ b/app/lib/link_details_extractor.rb @@ -269,16 +269,21 @@ class LinkDetailsExtractor end def document - @document ||= Nokogiri::HTML(@html, nil, encoding) + @document ||= detect_encoding_and_parse_document end - def encoding - @encoding ||= begin - guess = detector.detect(@html, @html_charset) - guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil + def detect_encoding_and_parse_document + [detect_encoding, nil, @html_charset, 'UTF-8'].uniq.each do |encoding| + document = Nokogiri::HTML(@html, nil, encoding) + return document if document.to_s.valid_encoding? end end + def detect_encoding + guess = detector.detect(@html, @html_charset) + guess&.fetch(:confidence, 0).to_i > 60 ? guess&.fetch(:encoding, nil) : nil + end + def detector @detector ||= CharlockHolmes::EncodingDetector.new.tap do |detector| detector.strip_tags = true diff --git a/app/lib/private_address_check.rb b/app/lib/private_address_check.rb new file mode 100644 index 0000000000..d00b16e66b --- /dev/null +++ b/app/lib/private_address_check.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module PrivateAddressCheck + module_function + + CIDR_LIST = [ + IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address) + IPAddr.new('100.64.0.0/10'), # Shared Address Space + IPAddr.new('172.16.0.0/12'), # Private network + IPAddr.new('192.0.0.0/24'), # IETF Protocol Assignments + IPAddr.new('192.0.2.0/24'), # TEST-NET-1, documentation and examples + IPAddr.new('192.88.99.0/24'), # IPv6 to IPv4 relay (includes 2002::/16) + IPAddr.new('198.18.0.0/15'), # Network benchmark tests + IPAddr.new('198.51.100.0/24'), # TEST-NET-2, documentation and examples + IPAddr.new('203.0.113.0/24'), # TEST-NET-3, documentation and examples + IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network) + IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network) + IPAddr.new('255.255.255.255'), # Broadcast + IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052) + IPAddr.new('100::/64'), # Discard prefix (RFC 6666) + IPAddr.new('2001::/32'), # Teredo tunneling + IPAddr.new('2001:10::/28'), # Deprecated (previously ORCHID) + IPAddr.new('2001:20::/28'), # ORCHIDv2 + IPAddr.new('2001:db8::/32'), # Addresses used in documentation and example source code + IPAddr.new('2002::/16'), # 6to4 + IPAddr.new('fc00::/7'), # Unique local address + IPAddr.new('ff00::/8'), # Multicast + ].freeze + + def private_address?(address) + address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) } + end +end diff --git a/app/lib/redis_configuration.rb b/app/lib/redis_configuration.rb index f0e86d985b..fb1249640f 100644 --- a/app/lib/redis_configuration.rb +++ b/app/lib/redis_configuration.rb @@ -42,9 +42,13 @@ class RedisConfiguration ENV['REDIS_URL'] end + def redis_driver + ENV.fetch('REDIS_DRIVER', 'hiredis') == 'ruby' ? :ruby : :hiredis + end + private def raw_connection - Redis.new(url: url, driver: :hiredis) + Redis.new(url: url, driver: redis_driver) end end diff --git a/app/lib/search_query_transformer.rb b/app/lib/search_query_transformer.rb index 927495eace..606819ed40 100644 --- a/app/lib/search_query_transformer.rb +++ b/app/lib/search_query_transformer.rb @@ -225,7 +225,7 @@ class SearchQueryTransformer < Parslet::Transform end rule(clause: subtree(:clause)) do - prefix = clause[:prefix][:term].to_s if clause[:prefix] + prefix = clause[:prefix][:term].to_s.downcase if clause[:prefix] operator = clause[:operator]&.to_s term = clause[:phrase] ? clause[:phrase].map { |term| term[:term].to_s }.join(' ') : clause[:term].to_s diff --git a/app/lib/themes.rb b/app/lib/themes.rb index a87323d8e9..64c2f1bfe3 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -8,7 +8,7 @@ class Themes THEME_COLORS = { dark: '#191b22', - light: '#f3f5f7', + light: '#ffffff', }.freeze def initialize diff --git a/app/lib/vacuum/access_tokens_vacuum.rb b/app/lib/vacuum/access_tokens_vacuum.rb index a224f6d638..281ae22bf0 100644 --- a/app/lib/vacuum/access_tokens_vacuum.rb +++ b/app/lib/vacuum/access_tokens_vacuum.rb @@ -9,12 +9,12 @@ class Vacuum::AccessTokensVacuum private def vacuum_revoked_access_tokens! - Doorkeeper::AccessToken.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all - Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all + Doorkeeper::AccessToken.expired.in_batches.delete_all + Doorkeeper::AccessToken.revoked.in_batches.delete_all end def vacuum_revoked_access_grants! - Doorkeeper::AccessGrant.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all - Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all + Doorkeeper::AccessGrant.expired.in_batches.delete_all + Doorkeeper::AccessGrant.revoked.in_batches.delete_all end end diff --git a/app/models/notification.rb b/app/models/notification.rb index 05c91bf3d6..6560194155 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -160,6 +160,7 @@ class Notification < ApplicationRecord .limit(1), query .joins('CROSS JOIN grouped_notifications') + .where('array_length(grouped_notifications.groups, 1) < :limit', limit: limit) .where('notifications.id < grouped_notifications.id') .where.not("COALESCE(notifications.group_key, 'ungrouped-' || notifications.id) = ANY(grouped_notifications.groups)") .select('notifications.*', "array_append(grouped_notifications.groups, COALESCE(notifications.group_key, 'ungrouped-' || notifications.id))") @@ -187,6 +188,7 @@ class Notification < ApplicationRecord .limit(1), query .joins('CROSS JOIN grouped_notifications') + .where('array_length(grouped_notifications.groups, 1) < :limit', limit: limit) .where('notifications.id > grouped_notifications.id') .where.not("COALESCE(notifications.group_key, 'ungrouped-' || notifications.id) = ANY(grouped_notifications.groups)") .select('notifications.*', "array_append(grouped_notifications.groups, COALESCE(notifications.group_key, 'ungrouped-' || notifications.id))") diff --git a/app/models/notification_group.rb b/app/models/notification_group.rb index 07967f9dcb..b1cbd7c19a 100644 --- a/app/models/notification_group.rb +++ b/app/models/notification_group.rb @@ -1,14 +1,20 @@ # frozen_string_literal: true class NotificationGroup < ActiveModelSerializers::Model - attributes :group_key, :sample_accounts, :notifications_count, :notification + attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id - def self.from_notification(notification) + def self.from_notification(notification, max_id: nil) if notification.group_key.present? # TODO: caching and preloading - sample_accounts = notification.account.notifications.where(group_key: notification.group_key).order(id: :desc).limit(3).map(&:from_account) - notifications_count = notification.account.notifications.where(group_key: notification.group_key).count + scope = notification.account.notifications.where(group_key: notification.group_key) + scope = scope.where(id: ..max_id) if max_id.present? + + most_recent_notifications = scope.order(id: :desc).take(3) + most_recent_id = most_recent_notifications.first.id + sample_accounts = most_recent_notifications.map(&:from_account) + notifications_count = scope.count else + most_recent_id = notification.id sample_accounts = [notification.from_account] notifications_count = 1 end @@ -17,7 +23,8 @@ class NotificationGroup < ActiveModelSerializers::Model notification: notification, group_key: notification.group_key || "ungrouped-#{notification.id}", sample_accounts: sample_accounts, - notifications_count: notifications_count + notifications_count: notifications_count, + most_recent_notification_id: most_recent_id ) end @@ -25,5 +32,6 @@ class NotificationGroup < ActiveModelSerializers::Model :target_status, :report, :account_relationship_severance_event, + :account_warning, to: :notification, prefix: false end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index cbfc393786..eac02ac14f 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -128,6 +128,22 @@ class PreviewCard < ApplicationRecord @history ||= Trends::History.new('links', id) end + def authors + @authors ||= [PreviewCard::Author.new(self)] + end + + class Author < ActiveModelSerializers::Model + attributes :name, :url, :account + + def initialize(preview_card) + super( + name: preview_card.author_name, + url: preview_card.author_url, + account: preview_card.author_account, + ) + end + end + class << self private diff --git a/app/models/site_upload.rb b/app/models/site_upload.rb index 6431d1007d..273dd6de9f 100644 --- a/app/models/site_upload.rb +++ b/app/models/site_upload.rb @@ -31,17 +31,10 @@ class SiteUpload < ApplicationRecord [:"#{size}", { format: 'png', geometry: "#{size}x#{size}#", file_geometry_parser: FastGeometryParser }] end.freeze, - favicon: { - ico: { - format: 'ico', - geometry: '48x48#', - file_geometry_parser: FastGeometryParser, - }.freeze, - }.merge( + favicon: FAVICON_SIZES.to_h do |size| [:"#{size}", { format: 'png', geometry: "#{size}x#{size}#", file_geometry_parser: FastGeometryParser }] - end - ).freeze, + end.freeze, thumbnail: { '@1x': { diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index c6f282d00f..e5d7cb46ea 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -43,7 +43,7 @@ class StatusEdit < ApplicationRecord scope :ordered, -> { order(id: :asc) } delegate :local?, :application, :edited?, :edited_at, - :discarded?, :visibility, to: :status + :discarded?, :visibility, :language, to: :status def emojis return @emojis if defined?(@emojis) diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index b482ad3afe..ddfd08146e 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -75,7 +75,7 @@ class Web::PushSubscription < ApplicationRecord class << self def unsubscribe_for(application_id, resource_owner) - access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil).pluck(:id) + access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id).not_revoked.pluck(:id) where(access_token_id: access_token_ids).delete_all end end diff --git a/app/models/webauthn_credential.rb b/app/models/webauthn_credential.rb index 4fa31ece52..d7ed1b9d40 100644 --- a/app/models/webauthn_credential.rb +++ b/app/models/webauthn_credential.rb @@ -15,9 +15,11 @@ # class WebauthnCredential < ApplicationRecord + SIGN_COUNT_LIMIT = (2**63) + validates :external_id, :public_key, :nickname, :sign_count, presence: true validates :external_id, uniqueness: true validates :nickname, uniqueness: { scope: :user_id } validates :sign_count, - numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: (2**63) - 1 } + numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: SIGN_COUNT_LIMIT - 1 } end diff --git a/app/serializers/activitypub/outbox_serializer.rb b/app/serializers/activitypub/outbox_serializer.rb index 4f4f950a5a..4d3d9706de 100644 --- a/app/serializers/activitypub/outbox_serializer.rb +++ b/app/serializers/activitypub/outbox_serializer.rb @@ -2,7 +2,7 @@ class ActivityPub::OutboxSerializer < ActivityPub::CollectionSerializer def self.serializer_for(model, options) - if model.class.name == 'ActivityPub::ActivityPresenter' + if model.instance_of?(::ActivityPub::ActivityPresenter) ActivityPub::ActivitySerializer else super diff --git a/app/serializers/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb index 05b51b07a5..9aa5663f4e 100644 --- a/app/serializers/rest/notification_group_serializer.rb +++ b/app/serializers/rest/notification_group_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::NotificationGroupSerializer < ActiveModel::Serializer - attributes :group_key, :notifications_count, :type + attributes :group_key, :notifications_count, :type, :most_recent_notification_id attribute :page_min_id, if: :paginated? attribute :page_max_id, if: :paginated? @@ -45,6 +45,6 @@ class REST::NotificationGroupSerializer < ActiveModel::Serializer end def paginated? - instance_options[:group_metadata].present? + !instance_options[:group_metadata].nil? end end diff --git a/app/serializers/rest/notification_policy_serializer.rb b/app/serializers/rest/notification_policy_serializer.rb index a50ba9e66b..8bf85250fa 100644 --- a/app/serializers/rest/notification_policy_serializer.rb +++ b/app/serializers/rest/notification_policy_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class REST::NotificationPolicySerializer < ActiveModel::Serializer + # Please update `app/javascript/mastodon/api_types/notification_policies.ts` when making changes to the attributes + attributes :filter_not_following, :filter_not_followers, :filter_new_accounts, diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 01a789eb14..1d867421bb 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::NotificationSerializer < ActiveModel::Serializer - attributes :id, :type, :created_at + attributes :id, :type, :created_at, :group_key belongs_to :from_account, key: :account, serializer: REST::AccountSerializer belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer @@ -13,6 +13,10 @@ class REST::NotificationSerializer < ActiveModel::Serializer object.id.to_s end + def group_key + object.group_key || "ungrouped-#{object.id}" + end + def status_type? [:favourite, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type) end diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb index 7d4c99c2d1..f73a051ac0 100644 --- a/app/serializers/rest/preview_card_serializer.rb +++ b/app/serializers/rest/preview_card_serializer.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true class REST::PreviewCardSerializer < ActiveModel::Serializer + class AuthorSerializer < ActiveModel::Serializer + attributes :name, :url + has_one :account, serializer: REST::AccountSerializer + end + include RoutingHelper attributes :url, :title, :description, :language, :type, @@ -8,7 +13,7 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer :provider_url, :html, :width, :height, :image, :image_description, :embed_url, :blurhash, :published_at - has_one :author_account, serializer: REST::AccountSerializer, if: -> { object.author_account.present? } + has_many :authors, serializer: AuthorSerializer def url object.original_url.presence || object.url diff --git a/app/services/account_search_service.rb b/app/services/account_search_service.rb index b86c9b9e7e..dab5f748bf 100644 --- a/app/services/account_search_service.rb +++ b/app/services/account_search_service.rb @@ -28,9 +28,7 @@ class AccountSearchService < BaseService }, functions: [ - reputation_score_function, followers_score_function, - time_distance_function, ], }, }, @@ -81,36 +79,12 @@ class AccountSearchService < BaseService } end - # This function deranks accounts that follow more people than follow them - def reputation_score_function - { - script_score: { - script: { - source: "(Math.max(doc['followers_count'].value, 0) + 0.0) / (Math.max(doc['followers_count'].value, 0) + Math.max(doc['following_count'].value, 0) + 1)", - }, - }, - } - end - # This function promotes accounts that have more followers def followers_score_function { script_score: { script: { - source: "(Math.max(doc['followers_count'].value, 0) / (Math.max(doc['followers_count'].value, 0) + 1))", - }, - }, - } - end - - # This function deranks accounts that haven't posted in a long time - def time_distance_function - { - gauss: { - last_status_at: { - scale: '30d', - offset: '30d', - decay: 0.3, + source: "Math.log10((Math.max(doc['followers_count'].value, 0) + 1))", }, }, } @@ -126,10 +100,24 @@ class AccountSearchService < BaseService def core_query { - multi_match: { - query: @query, - type: 'bool_prefix', - fields: %w(username^2 username.*^2 display_name display_name.*), + dis_max: { + queries: [ + { + multi_match: { + query: @query, + type: 'most_fields', + fields: %w(username username.*), + }, + }, + + { + multi_match: { + query: @query, + type: 'most_fields', + fields: %w(display_name display_name.*), + }, + }, + ], }, } end @@ -142,7 +130,7 @@ class AccountSearchService < BaseService { multi_match: { query: @query, - type: 'most_fields', + type: 'best_fields', fields: %w(username^2 display_name^2 text text.*), operator: 'and', }, diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 4552fc0977..94be214eb2 100644 --- a/app/services/backup_service.rb +++ b/app/services/backup_service.rb @@ -19,8 +19,8 @@ class BackupService < BaseService def build_outbox_json!(file) skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer) - skeleton[:@context] = full_context - skeleton[:orderedItems] = ['!PLACEHOLDER!'] + skeleton['@context'] = full_context + skeleton['orderedItems'] = ['!PLACEHOLDER!'] skeleton = Oj.dump(skeleton) prepend, append = skeleton.split('"!PLACEHOLDER!"') add_comma = false diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 900cb9863d..8bc9f912c5 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -15,6 +15,9 @@ class FetchLinkCardService < BaseService ) }iox + # URL size limit to safely store in PosgreSQL's unique indexes + BYTESIZE_LIMIT = 2692 + def call(status) @status = status @original_url = parse_urls @@ -29,7 +32,7 @@ class FetchLinkCardService < BaseService end attach_card if @card&.persisted? - rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError => e + rescue HTTP::Error, OpenSSL::SSL::SSLError, Addressable::URI::InvalidURIError, Mastodon::HostValidationError, Mastodon::LengthValidationError, Encoding::UndefinedConversionError => e Rails.logger.debug { "Error fetching link #{@original_url}: #{e}" } nil end @@ -85,7 +88,7 @@ class FetchLinkCardService < BaseService def bad_url?(uri) # Avoid local instance URLs and invalid URLs - uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) + uri.host.blank? || TagManager.instance.local_url?(uri.to_s) || !%w(http https).include?(uri.scheme) || uri.to_s.bytesize > BYTESIZE_LIMIT end def mention_link?(anchor) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 01b072938d..9dd4f0e4e4 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -1,38 +1,41 @@ - content_for :page_title do = t('admin.accounts.title') -= form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do += form_with url: admin_accounts_url, method: :get, class: :simple_form do |form| .filters .filter-subset.filter-subset--with-select %strong= t('admin.accounts.location.title') .input.select.optional - = select_tag :origin, - options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), - prompt: I18n.t('generic.all') + = form.select :origin, + options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), + prompt: I18n.t('generic.all') .filter-subset.filter-subset--with-select %strong= t('admin.accounts.moderation.title') .input.select.optional - = select_tag :status, - options_for_select(admin_accounts_moderation_options, params[:status]), - prompt: I18n.t('generic.all') + = form.select :status, + options_for_select(admin_accounts_moderation_options, params[:status]), + prompt: I18n.t('generic.all') .filter-subset.filter-subset--with-select %strong= t('admin.accounts.role') .input.select.optional - = select_tag :role_ids, - options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), - prompt: I18n.t('admin.accounts.moderation.all') + = form.select :role_ids, + options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), + prompt: I18n.t('admin.accounts.moderation.all') .filter-subset.filter-subset--with-select %strong= t 'generic.order_by' .input.select - = select_tag :order, - options_for_select([[t('relationships.most_recent'), 'recent'], [t('relationships.last_active'), 'active']], params[:order]) + = form.select :order, + options_for_select([[t('relationships.most_recent'), 'recent'], [t('relationships.last_active'), 'active']], params[:order]) .fields-group - %i(username by_domain display_name email ip).each do |key| - next if key == :by_domain && params[:origin] != 'remote' .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}") + = form.text_field key, + value: params[key], + class: 'string optional', + placeholder: I18n.t("admin.accounts.#{key}") .actions %button.button= t('admin.accounts.search') @@ -40,7 +43,7 @@ %hr.spacer/ -= form_for(@form, url: batch_admin_accounts_path) do |f| += form_with model: @form, url: batch_admin_accounts_path do |f| = hidden_field_tag :page, params[:page] || 1 = hidden_field_tag :select_all_matching, '0' diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml index c4929cc422..c02c8f0ad4 100644 --- a/app/views/admin/action_logs/index.html.haml +++ b/app/views/admin/action_logs/index.html.haml @@ -1,19 +1,23 @@ - content_for :page_title do = t('admin.action_logs.title') -= form_tag admin_action_logs_url, method: 'GET', class: 'simple_form' do += form_with url: admin_action_logs_url, method: :get, class: :simple_form do |form| = hidden_field_tag :target_account_id, params[:target_account_id] if params[:target_account_id].present? .filters .filter-subset.filter-subset--with-select %strong= t('admin.action_logs.filter_by_user') .input.select.optional - = select_tag :account_id, options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all') + = form.select :account_id, + options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), + prompt: I18n.t('admin.accounts.moderation.all') .filter-subset.filter-subset--with-select %strong= t('admin.action_logs.filter_by_action') .input.select.optional - = select_tag :action_type, options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all') + = form.select :action_type, + options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }, params[:action_type]), + prompt: I18n.t('admin.accounts.moderation.all') - if @action_logs.empty? .muted-hint.center-text diff --git a/app/views/admin/custom_emojis/index.html.haml b/app/views/admin/custom_emojis/index.html.haml index e87dd41282..82fec554b0 100644 --- a/app/views/admin/custom_emojis/index.html.haml +++ b/app/views/admin/custom_emojis/index.html.haml @@ -21,20 +21,23 @@ - else = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil -= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do += form_with url: admin_custom_emojis_url, method: :get, class: :simple_form do |form| .fields-group - CustomEmojiFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? - %i(shortcode by_domain).each do |key| .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}") + = form.text_field key, + value: params[key], + class: 'string optional', + placeholder: I18n.t("admin.custom_emojis.#{key}") .actions %button.button= t('admin.accounts.search') = link_to t('admin.accounts.reset'), admin_custom_emojis_path, class: 'button negative' -= form_for(@form, url: batch_admin_custom_emojis_path) do |f| += form_with model: @form, url: batch_admin_custom_emojis_path do |f| = hidden_field_tag :page, params[:page] || 1 - CustomEmojiFilter::KEYS.each do |key| diff --git a/app/views/admin/email_domain_blocks/index.html.haml b/app/views/admin/email_domain_blocks/index.html.haml index 684735c207..4fae6557a5 100644 --- a/app/views/admin/email_domain_blocks/index.html.haml +++ b/app/views/admin/email_domain_blocks/index.html.haml @@ -4,7 +4,7 @@ - content_for :heading_actions do = link_to t('admin.email_domain_blocks.add_new'), new_admin_email_domain_block_path, class: 'button' -= form_for(@form, url: batch_admin_email_domain_blocks_path) do |f| += form_with model: @form, url: batch_admin_email_domain_blocks_path do |f| = hidden_field_tag :page, params[:page] || 1 .batch-table diff --git a/app/views/admin/export_domain_blocks/import.html.haml b/app/views/admin/export_domain_blocks/import.html.haml index 52ffc3d465..2b0d2c5eb3 100644 --- a/app/views/admin/export_domain_blocks/import.html.haml +++ b/app/views/admin/export_domain_blocks/import.html.haml @@ -6,7 +6,7 @@ - if defined?(@global_private_comment) && @global_private_comment.present? %p= t('admin.export_domain_blocks.import.private_comment_description_html', comment: @global_private_comment) -= form_for(@form, url: batch_admin_domain_blocks_path) do |f| += form_with model: @form, url: batch_admin_domain_blocks_path do |f| .batch-table .batch-table__toolbar %label.batch-table__toolbar__select.batch-checkbox-all diff --git a/app/views/admin/follow_recommendations/show.html.haml b/app/views/admin/follow_recommendations/show.html.haml index c8ad653a88..62cd315725 100644 --- a/app/views/admin/follow_recommendations/show.html.haml +++ b/app/views/admin/follow_recommendations/show.html.haml @@ -5,23 +5,23 @@ %hr.spacer/ -= form_tag admin_follow_recommendations_path, method: 'GET', class: 'simple_form' do += form_with url: admin_follow_recommendations_path, method: :get, class: :simple_form do |form| - RelationshipFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? .filters .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :language, - options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, @language) + = form.select :language, + options_for_select(Trends.available_locales.map { |key| [standard_locale_name(key), key] }, @language) .filter-subset %strong= t('admin.follow_recommendations.status') %ul %li= filter_link_to t('admin.accounts.moderation.active'), status: nil %li= filter_link_to t('admin.follow_recommendations.suppressed'), status: 'suppressed' -= form_for(@form, url: admin_follow_recommendations_path, method: :patch) do |f| += form_with model: @form, url: admin_follow_recommendations_path, method: :patch do |f| - RelationshipFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 7e43b4c538..b5f084f880 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -28,14 +28,17 @@ %li= filter_link_to t('admin.instances.delivery.unavailable'), availability: 'unavailable' - unless limited_federation_mode? - = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do + = form_with url: admin_instances_url, method: :get, class: :simple_form do |form| .fields-group - InstanceFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? - %i(by_domain).each do |key| .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}") + = form.text_field key, + value: params[key], + class: 'string optional', + placeholder: I18n.t("admin.instances.#{key}") .actions %button.button= t('admin.accounts.search') diff --git a/app/views/admin/ip_blocks/index.html.haml b/app/views/admin/ip_blocks/index.html.haml index 9eba6c68ff..207d23aeeb 100644 --- a/app/views/admin/ip_blocks/index.html.haml +++ b/app/views/admin/ip_blocks/index.html.haml @@ -5,7 +5,7 @@ - content_for :heading_actions do = link_to t('admin.ip_blocks.add_new'), new_admin_ip_block_path, class: 'button' -= form_for(@form, url: batch_admin_ip_blocks_path) do |f| += form_with model: @form, url: batch_admin_ip_blocks_path do |f| = hidden_field_tag :page, params[:page] || 1 .batch-table diff --git a/app/views/admin/relationships/index.html.haml b/app/views/admin/relationships/index.html.haml index c2daefb424..83ffd139de 100644 --- a/app/views/admin/relationships/index.html.haml +++ b/app/views/admin/relationships/index.html.haml @@ -24,7 +24,7 @@ %hr.spacer/ -= form_for(@form, url: batch_admin_accounts_path) do |f| += form_with model: @form, url: batch_admin_accounts_path do |f| .batch-table .batch-table__toolbar %label.batch-table__toolbar__select.batch-checkbox-all diff --git a/app/views/admin/reports/_actions.html.haml b/app/views/admin/reports/_actions.html.haml index da9ac89315..5fb540931b 100644 --- a/app/views/admin/reports/_actions.html.haml +++ b/app/views/admin/reports/_actions.html.haml @@ -1,4 +1,4 @@ -= form_tag preview_admin_report_actions_path(report), method: :post do += form_with url: preview_admin_report_actions_path(report) do |form| .report-actions .report-actions__item .report-actions__item__button @@ -8,26 +8,36 @@ - if statuses.any? { |status| (status.with_media? || status.with_preview_card?) && !status.discarded? } .report-actions__item .report-actions__item__button - = button_tag t('admin.reports.mark_as_sensitive'), name: :mark_as_sensitive, class: 'button' + = form.button t('admin.reports.mark_as_sensitive'), + name: :mark_as_sensitive, + class: 'button' .report-actions__item__description = t('admin.reports.actions.mark_as_sensitive_description_html') .report-actions__item .report-actions__item__button - = button_tag t('admin.reports.delete_and_resolve'), name: :delete, class: 'button button--destructive' + = form.button t('admin.reports.delete_and_resolve'), + name: :delete, + class: 'button button--destructive' .report-actions__item__description = t('admin.reports.actions.delete_description_html') .report-actions__item .report-actions__item__button - = button_tag t('admin.accounts.silence'), name: :silence, class: 'button button--destructive' + = form.button t('admin.accounts.silence'), + name: :silence, + class: 'button button--destructive' .report-actions__item__description = t('admin.reports.actions.silence_description_html') .report-actions__item .report-actions__item__button - = button_tag t('admin.accounts.suspend'), name: :suspend, class: 'button button--destructive' + = form.button t('admin.accounts.suspend'), + name: :suspend, + class: 'button button--destructive' .report-actions__item__description = t('admin.reports.actions.suspend_description_html') .report-actions__item .report-actions__item__button - = link_to t('admin.accounts.custom'), new_admin_account_action_path(report.target_account_id, report_id: report.id), class: 'button' + = link_to t('admin.accounts.custom'), + new_admin_account_action_path(report.target_account_id, report_id: report.id), + class: 'button' .report-actions__item__description = t('admin.reports.actions.other_description_html') diff --git a/app/views/admin/reports/actions/preview.html.haml b/app/views/admin/reports/actions/preview.html.haml index 7a737d4f72..79c444453f 100644 --- a/app/views/admin/reports/actions/preview.html.haml +++ b/app/views/admin/reports/actions/preview.html.haml @@ -4,8 +4,8 @@ - content_for :page_title do = t('admin.reports.confirm_action', acct: target_acct) -= form_tag admin_report_actions_path(@report), class: 'simple_form', method: :post do - = hidden_field_tag :moderation_action, @moderation_action += form_with url: admin_report_actions_path(@report), class: :simple_form do |form| + = form.hidden_field :moderation_action, value: @moderation_action %p.hint= t("admin.reports.summary.action_preambles.#{@moderation_action}_html", acct: target_acct) %ul.hint @@ -30,7 +30,9 @@ %p= t "user_mailer.warning.explanation.#{warning_action}", instance: Rails.configuration.x.local_domain .fields-group - = text_area_tag :text, nil, placeholder: t('admin.reports.summary.warning_placeholder') + = form.text_area :text, + value: nil, + placeholder: t('admin.reports.summary.warning_placeholder') - unless @report.other? %p @@ -75,4 +77,7 @@ .actions = link_to t('admin.reports.cancel'), admin_report_path(@report), class: 'button button-tertiary' - = button_tag t('admin.reports.confirm'), name: :confirm, class: 'button', type: :submit + = form.button t('admin.reports.confirm'), + name: :confirm, + class: 'button', + type: :submit diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index e2a9868aa5..dae2c1aa5b 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -14,14 +14,17 @@ %li= filter_link_to t('admin.accounts.location.local'), target_origin: 'local' %li= filter_link_to t('admin.accounts.location.remote'), target_origin: 'remote' -= form_tag admin_reports_url, method: 'GET', class: 'simple_form' do += form_with url: admin_reports_url, method: :get, class: :simple_form do |form| .fields-group - ReportFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? - %i(by_target_domain).each do |key| .input.string.optional - = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}") + = form.text_field key, + value: params[key], + class: 'string optional', + placeholder: I18n.t("admin.reports.#{key}") .actions %button.button= t('admin.accounts.search') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 842aa51597..ca1edea0fe 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -45,7 +45,7 @@ admin_account_statuses_path(@report.target_account_id, report_id: @report.id), class: 'table-action-link' -= form_for(@form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id)) do |f| += form_with model: @form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id) do |f| .batch-table .batch-table__toolbar %label.batch-table__toolbar__select.batch-checkbox-all diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index a41a6332dd..b03b8ac51b 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -8,7 +8,7 @@ %strong= t('admin.statuses.media.title') %ul %li= filter_link_to t('generic.all'), media: nil, id: nil - %li= filter_link_to t('admin.statuses.with_media'), media: '1' + %li= filter_link_to t('admin.statuses.with_media'), media: true .back-link - if params[:report_id] = link_to admin_report_path(params[:report_id].to_i) do @@ -21,7 +21,7 @@ %hr.spacer/ -= form_for(@status_batch_action, url: batch_admin_account_statuses_path(@account.id)) do |f| += form_with model: @status_batch_action, url: batch_admin_account_statuses_path(@account.id) do |f| = hidden_field_tag :page, params[:page] || 1 - Admin::StatusFilter::KEYS.each do |key| diff --git a/app/views/admin/trends/links/index.html.haml b/app/views/admin/trends/links/index.html.haml index c503b2d396..647c24b1e9 100644 --- a/app/views/admin/trends/links/index.html.haml +++ b/app/views/admin/trends/links/index.html.haml @@ -5,17 +5,17 @@ %hr.spacer/ -= form_tag admin_trends_links_path, method: 'GET', class: 'simple_form' do += form_with url: admin_trends_links_path, method: :get, class: :simple_form do |form| - Trends::PreviewCardFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? .filters .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :locale, - options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), - include_blank: true + = form.select :locale, + options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), + include_blank: true .filter-subset %strong= t('admin.trends.trending') %ul @@ -26,7 +26,7 @@ = t('admin.trends.preview_card_providers.title') = material_symbol 'chevron_right' -= form_for(@form, url: batch_admin_trends_links_path) do |f| += form_with model: @form, url: batch_admin_trends_links_path do |f| = hidden_field_tag :page, params[:page] || 1 - Trends::PreviewCardFilter::KEYS.each do |key| diff --git a/app/views/admin/trends/links/preview_card_providers/index.html.haml b/app/views/admin/trends/links/preview_card_providers/index.html.haml index 706c607010..b43b8dfff9 100644 --- a/app/views/admin/trends/links/preview_card_providers/index.html.haml +++ b/app/views/admin/trends/links/preview_card_providers/index.html.haml @@ -20,7 +20,7 @@ %hr.spacer/ -= form_for(@form, url: batch_admin_trends_links_preview_card_providers_path) do |f| += form_with model: @form, url: batch_admin_trends_links_preview_card_providers_path do |f| = hidden_field_tag :page, params[:page] || 1 - Trends::PreviewCardProviderFilter::KEYS.each do |key| diff --git a/app/views/admin/trends/statuses/index.html.haml b/app/views/admin/trends/statuses/index.html.haml index 66151ad31e..4713f8c2ae 100644 --- a/app/views/admin/trends/statuses/index.html.haml +++ b/app/views/admin/trends/statuses/index.html.haml @@ -5,22 +5,24 @@ %hr.spacer/ -= form_tag admin_trends_statuses_path, method: 'GET', class: 'simple_form' do += form_with url: admin_trends_statuses_path, method: :get, class: :simple_form do |form| - Trends::StatusFilter::KEYS.each do |key| - = hidden_field_tag key, params[key] if params[key].present? + = form.hidden_field key, value: params[key] if params[key].present? .filters .filter-subset.filter-subset--with-select %strong= t('admin.follow_recommendations.language') .input.select.optional - = select_tag :locale, options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), include_blank: true + = form.select :locale, + options_for_select(@locales.map { |key| [standard_locale_name(key), key] }, params[:locale]), + include_blank: true .filter-subset %strong= t('admin.trends.trending') %ul %li= filter_link_to t('generic.all'), trending: nil %li= filter_link_to t('admin.trends.only_allowed'), trending: 'allowed' -= form_for(@form, url: batch_admin_trends_statuses_path) do |f| += form_with model: @form, url: batch_admin_trends_statuses_path do |f| = hidden_field_tag :page, params[:page] || 1 - Trends::StatusFilter::KEYS.each do |key| diff --git a/app/views/admin/trends/tags/index.html.haml b/app/views/admin/trends/tags/index.html.haml index 655955f7f6..3a44cf3a70 100644 --- a/app/views/admin/trends/tags/index.html.haml +++ b/app/views/admin/trends/tags/index.html.haml @@ -14,7 +14,7 @@ %li= filter_link_to t('admin.trends.rejected'), status: 'rejected' %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{Tag.pending_review.count})"], ' '), status: 'pending_review' -= form_for(@form, url: batch_admin_trends_tags_path) do |f| += form_with model: @form, url: batch_admin_trends_tags_path do |f| = hidden_field_tag :page, params[:page] || 1 - Trends::TagFilter::KEYS.each do |key| diff --git a/app/views/auth/confirmations/captcha.html.haml b/app/views/auth/confirmations/captcha.html.haml index 964d0e63e7..035ac3a86a 100644 --- a/app/views/auth/confirmations/captcha.html.haml +++ b/app/views/auth/confirmations/captcha.html.haml @@ -1,11 +1,13 @@ - content_for :page_title do = t('auth.captcha_confirmation.title') -= form_tag auth_captcha_confirmation_url, method: 'POST', class: 'simple_form' do += form_with url: auth_captcha_confirmation_url, class: :simple_form do |form| = render 'auth/shared/progress', stage: 'confirm' - = hidden_field_tag :confirmation_token, params[:confirmation_token] - = hidden_field_tag :redirect_to_app, params[:redirect_to_app] + = form.hidden_field :confirmation_token, + value: params[:confirmation_token] + = form.hidden_field :redirect_to_app, + value: params[:redirect_to_app] %h1.title= t('auth.captcha_confirmation.title') %p.lead= t('auth.captcha_confirmation.hint_html') @@ -15,4 +17,6 @@ %p.lead= t('auth.captcha_confirmation.help_html', email: mail_to(Setting.site_contact_email, nil)) .actions - = button_tag t('challenge.confirm'), class: 'button', type: :submit + = form.button t('challenge.confirm'), + class: 'button', + type: :submit diff --git a/app/views/filters/statuses/index.html.haml b/app/views/filters/statuses/index.html.haml index eaa39e170f..915ec59caf 100644 --- a/app/views/filters/statuses/index.html.haml +++ b/app/views/filters/statuses/index.html.haml @@ -13,7 +13,7 @@ %hr.spacer/ -= form_for(@status_filter_batch_action, url: batch_filter_statuses_path(@filter.id)) do |f| += form_with model: @status_filter_batch_action, url: batch_filter_statuses_path(@filter.id) do |f| = hidden_field_tag :page, params[:page] || 1 - Admin::StatusFilter::KEYS.each do |key| diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 36f55bdd97..f022c3f516 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -11,15 +11,14 @@ - if storage_host? %link{ rel: 'dns-prefetch', href: storage_host }/ - %link{ rel: 'icon', href: favicon_path('ico') || '/favicon.ico', type: 'image/x-icon' }/ - - SiteUpload::FAVICON_SIZES.each do |size| %link{ rel: 'icon', sizes: "#{size}x#{size}", href: favicon_path(size.to_i) || frontend_asset_path("icons/favicon-#{size}x#{size}.png"), type: 'image/png' }/ - SiteUpload::APPLE_ICON_SIZES.each do |size| %link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: app_icon_path(size.to_i) || frontend_asset_path("icons/apple-touch-icon-#{size}x#{size}.png") }/ - %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ + - if use_mask_icon? + %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ = theme_color_tags current_theme %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/ @@ -30,7 +29,7 @@ = theme_style_tags current_theme -# Needed for the wicg-inert polyfill. It needs to be on it's own