diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index 0671df390..000000000 --- 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/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index b5e72a097..c6dcc4d46 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,20 +1,15 @@ # For details, see https://github.com/devcontainers/images/tree/main/src/ruby -FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye +FROM mcr.microsoft.com/devcontainers/ruby:1-3.3-bookworm -# Install Rails -# RUN gem install rails webdrivers +# Install node version from .nvmrc +WORKDIR /app +COPY .nvmrc . +RUN /bin/bash --login -i -c "nvm install" -ARG NODE_VERSION="20" -RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1" +# Install additional OS packages +RUN apt-get update && \ + export DEBIAN_FRONTEND=noninteractive && \ + apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev -# [Optional] Uncomment this section to install additional OS packages. -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev - -# [Optional] Uncomment this line to install additional gems. -RUN gem install foreman - -# [Optional] Uncomment this line to install global node packages. -RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && corepack enable" 2>&1 - -COPY welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt +# Move welcome message to where VS Code expects it +COPY .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/codespaces/devcontainer.json b/.devcontainer/codespaces/devcontainer.json index ca9156fda..d2358657f 100644 --- a/.devcontainer/codespaces/devcontainer.json +++ b/.devcontainer/codespaces/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Mastodon on GitHub Codespaces", - "dockerComposeFile": "../docker-compose.yml", + "dockerComposeFile": "../compose.yaml", "service": "app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -23,6 +23,8 @@ } }, + "remoteUser": "root", + "otherPortsAttributes": { "onAutoForward": "silent" }, @@ -37,7 +39,7 @@ }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": ".devcontainer/post-create.sh", + "postCreateCommand": "bin/setup", "waitFor": "postCreateCommand", "customizations": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/compose.yaml similarity index 95% rename from .devcontainer/docker-compose.yml rename to .devcontainer/compose.yaml index 5d9917b39..1e2e1ba7d 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/compose.yaml @@ -1,13 +1,11 @@ -version: '3' - services: app: working_dir: /workspaces/mastodon/ build: - context: . - dockerfile: Dockerfile + context: .. + dockerfile: .devcontainer/Dockerfile volumes: - - ../..:/workspaces:cached + - ..:/workspaces/mastodon:cached environment: RAILS_ENV: development NODE_ENV: development diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fa8d6542c..fb88f7801 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Mastodon on local machine", - "dockerComposeFile": "docker-compose.yml", + "dockerComposeFile": "compose.yaml", "service": "app", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -23,12 +23,14 @@ } }, + "remoteUser": "root", + "otherPortsAttributes": { "onAutoForward": "silent" }, "onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "postCreateCommand": ".devcontainer/post-create.sh", + "postCreateCommand": "bin/setup", "waitFor": "postCreateCommand", "customizations": { diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh deleted file mode 100755 index 82a2ccbb6..000000000 --- a/.devcontainer/post-create.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -e # Fail the whole script on first error - -# Fetch Ruby gem dependencies -bundle config path 'vendor/bundle' -bundle config with 'development test' -bundle install - -# Make Gemfile.lock pristine again -git checkout -- Gemfile.lock - -# Fetch Javascript dependencies -corepack prepare -yarn install --immutable - -# [re]create, migrate, and seed the test database -RAILS_ENV=test ./bin/rails db:setup - -# [re]create, migrate, and seed the development database -RAILS_ENV=development ./bin/rails db:setup - -# Precompile assets for development -RAILS_ENV=development ./bin/rails assets:precompile - -# Precompile assets for test -RAILS_ENV=test ./bin/rails assets:precompile diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt index 488cf9285..dbc19c910 100644 --- a/.devcontainer/welcome-message.txt +++ b/.devcontainer/welcome-message.txt @@ -1,8 +1,7 @@ -👋 Welcome to "Mastodon" in GitHub Codespaces! +👋 Welcome to your Mastodon Dev Container! -🛠️ Your environment is fully setup with all the required software. +🛠️ Your environment is fully setup with all the required software. -🔍 To explore VS Code to its fullest, search using the Command Palette (Cmd/Ctrl + Shift + P or F1). - -📝 Edit away, run your app as usual, and we'll automatically make it available for you to access. +💥 Run `bin/dev` to start the application processes. +🥼 Run `RAILS_ENV=test bin/rails assets:precompile && RAILS_ENV=test bin/rspec` to run the test suite. diff --git a/.eslintrc.js b/.eslintrc.js index 759003b55..e3afb1c9f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -349,6 +349,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/actions/setup-ruby/action.yml b/.github/actions/setup-ruby/action.yml index 3a6fba940..3e232f134 100644 --- a/.github/actions/setup-ruby/action.yml +++ b/.github/actions/setup-ruby/action.yml @@ -14,7 +14,7 @@ runs: shell: bash run: | sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev ${{ inputs.additional-system-dependencies }} + sudo apt-get install -y libicu-dev libidn11-dev libvips42 ${{ inputs.additional-system-dependencies }} - name: Set up Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/codecov.yml b/.github/codecov.yml index 9d6413a10..701ba3af8 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -3,9 +3,9 @@ coverage: status: project: default: - # Github status check is not blocking + # GitHub status check is not blocking informational: true patch: default: - # Github status check is not blocking + # GitHub status check is not blocking informational: true diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 378d4fc83..2cf7bec8e 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -2,6 +2,7 @@ $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', + 'customManagers:dockerfileVersions', ':labels(dependencies)', ':prConcurrentLimitNone', // Remove limit for open PRs at any time. ':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour. @@ -59,7 +60,7 @@ dependencyDashboardApproval: true, }, { - // Update Github Actions and Docker images weekly + // Update GitHub Actions and Docker images weekly matchManagers: ['github-actions', 'dockerfile', 'docker-compose'], extends: ['schedule:weekly'], }, diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml index e100e1582..dbb32af9b 100644 --- a/.github/workflows/build-container-image.yml +++ b/.github/workflows/build-container-image.yml @@ -68,7 +68,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Log in to the Github Container registry + - name: Log in to the GitHub Container registry if: contains(inputs.push_to_images, 'ghcr.io') uses: docker/login-action@v3 with: diff --git a/.github/workflows/bundler-audit.yml b/.github/workflows/bundler-audit.yml index bbc31598c..e3e2da0c7 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/crowdin-download.yml b/.github/workflows/crowdin-download.yml index 1df7672d6..e9da7cb26 100644 --- a/.github/workflows/crowdin-download.yml +++ b/.github/workflows/crowdin-download.yml @@ -58,13 +58,13 @@ jobs: title: 'New Crowdin Translations (automated)' author: 'GitHub Actions ' body: | - New Crowdin translations, automated with Github Actions + New Crowdin translations, automated with GitHub Actions See `.github/workflows/crowdin-download.yml` This PR will be updated every day with new translations. - Due to a limitation in Github Actions, checks are not running on this PR without manual action. + Due to a limitation in GitHub Actions, checks are not running on this PR without manual action. If you want to run the checks, then close and re-open it. branch: i18n/crowdin/translations base: main diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 25615b720..ca4b0c80b 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 411b32348..b3a89c3ca 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/rebase-needed.yml b/.github/workflows/rebase-needed.yml index 06d835c09..8784397a8 100644 --- a/.github/workflows/rebase-needed.yml +++ b/.github/workflows/rebase-needed.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Check for merge conflicts - uses: eps1lon/actions-label-merge-conflict@releases/2.x + uses: eps1lon/actions-label-merge-conflict@v3 with: dirtyLabel: 'rebase needed :construction:' repoToken: '${{ secrets.GITHUB_TOKEN }}' diff --git a/.github/workflows/test-migrations-two-step.yml b/.github/workflows/test-migrations-two-step.yml deleted file mode 100644 index 669884731..000000000 --- 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 1ff5cc06b..3eaf2c2d7 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 2bfa59e6b..513de2072 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 @@ -133,7 +129,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev + additional-system-dependencies: ffmpeg libpam-dev - name: Load database schema run: './bin/rails db:create db:schema:load db:seed' @@ -148,6 +144,93 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-libvips: + name: Libvips tests + runs-on: ubuntu-24.04 + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10ms + --health-timeout 3s + --health-retries 50 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10ms + --health-timeout 3s + --health-retries 50 + ports: + - 6379:6379 + + env: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: ${{ matrix.ruby-version != '.ruby-version' }} + RAILS_ENV: test + ALLOW_NOPAM: true + PAM_ENABLED: true + PAM_DEFAULT_SERVICE: pam_test + PAM_CONTROLLED_SERVICE: pam_test_controlled + OIDC_ENABLED: true + OIDC_SCOPE: read + SAML_ENABLED: true + CAS_ENABLED: true + BUNDLE_WITH: 'pam_authentication test' + GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} + MASTODON_USE_LIBVIPS: true + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.1' + - '3.2' + - '.ruby-version' + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + 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: + ruby-version: ${{ matrix.ruby-version}} + additional-system-dependencies: ffmpeg libpam-dev libyaml-dev + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bin/rspec --tag paperclip_processing + + - name: Upload coverage reports to Codecov + if: matrix.ruby-version == '.ruby-version' + uses: codecov/codecov-action@v4 + with: + files: coverage/lcov/mastodon.lcov + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + test-e2e: name: End to End testing runs-on: ubuntu-latest @@ -163,9 +246,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 +256,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 @@ -209,7 +292,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript @@ -248,9 +331,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 @@ -258,9 +341,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 @@ -271,9 +354,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 @@ -285,9 +368,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 @@ -329,7 +412,7 @@ jobs: uses: ./.github/actions/setup-ruby with: ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick + additional-system-dependencies: ffmpeg - name: Set up Javascript environment uses: ./.github/actions/setup-javascript diff --git a/.nanoignore b/.nanoignore deleted file mode 100644 index 80e939703..000000000 --- a/.nanoignore +++ /dev/null @@ -1,19 +0,0 @@ -.DS_Store -.git/ -.gitignore - -.bundle/ -.cache/ -config/deploy/* -coverage -docs/ -.env -log/*.log -neo4j/ -node_modules/ -public/assets/ -public/system/ -spec/ -tmp/ -.vagrant/ -vendor/bundle/ diff --git a/.rubocop.yml b/.rubocop.yml index cbc0afd28..cf4ee565e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,34 @@ -# Can be removed once all rules are addressed or moved to this file as documented overrides -inherit_from: .rubocop_todo.yml +--- +AllCops: + CacheRootDirectory: tmp + DisplayCopNames: true + DisplayStyleGuide: true + Exclude: + - db/schema.rb + - bin/* + - node_modules/**/* + - Vagrantfile + - vendor/**/* + - config/initializers/json_ld* + - lib/mastodon/migration_helpers.rb + - lib/templates/**/* + ExtraDetails: true + NewCops: enable + TargetRubyVersion: 3.1 # Oldest supported ruby version + UseCache: true + +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 +39,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 000000000..63035837f --- /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 000000000..487879ca2 --- /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 000000000..89532af42 --- /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 000000000..da6ad4ac5 --- /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 000000000..b83928dee --- /dev/null +++ b/.rubocop/rails.yml @@ -0,0 +1,27 @@ +--- +Rails/FilePath: + EnforcedStyle: arguments + +Rails/HttpStatus: + EnforcedStyle: numeric + +Rails/LexicallyScopedActionFilter: + Exclude: + - app/controllers/auth/* # Conflicts with `Lint/UselessMethodDefinition` for inherited controller actions + +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 + +Rails/UnusedIgnoredColumns: + Enabled: false # Preserve ability to migrate from arbitrary old versions diff --git a/.rubocop/rspec.yml b/.rubocop/rspec.yml new file mode 100644 index 000000000..d2d2f8325 --- /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 000000000..993a5689a --- /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 000000000..2222c6d8b --- /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 000000000..03e35a70a --- /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 a70caad8c..8a4e59803 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -27,21 +27,6 @@ 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' diff --git a/.ruby-version b/.ruby-version index 477254331..619b53766 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.3.2 +3.3.3 diff --git a/Dockerfile b/Dockerfile index 55e543afb..2885706c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,8 @@ # syntax=docker/dockerfile:1.7 +# 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 + # Please see https://docs.docker.com/engine/reference/builder for information about # the extended buildx capabilities used in this file. # Make sure multiarch TARGETPLATFORM is available for interpolation @@ -7,22 +10,24 @@ ARG TARGETPLATFORM=${TARGETPLATFORM} ARG BUILDPLATFORM=${BUILDPLATFORM} -# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.1"] -ARG RUBY_VERSION="3.3.1" +# 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.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 -# Ruby image to use for base image based on combined variables (ex: 3.3.1-slim-bookworm) +# 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 # 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"] ARG MASTODON_VERSION_PRERELEASE="bark" -# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"] +# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="pr-12345"] ARG MASTODON_VERSION_METADATA="dev" # Allow Ruby on Rails to serve static files @@ -60,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"] @@ -97,7 +104,6 @@ RUN \ curl \ ffmpeg \ file \ - imagemagick \ libjemalloc2 \ patchelf \ procps \ @@ -131,18 +137,31 @@ 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 \ + build-essential \ git \ libgdbm-dev \ + libglib2.0-dev \ libgmp-dev \ libicu-dev \ libidn-dev \ libpq-dev \ libssl-dev \ - make \ + meson \ + pkg-config \ shared-mime-info \ - zlib1g-dev \ + # 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 \ ; RUN \ @@ -151,6 +170,26 @@ 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 bundler specific build layer from build layer FROM build as bundler @@ -200,16 +239,16 @@ 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; @@ -229,12 +268,27 @@ 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 \ ; # Copy Mastodon sources into final layer @@ -245,9 +299,17 @@ 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 RUN \ -# Precompile bootsnap code for faster Rails startup + ldconfig; \ +# Smoketest media processors + vips -v; + +RUN \ + # Precompile bootsnap code for faster Rails startup bundle exec bootsnap precompile --gemfile app/ lib/; RUN \ diff --git a/Gemfile b/Gemfile index d9de33182..b00eaecbc 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 1.0', require: false gem 'kt-paperclip', '~> 7.2' gem 'md-paperclip-azure', '~> 2.2', require: false +gem 'ruby-vips', '~> 2.2', require: false gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.8' @@ -56,7 +57,7 @@ gem 'hiredis', '~> 0.6' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.2.0' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.6.2' +gem 'httplog', '~> 1.7.0' gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' @@ -106,7 +107,7 @@ gem 'private_address_check', '~> 0.5' gem 'opentelemetry-api', '~> 1.2.5' group :opentelemetry do - gem 'opentelemetry-exporter-otlp', '~> 0.26.3', require: false + gem 'opentelemetry-exporter-otlp', '~> 0.27.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 @@ -170,6 +171,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 c62c57dcc..21c43a828 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,35 +10,35 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.1.3.3) - actionpack (= 7.1.3.3) - activesupport (= 7.1.3.3) + actioncable (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.3.3) - actionpack (= 7.1.3.3) - activejob (= 7.1.3.3) - activerecord (= 7.1.3.3) - activestorage (= 7.1.3.3) - activesupport (= 7.1.3.3) + actionmailbox (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.3.3) - actionpack (= 7.1.3.3) - actionview (= 7.1.3.3) - activejob (= 7.1.3.3) - activesupport (= 7.1.3.3) + actionmailer (7.1.3.4) + actionpack (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activesupport (= 7.1.3.4) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.3.3) - actionview (= 7.1.3.3) - activesupport (= 7.1.3.3) + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) nokogiri (>= 1.8.5) racc rack (>= 2.2.4) @@ -46,15 +46,15 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.3.3) - actionpack (= 7.1.3.3) - activerecord (= 7.1.3.3) - activestorage (= 7.1.3.3) - activesupport (= 7.1.3.3) + actiontext (7.1.3.4) + actionpack (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.3.3) - activesupport (= 7.1.3.3) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) @@ -64,22 +64,22 @@ GEM activemodel (>= 4.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.1.3.3) - activesupport (= 7.1.3.3) + activejob (7.1.3.4) + activesupport (= 7.1.3.4) globalid (>= 0.3.6) - activemodel (7.1.3.3) - activesupport (= 7.1.3.3) - activerecord (7.1.3.3) - activemodel (= 7.1.3.3) - activesupport (= 7.1.3.3) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) timeout (>= 0.4.0) - activestorage (7.1.3.3) - actionpack (= 7.1.3.3) - activejob (= 7.1.3.3) - activerecord (= 7.1.3.3) - activesupport (= 7.1.3.3) + activestorage (7.1.3.4) + actionpack (= 7.1.3.4) + activejob (= 7.1.3.4) + activerecord (= 7.1.3.4) + activesupport (= 7.1.3.4) marcel (~> 1.0) - activesupport (7.1.3.3) + activesupport (7.1.3.4) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -100,17 +100,17 @@ GEM attr_required (1.0.2) awrence (1.2.1) aws-eventstream (1.3.0) - aws-partitions (1.929.0) - aws-sdk-core (3.196.1) + aws-partitions (1.940.0) + aws-sdk-core (3.197.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.81.0) - aws-sdk-core (~> 3, >= 3.193.0) + aws-sdk-kms (1.83.0) + aws-sdk-core (~> 3, >= 3.197.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.151.0) - aws-sdk-core (~> 3, >= 3.194.0) + aws-sdk-s3 (1.152.3) + aws-sdk-core (~> 3, >= 3.197.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) @@ -168,7 +168,7 @@ GEM climate_control (1.2.0) cocoon (1.2.15) color_diff (0.1) - concurrent-ruby (1.3.1) + concurrent-ruby (1.3.3) connection_pool (2.4.1) cose (1.3.0) cbor (~> 0.5.9) @@ -272,7 +272,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) @@ -321,7 +321,7 @@ GEM http-form_data (2.3.0) http_accept_language (2.1.1) httpclient (2.8.3) - httplog (1.6.3) + httplog (1.7.0) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.5) @@ -422,9 +422,9 @@ GEM memory_profiler (1.0.1) 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.6) + mini_portile2 (2.8.7) minitest (5.23.1) msgpack (1.7.2) multi_json (1.15.0) @@ -434,7 +434,7 @@ GEM uri net-http-persistent (4.0.2) connection_pool (~> 2.2) - net-imap (0.4.11) + net-imap (0.4.12) date net-protocol net-ldap (0.19.0) @@ -445,7 +445,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) @@ -453,7 +453,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.16.3) + oj (3.16.4) bigdecimal (>= 3.0) omniauth (2.1.2) hashie (>= 3.4.6) @@ -489,7 +489,7 @@ GEM opentelemetry-api (1.2.5) opentelemetry-common (0.20.1) opentelemetry-api (~> 1.0) - opentelemetry-exporter-otlp (0.26.3) + opentelemetry-exporter-otlp (0.27.0) google-protobuf (~> 3.14) googleapis-common-protos-types (~> 1.3) opentelemetry-api (~> 1.1) @@ -498,6 +498,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) @@ -551,8 +555,9 @@ GEM 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.30.2) 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) @@ -578,15 +583,15 @@ GEM opentelemetry-api (~> 1.0) orm_adapter (0.5.0) ox (2.14.18) - parallel (1.24.0) - parser (3.3.2.0) + parallel (1.25.1) + parser (3.3.3.0) ast (~> 2.4.1) racc parslet (2.0.0) pastel (0.8.0) tty-color (~> 0.5) pg (1.5.6) - pghero (3.4.1) + pghero (3.5.0) activerecord (>= 6) premailer (1.23.0) addressable @@ -634,20 +639,20 @@ GEM rackup (1.0.0) rack (< 3) webrick - rails (7.1.3.3) - actioncable (= 7.1.3.3) - actionmailbox (= 7.1.3.3) - actionmailer (= 7.1.3.3) - actionpack (= 7.1.3.3) - actiontext (= 7.1.3.3) - actionview (= 7.1.3.3) - activejob (= 7.1.3.3) - activemodel (= 7.1.3.3) - activerecord (= 7.1.3.3) - activestorage (= 7.1.3.3) - activesupport (= 7.1.3.3) + rails (7.1.3.4) + actioncable (= 7.1.3.4) + actionmailbox (= 7.1.3.4) + actionmailer (= 7.1.3.4) + actionpack (= 7.1.3.4) + actiontext (= 7.1.3.4) + actionview (= 7.1.3.4) + activejob (= 7.1.3.4) + activemodel (= 7.1.3.4) + activerecord (= 7.1.3.4) + activestorage (= 7.1.3.4) + activesupport (= 7.1.3.4) bundler (>= 1.15.0) - railties (= 7.1.3.3) + railties (= 7.1.3.4) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -662,9 +667,9 @@ GEM rails-i18n (7.0.9) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.1.3.3) - actionpack (= 7.1.3.3) - activesupport (= 7.1.3.3) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) irb rackup (>= 1.0.0) rake (>= 12.2) @@ -686,15 +691,15 @@ GEM redlock (1.3.2) redis (>= 3.0.0, < 6.0) regexp_parser (2.9.2) - reline (0.5.7) + reline (0.5.8) 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.0) + strscan rotp (6.3.0) rouge (4.2.1) rpam2 (4.0.2) @@ -739,9 +744,7 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.31.3) parser (>= 3.3.1.0) - rubocop-capybara (2.20.0) - rubocop (~> 1.41) - rubocop-factory_bot (2.25.1) + rubocop-capybara (2.21.0) rubocop (~> 1.41) rubocop-performance (1.21.0) rubocop (>= 1.48.1, < 2.0) @@ -751,25 +754,25 @@ GEM rack (>= 1.1) rubocop (>= 1.33.0, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) - rubocop-rspec (2.29.2) - 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.1) + 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) nokogiri (>= 1.13.10) rexml + ruby-vips (2.2.1) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyzip (2.3.2) rufus-scheduler (3.9.1) 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) @@ -895,7 +898,7 @@ GEM xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.14) + zeitwerk (2.6.15) PLATFORMS ruby @@ -945,7 +948,7 @@ DEPENDENCIES htmlentities (~> 4.3) http (~> 5.2.0) http_accept_language (~> 2.1) - httplog (~> 1.6.2) + httplog (~> 1.7.0) i18n i18n-tasks (~> 1.0) idn-ruby @@ -976,7 +979,7 @@ DEPENDENCIES omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) opentelemetry-api (~> 1.2.5) - opentelemetry-exporter-otlp (~> 0.26.3) + opentelemetry-exporter-otlp (~> 0.27.0) opentelemetry-instrumentation-active_job (~> 0.7.1) opentelemetry-instrumentation-active_model_serializers (~> 0.20.1) opentelemetry-instrumentation-concurrent_ruby (~> 0.21.2) @@ -1021,8 +1024,10 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + rubocop-rspec_rails ruby-prof ruby-progressbar (~> 1.13) + ruby-vips (~> 2.2) rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) @@ -1050,7 +1055,7 @@ DEPENDENCIES xorcist (~> 1.1) RUBY VERSION - ruby 3.3.1p55 + ruby 3.3.2p78 BUNDLED WITH - 2.5.9 + 2.5.11 diff --git a/README.md b/README.md index 0353a4c67..9c0b0d20e 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,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 @@ -72,7 +72,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre - **Ruby** 3.1+ - **Node.js** 18+ -The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. +The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, and **Scalingo**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. ## Development @@ -86,40 +86,51 @@ A **Vagrant** configuration is included for development purposes. To use it, com - Run `vagrant ssh -c "cd /vagrant && bin/dev"` - Open `http://mastodon.local` in your browser -### MacOS +### macOS -To set up **MacOS** for native development, complete the following steps: +To set up **macOS** for native development, complete the following steps: -- Use a Ruby version manager to install the specified version from `.ruby-version` -- Run `bundle` to install required gems -- Run `brew install postgresql@14 redis imagemagick libidn` to install required dependencies -- Navigate to Mastodon's root directory and run `brew install nvm` then `nvm use` to use the version from `.nvmrc` -- Run `yarn` to install required packages -- Run `corepack enable && corepack prepare` -- Run `RAILS_ENV=development bundle exec rails db:setup` -- Finally, run `bin/dev` which will launch the local services via `overmind` (if installed) or `foreman` +- Install [Homebrew] and run `brew install postgresql@14 redis imagemagick +libidn nvm` to install the required project dependencies +- Use a Ruby version manager to activate the ruby in `.ruby-version` and run + `nvm use` to activate the node version from `.nvmrc` +- Run the `bin/setup` script, which will install the required ruby gems and node + packages and prepare the database for local development +- Finally, run the `bin/dev` script which will launch services via `overmind` + (if installed) or `foreman` ### Docker -For development with **Docker**, complete the following steps: +For production hosting and deployment with **Docker**, use the `Dockerfile` and +`docker-compose.yml` in the project root directory. -- Install Docker Desktop -- Run `docker compose -f .devcontainer/docker-compose.yml up -d` -- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh` -- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app bin/dev` +For local development, install and launch [Docker], and run: -If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers). +```shell +docker compose -f .devcontainer/compose.yaml up -d +docker compose -f .devcontainer/compose.yaml exec app bin/setup +docker compose -f .devcontainer/compose.yaml exec app bin/dev +``` + +### Dev Containers + +Within IDEs that support the [Development Containers] specification, start the +"Mastodon on local machine" container from the editor. The necessary `docker +compose` commands to build and setup the container should run automatically. For +**Visual Studio Code** this requires installing the [Dev Container extension]. ### GitHub Codespaces -To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project.. +[GitHub Codespaces] provides a web-based version of VS Code and a cloud hosted +development environment configured with the software needed for this project. -- Click this button to create a new codespace:
- [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json) -- Wait for the environment to build. This will take a few minutes. -- When the editor is ready, run `bin/dev` in the terminal. -- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon. -- On the _Ports_ tab, right click on the “stream” row and select _Port visibility_ → _Public_. +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)][codespace] + +- Click the button to create a new codespace, and confirm the options +- Wait for the environment to build (takes a few minutes) +- When the editor is ready, run `bin/dev` in the terminal +- Wait for an _Open in Browser_ prompt. This will open Mastodon +- On the _Ports_ tab "stream" setting change _Port visibility_ → _Public_ ## Contributing @@ -138,3 +149,10 @@ This program is free software: you can redistribute it and/or modify it under th This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +[codespace]: https://codespaces.new/mastodon/mastodon?quickstart=1&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json +[Dev Container extension]: https://containers.dev/supporting#dev-containers +[Development Containers]: https://containers.dev/supporting +[Docker]: https://docs.docker.com +[GitHub Codespaces]: https://docs.github.com/en/codespaces +[Homebrew]: https://brew.sh diff --git a/SECURITY.md b/SECURITY.md index 81472b01b..156954ce0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either: -- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) +- open a [GitHub security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) - reach us at You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. diff --git a/Vagrantfile b/Vagrantfile index 8a95e91f3..89f5536ed 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -151,6 +151,12 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| vb.customize ["modifyvm", :id, "--nictype2", "virtio"] end + config.vm.provider :libvirt do |libvirt| + libvirt.cpus = 3 + libvirt.memory = 8192 + end + + # This uses the vagrant-hostsupdater plugin, and lets you # access the development site at http://mastodon.local. # If you change it, also change it in .env.vagrant before provisioning diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 325b33df8..16a8cb9ee 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/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index e8f712457..a37842518 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Accounts::CredentialsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:accounts', :'read:me' }, except: [:update] + before_action -> { doorkeeper_authorize! :profile, :read, :'read:accounts' }, except: [:update] before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:update] before_action :require_user! diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb index 67d987d0e..283383acb 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/timelines/link_controller.rb b/app/controllers/api/v1/timelines/link_controller.rb new file mode 100644 index 000000000..af962c430 --- /dev/null +++ b/app/controllers/api/v1/timelines/link_controller.rb @@ -0,0 +1,52 @@ +# 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 :set_preview_card + before_action :set_statuses + + PERMITTED_PARAMS = %i( + url + limit + ).freeze + + def show + cache_if_unauthenticated! + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + end + + 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 + + def set_statuses + @statuses = @preview_card.nil? ? [] : preload_collection(link_timeline_statuses, Status) + end + + def link_timeline_statuses + link_feed.get( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + params[:min_id] + ) + end + + def link_feed + LinkFeed.new(@preview_card, current_account) + end + + def next_path + api_v1_timelines_link_url next_path_params + end + + def prev_path + api_v1_timelines_link_url prev_path_params + end +end diff --git a/app/controllers/api/v2_alpha/notifications_controller.rb b/app/controllers/api/v2_alpha/notifications_controller.rb new file mode 100644 index 000000000..19d3ac901 --- /dev/null +++ b/app/controllers/api/v2_alpha/notifications_controller.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +class Api::V2Alpha::NotificationsController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] + before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] + before_action :require_user! + after_action :insert_pagination_headers, only: :index + + DEFAULT_NOTIFICATIONS_LIMIT = 40 + + def index + with_read_replica do + @notifications = load_notifications + @group_metadata = load_group_metadata + @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 + end + + def show + @notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id]) + render json: NotificationGroup.from_notification(@notification), serializer: REST::NotificationGroupSerializer + end + + def clear + current_account.notifications.delete_all + render_empty + end + + def dismiss + current_account.notifications.where(group_key: params[:id]).destroy_all + render_empty + end + + private + + def load_notifications + notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id( + limit_param(DEFAULT_NOTIFICATIONS_LIMIT), + params_slice(:max_id, :since_id, :min_id) + ) + + Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses| + preload_collection(target_statuses, Status) + end + end + + def load_group_metadata + return {} if @notifications.empty? + + browserable_account_notifications + .where(group_key: @notifications.filter_map(&:group_key)) + .where(id: (@notifications.last.id)..(@notifications.first.id)) + .group(:group_key) + .pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at') + .to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] } + end + + def browserable_account_notifications + current_account.notifications.without_suspended.browserable( + types: Array(browserable_params[:types]), + exclude_types: Array(browserable_params[:exclude_types]), + include_filtered: truthy_param?(:include_filtered) + ) + end + + def target_statuses_from_notifications + @notifications.filter_map(&:target_status) + end + + def next_path + api_v2_alpha_notifications_url pagination_params(max_id: pagination_max_id) unless @notifications.empty? + end + + def prev_path + api_v2_alpha_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? + end + + def pagination_collection + @notifications + end + + def browserable_params + params.permit(:include_filtered, types: [], exclude_types: []) + end + + def pagination_params(core_params) + params.slice(:limit, :types, :exclude_types, :include_filtered).permit(:limit, :include_filtered, types: [], exclude_types: []).merge(core_params) + end +end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 6849979b1..d6573f9b4 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -13,7 +13,7 @@ class Settings::ApplicationsController < Settings::BaseController def new @application = Doorkeeper::Application.new( redirect_uri: Doorkeeper.configuration.native_redirect_uri, - scopes: 'read:me' + scopes: 'profile' ) end diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index 4018ef6b1..e8d563412 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/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index 16f191b58..d906bdfb1 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?.author_account) { + pushUnique(accounts, status.card.author_account); + } } statuses.forEach(processStatus); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index b5a30343e..be76b0f39 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -36,6 +36,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + if (status.card?.author_account) { + normalStatus.card = { ...status.card, author_account: status.card.author_account.id }; + } + if (status.filtered) { normalStatus.filtered = status.filtered.map(normalizeFilterResult); } diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js index 0b840b41c..01089fccb 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.map(link => link.author_account).filter(account => !!account))); + dispatch(fetchTrendingLinksSuccess(data)); + }) .catch(err => dispatch(fetchTrendingLinksFail(err))); }; 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 000000000..c20e76ac4 --- /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 63eec5349..baa220af5 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/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx index 5ec029593..0478f7a1a 100644 --- a/app/javascript/mastodon/features/account_timeline/index.jsx +++ b/app/javascript/mastodon/features/account_timeline/index.jsx @@ -199,6 +199,7 @@ class AccountTimeline extends ImmutablePureComponent { emptyMessage={emptyMessage} bindToDocument={!multiColumn} timelineId='account' + withCounters /> ); diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index c3bd908a4..47e81cf13 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx +++ b/app/javascript/mastodon/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/mastodon/features/explore/components/author_link.jsx b/app/javascript/mastodon/features/explore/components/author_link.jsx new file mode 100644 index 000000000..b9dec3367 --- /dev/null +++ b/app/javascript/mastodon/features/explore/components/author_link.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; + +import { Link } from 'react-router-dom'; + +import { Avatar } from 'mastodon/components/avatar'; +import { useAppSelector } from 'mastodon/store'; + +export const AuthorLink = ({ accountId }) => { + const account = useAppSelector(state => state.getIn(['accounts', accountId])); + + return ( + + + + + ); +}; + +AuthorLink.propTypes = { + accountId: PropTypes.string.isRequired, +}; diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index 80dd5200f..a2cae942d 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -1,61 +1,89 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useState, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; + 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 9e143b450..93fd1fb6d 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(['author_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/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index c2f5703b3..f562e53f0 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 = { @@ -259,7 +244,7 @@ export default class Card extends PureComponent { {description} - {showAuthor && } + {showAuthor && } ); } 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 4216f3da3..74a8fdb84 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/locales/ar.json b/app/javascript/mastodon/locales/ar.json index 68e32dd2a..6d67d354b 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -225,7 +225,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 +266,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 +297,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 +308,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 +414,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 +475,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": "لقد انتهى استطلاع رأي شاركتَ فيه", diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 61e96e4b5..041d90775 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -308,6 +308,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 +317,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 +414,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": "Выдаліць спіс", @@ -458,7 +463,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 +475,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": "Прыняць", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 68429b093..e178c4736 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -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", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index ae2968087..1b6bd3642 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -415,6 +415,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", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index ef08e9b6d..5a6e15b09 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", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 63298d59e..f0c27ad70 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", @@ -695,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 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 6e7885f48..2dbbf7877 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -498,7 +498,14 @@ "poll_button.add_poll": "Aldoni balotenketon", "poll_button.remove_poll": "Forigi balotenketon", "privacy.change": "Agordi mesaĝan privatecon", + "privacy.direct.long": "Ĉiuj menciitaj en la afiŝo", + "privacy.direct.short": "Specifaj homoj", + "privacy.private.long": "Nur viaj sekvantoj", + "privacy.private.short": "Sekvantoj", + "privacy.public.long": "Ĉiujn ajn ĉe kaj ekster Mastodon", "privacy.public.short": "Publika", + "privacy.unlisted.long": "Malpli algoritmaj fanfaroj", + "privacy.unlisted.short": "Diskrete publika", "privacy_policy.last_updated": "Laste ĝisdatigita en {date}", "privacy_policy.title": "Politiko de privateco", "recommended": "Rekomendita", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 4c30bfa25..1cbd7b18e 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -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", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 564d7ec57..dd7dd2bea 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -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", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 14d3bf0dd..e1ba41347 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -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", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 6c2162e52..68ea3925b 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -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", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index b77f609a2..f4ae229d4 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -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", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 9e2985290..9c14d05d5 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -156,7 +156,7 @@ "compose_form.poll.duration": "Durée du sondage", "compose_form.poll.multiple": "Choix multiple", "compose_form.poll.option_placeholder": "Option {number}", - "compose_form.poll.single": "Choisissez-en un", + "compose_form.poll.single": "Choix unique", "compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix", "compose_form.poll.switch_to_single": "Changer le sondage pour n'autoriser qu'un seul choix", "compose_form.poll.type": "Style", @@ -585,9 +585,9 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.unlisted.additional": "Cette option se comporte exactement comme l'option publique, sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, l'exploration ou la recherche Mastodon, même si vous avez opté pour l'option publique pour l'ensemble de votre compte.", + "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", "privacy.unlisted.long": "Moins de fanfares algorithmiques", - "privacy.unlisted.short": "Public calme", + "privacy.unlisted.short": "Public discret", "privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.title": "Politique de confidentialité", "recommended": "Recommandé", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 1a5803623..36ec673a4 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -156,7 +156,7 @@ "compose_form.poll.duration": "Durée du sondage", "compose_form.poll.multiple": "Choix multiple", "compose_form.poll.option_placeholder": "Option {number}", - "compose_form.poll.single": "Choisissez-en un", + "compose_form.poll.single": "Choix unique", "compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix", "compose_form.poll.switch_to_single": "Modifier le sondage pour autoriser qu'un seul choix", "compose_form.poll.type": "Style", @@ -585,9 +585,9 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.unlisted.additional": "Cette option se comporte exactement comme l'option publique, sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, l'exploration ou la recherche Mastodon, même si vous avez opté pour l'option publique pour l'ensemble de votre compte.", + "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", "privacy.unlisted.long": "Moins de fanfares algorithmiques", - "privacy.unlisted.short": "Public calme", + "privacy.unlisted.short": "Public discret", "privacy_policy.last_updated": "Dernière mise à jour {date}", "privacy_policy.title": "Politique de confidentialité", "recommended": "Recommandé", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 0847b8bf0..925e4dd6f 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -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", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index 4bcf4c88b..ed33a45d4 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -19,7 +19,7 @@ "account.block_domain": "Blocar dominio {domain}", "account.block_short": "Blocar", "account.blocked": "Blocate", - "account.browse_more_on_origin_server": "Percurrer plus sur le profilo original", + "account.browse_more_on_origin_server": "Explorar plus sur le profilo original", "account.cancel_follow_request": "Cancellar sequimento", "account.copy": "Copiar ligamine a profilo", "account.direct": "Mentionar privatemente @{name}", @@ -58,7 +58,7 @@ "account.open_original_page": "Aperir le pagina original", "account.posts": "Messages", "account.posts_with_replies": "Messages e responsas", - "account.report": "Signalar @{name}", + "account.report": "Reportar @{name}", "account.requested": "Attendente le approbation. Clicca pro cancellar le requesta de sequer", "account.requested_follow": "{name} ha requestate de sequer te", "account.share": "Compartir profilo de @{name}", @@ -111,7 +111,7 @@ "bundle_modal_error.message": "Un error ha occurrite durante le cargamento de iste componente.", "bundle_modal_error.retry": "Tentar novemente", "closed_registrations.other_server_instructions": "Perque Mastodon es decentralisate, tu pote crear un conto sur un altere servitor e totevia interager con iste servitor.", - "closed_registrations_modal.description": "Crear un conto in {domain} actualmente non es possibile, ma considera que non es necessari haber un conto specificamente sur {domain} pro usar Mastodon.", + "closed_registrations_modal.description": "Crear un conto sur {domain} non es actualmente possibile, ma considera que non es necessari haber un conto specificamente sur {domain} pro usar Mastodon.", "closed_registrations_modal.find_another_server": "Cercar un altere servitor", "closed_registrations_modal.preamble": "Mastodon es decentralisate, dunque, non importa ubi tu crea tu conto, tu pote sequer e communicar con omne persona sur iste servitor. Tu pote mesmo hospitar tu proprie servitor!", "closed_registrations_modal.title": "Crear un conto sur Mastodon", @@ -274,7 +274,7 @@ "error.unexpected_crash.next_steps": "Tenta refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.", "error.unexpected_crash.next_steps_addons": "Tenta disactivar istes e refrescar le pagina. Si isto non remedia le problema, es possibile que tu pote totevia usar Mastodon per medio de un altere navigator o application native.", "errors.unexpected_crash.copy_stacktrace": "Copiar le traciamento del pila al area de transferentia", - "errors.unexpected_crash.report_issue": "Signalar un defecto", + "errors.unexpected_crash.report_issue": "Reportar problema", "explore.search_results": "Resultatos de recerca", "explore.suggested_follows": "Personas", "explore.title": "Explorar", @@ -468,7 +468,7 @@ "navigation_bar.search": "Cercar", "navigation_bar.security": "Securitate", "not_signed_in_indicator.not_signed_in": "Es necessari aperir session pro acceder a iste ressource.", - "notification.admin.report": "{name} ha signalate {target}", + "notification.admin.report": "{name} ha reportate {target}", "notification.admin.sign_up": "{name} se ha inscribite", "notification.favourite": "{name} ha marcate tu message como favorite", "notification.follow": "{name} te ha sequite", @@ -499,7 +499,7 @@ "notification_requests.title": "Notificationes filtrate", "notifications.clear": "Rader notificationes", "notifications.clear_confirmation": "Es tu secur que tu vole rader permanentemente tote tu notificationes?", - "notifications.column_settings.admin.report": "Nove signalationes:", + "notifications.column_settings.admin.report": "Nove reportos:", "notifications.column_settings.admin.sign_up": "Nove inscriptiones:", "notifications.column_settings.alert": "Notificationes de scriptorio", "notifications.column_settings.favourite": "Favorites:", @@ -636,7 +636,7 @@ "report.close": "Facite", "report.comment.title": "Ha il altere cosas que nos deberea saper?", "report.forward": "Reinviar a {target}", - "report.forward_hint": "Le conto es de un altere servitor. Inviar un copia anonymisate del signalation a illo tamben?", + "report.forward_hint": "Le conto es de un altere servitor. Inviar un copia anonymisate del reporto a illo tamben?", "report.mute": "Silentiar", "report.mute_explanation": "Tu non videra le messages de iste persona. Ille pote totevia sequer te e vider tu messages e non sapera de esser silentiate.", "report.next": "Sequente", @@ -656,11 +656,11 @@ "report.statuses.subtitle": "Selige tote le responsas appropriate", "report.statuses.title": "Existe alcun messages que appoia iste reporto?", "report.submit": "Submitter", - "report.target": "Signalamento de {target}", + "report.target": "Reportage de {target}", "report.thanks.take_action": "Ecce tu optiones pro controlar lo que tu vide sur Mastodon:", "report.thanks.take_action_actionable": "Durante que nos revide isto, tu pote prender mesuras contra @{name}:", "report.thanks.title": "Non vole vider isto?", - "report.thanks.title_actionable": "Gratias pro signalar, nos investigara isto.", + "report.thanks.title_actionable": "Gratias pro reportar, nos investigara isto.", "report.unfollow": "Cessar de sequer @{name}", "report.unfollow_explanation": "Tu seque iste conto. Pro non plus vider su messages in tu fluxo de initio, cessa de sequer lo.", "report_notification.attached_statuses": "{count, plural, one {{count} message} other {{count} messages}} annexate", @@ -747,7 +747,7 @@ "status.replied_to": "Respondite a {name}", "status.reply": "Responder", "status.replyAll": "Responder al discussion", - "status.report": "Signalar @{name}", + "status.report": "Reportar @{name}", "status.sensitive_warning": "Contento sensibile", "status.share": "Compartir", "status.show_filter_reason": "Monstrar in omne caso", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 33161f888..79224c57d 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -299,6 +299,11 @@ "follow_suggestions.dismiss": "Jangan tampilkan lagi", "follow_suggestions.hints.featured": "Profil ini telah dipilih sendiri oleh tim {domain}.", "follow_suggestions.hints.friends_of_friends": "Profil ini populer di kalangan orang yang anda ikuti.", + "follow_suggestions.personalized_suggestion": "Saran yang dipersonalisasi", + "follow_suggestions.popular_suggestion": "Saran populer", + "follow_suggestions.popular_suggestion_longer": "Populer di {domain}", + "follow_suggestions.similar_to_recently_followed_longer": "Serupa dengan profil yang baru Anda ikuti", + "follow_suggestions.view_all": "Lihat semua", "followed_tags": "Tagar yang diikuti", "footer.about": "Tentang", "footer.directory": "Direktori profil", @@ -324,6 +329,7 @@ "home.column_settings.show_reblogs": "Tampilkan boost", "home.column_settings.show_replies": "Tampilkan balasan", "home.hide_announcements": "Sembunyikan pengumuman", + "home.pending_critical_update.link": "Lihat pembaruan", "home.show_announcements": "Tampilkan pengumuman", "interaction_modal.description.follow": "Dengan sebuah akun di Mastodon, Anda bisa mengikuti {name} untuk menerima kirimannya di beranda Anda.", "interaction_modal.description.reblog": "Dengan sebuah akun di Mastodon, Anda bisa mem-boost kiriman ini untuk membagikannya ke pengikut Anda sendiri.", @@ -375,6 +381,7 @@ "lightbox.previous": "Sebelumnya", "limited_account_hint.action": "Tetap tampilkan profil", "limited_account_hint.title": "Profil ini telah disembunyikan oleh moderator {domain}.", + "link_preview.author": "Oleh {name}", "lists.account.add": "Tambah ke daftar", "lists.account.remove": "Hapus dari daftar", "lists.delete": "Hapus daftar", @@ -389,8 +396,11 @@ "lists.search": "Cari di antara orang yang Anda ikuti", "lists.subheading": "Daftar Anda", "load_pending": "{count, plural, other {# item baru}}", + "loading_indicator.label": "Memuat…", "media_gallery.toggle_visible": "Tampil/Sembunyikan", "moved_to_account_banner.text": "Akun {disabledAccount} Anda kini dinonaktifkan karena Anda pindah ke {movedToAccount}.", + "mute_modal.hide_options": "Sembunyikan opsi", + "mute_modal.title": "Bisukan pengguna?", "navigation_bar.about": "Tentang", "navigation_bar.blocks": "Pengguna diblokir", "navigation_bar.bookmarks": "Markah", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 6ce72b43f..2a33c160a 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -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", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index f66497e0a..4c183e32c 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -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", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 7cd74fa50..277a87fe3 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -414,7 +414,7 @@ "limited_account_hint.action": "그래도 프로필 보기", "limited_account_hint.title": "이 프로필은 {domain}의 중재자에 의해 숨겨진 상태입니다.", "link_preview.author": "{name}", - "link_preview.more_from_author": "{name} 더 둘러보기", + "link_preview.more_from_author": "{name} 프로필 보기", "lists.account.add": "리스트에 추가", "lists.account.remove": "리스트에서 제거", "lists.delete": "리스트 삭제", diff --git a/app/javascript/mastodon/locales/la.json b/app/javascript/mastodon/locales/la.json index 48b233400..4bee0efed 100644 --- a/app/javascript/mastodon/locales/la.json +++ b/app/javascript/mastodon/locales/la.json @@ -1,7 +1,9 @@ { "about.contact": "Ratio:", "about.domain_blocks.no_reason_available": "Ratio abdere est", + "about.domain_blocks.silenced.explanation": "Tua profilia atque tuum contentum ab hac serve praecipue non videbis, nisi explōrēs expresse aut subsequeris et optēs.", "account.account_note_header": "Annotatio", + "account.add_or_remove_from_list": "Adde aut ēripe ex tabellīs", "account.badges.bot": "Robotum", "account.badges.group": "Congregatio", "account.block": "Impedire @{name}", @@ -11,11 +13,21 @@ "account.domain_blocked": "Dominium impeditum", "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", + "admin.impact_report.instance_accounts": "Rationes perfiles hoc deleret", + "alert.unexpected.message": "Error inopinatus occurrit.", "announcement.announcement": "Proclamatio", + "attachments_list.unprocessed": "(immūtātus)", + "block_modal.you_wont_see_mentions": "Nuntios quibus eos commemorant non videbis.", "bundle_column_error.error.title": "Eheu!", "bundle_column_error.retry": "Retemptare", "bundle_column_error.routing.title": "CCCCIIII", @@ -32,30 +44,60 @@ "compose_form.direct_message_warning_learn_more": "Discere plura", "compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", + "compose_form.lock_disclaimer": "Tua ratio non est {clausa}. Quisquis te sequi potest ut visum accipiat nuntios tuos tantum pro sectatoribus.", "compose_form.lock_disclaimer.lock": "clausum", "compose_form.placeholder": "What is on your mind?", "compose_form.publish_form": "Barrire", "compose_form.spoiler.marked": "Text is hidden behind warning", - "compose_form.spoiler.unmarked": "Text is not hidden", + "compose_form.spoiler.unmarked": "Adde praeconium contentūs", "confirmations.block.confirm": "Impedire", "confirmations.delete.confirm": "Oblitterare", "confirmations.delete.message": "Are you sure you want to delete this status?", "confirmations.delete_list.confirm": "Oblitterare", + "confirmations.discard_edit_media.message": "Habēs mutationēs in descriptionem vel prōspectum medii quae nōn sunt servātae; eas dēmittam?", "confirmations.mute.confirm": "Confutare", "confirmations.reply.confirm": "Respondere", + "disabled_account_banner.account_settings": "Praeferentiae ratiōnis", + "disabled_account_banner.text": "Ratio tua {disabledAccount} debilitata est.", "dismissable_banner.explore_links": "These news stories are being talked about by people on this and other servers of the decentralized network right now.", "dismissable_banner.explore_tags": "These hashtags are gaining traction among people on this and other servers of the decentralized network right now.", + "domain_block_modal.you_will_lose_followers": "Omnes sectatores tuī ex hoc servō removēbuntur.", + "domain_block_modal.you_wont_see_posts": "Nuntios aut notificātiōnēs ab usoribus in hōc servō nōn vidēbis.", + "domain_pill.activitypub_like_language": "ActivityPub est velut lingua quam Mastodon cum aliīs sociālibus rētibus loquitur.", + "domain_pill.your_handle": "Tuus nominulus:", + "domain_pill.your_server": "Tua domus digitalis, ubi omnia tua nuntia habitant. Hanc non amas? Servēs trānsferāre potes quōcumque tempore et sectātōrēs tuōs simul addūcere.", + "domain_pill.your_username": "Tuō singulāre id indicium in hōc servō est. Est possibile invenīre usōrēs cum eōdem nōmine in servīs aliīs.", "embed.instructions": "Embed this status on your website by copying the code below.", + "emoji_button.activity": "Actiō", "emoji_button.food": "Cibus et potus", "emoji_button.people": "Homines", "emoji_button.search": "Quaerere...", + "empty_column.account_suspended": "Rātiō suspēnsa", "empty_column.account_timeline": "Hic nulla contributa!", "empty_column.account_unavailable": "Notio non impetrabilis", - "empty_column.home": "Your home timeline is empty! Follow more people to fill it up. {suggestions}", + "empty_column.blocks": "Nondum quemquam usorem obsēcāvisti.", + "empty_column.direct": "Nōn habēs adhūc ullo mentionēs prīvātās. Cum ūnam mīseris aut accipis, hīc apparēbit.", + "empty_column.followed_tags": "Nōn adhūc aliquem hastāginem secūtus es. Cum id fēceris, hic ostendētur.", + "empty_column.home": "Tua linea temporum domesticus vacua est! Sequere plures personas ut eam compleas.", "empty_column.list": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", + "empty_column.lists": "Nōn adhūc habēs ullo tabellās. Cum creās, hīc apparēbunt.", + "empty_column.mutes": "Nondum quemquam usorem tacuisti.", + "empty_column.notification_requests": "Omnia clara sunt! Nihil hic est. Cum novās notificātiōnēs accipīs, hic secundum tua praecepta apparebunt.", + "empty_column.notifications": "Nōn adhūc habēs ullo notificātiōnēs. Cum aliī tē interagunt, hīc videbis.", "explore.trending_statuses": "Contributa", + "filtered_notifications_banner.mentions": "{count, plural, one {mentiō} other {mentiōnēs}}", + "firehose.all": "Omnis", + "footer.about": "De", "generic.saved": "Servavit", + "hashtag.column_settings.tag_mode.all": "Haec omnia", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.counter_by_accounts": "{count, plural, one {{counter} particeps} other {{counter} participēs}}", + "hashtag.counter_by_uses": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}}", + "hashtag.counter_by_uses_today": "{count, plural, one {{counter} nuntius} other {{counter} nuntii}} hodie", + "hashtags.and_other": "…et {count, plural, other {# plus}}", + "intervals.full.days": "{number, plural, one {# die} other {# dies}}", + "intervals.full.hours": "{number, plural, one {# hora} other {# horae}}", + "intervals.full.minutes": "{number, plural, one {# minutum} other {# minuta}}", "keyboard_shortcuts.back": "Re navigare", "keyboard_shortcuts.blocked": "Aperire listam usorum obstructorum", "keyboard_shortcuts.boost": "Inlustrare publicatio", @@ -89,17 +131,47 @@ "keyboard_shortcuts.up": "to move up in the list", "lightbox.close": "Claudere", "lightbox.next": "Secundum", + "lists.account.add": "Adde ad tabellās", + "lists.new.create": "Addere tabella", + "load_pending": "{count, plural, one {# novum item} other {# nova itema}}", + "media_gallery.toggle_visible": "{number, plural, one {Cēla imaginem} other {Cēla imagines}}", + "moved_to_account_banner.text": "Tua ratione {disabledAccount} interdum reposita est, quod ad {movedToAccount} migrāvisti.", + "mute_modal.you_wont_see_mentions": "Non videbis nuntios quī eōs commemorant.", + "navigation_bar.about": "De", "navigation_bar.domain_blocks": "Hidden domains", - "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", - "notification.reblog": "{name} boosted your status", + "not_signed_in_indicator.not_signed_in": "Ad hunc locum pervenire oportet ut inīre facias.", + "notification.admin.report": "{name} nuntiavit {target}", + "notification.admin.sign_up": "{name} subscripsit", + "notification.favourite": "{name} nuntium tuum favit", + "notification.follow": "{name} te secutus est", + "notification.follow_request": "{name} postulavit ut te sequeretur", + "notification.mention": "{name} memoravi", + "notification.moderation_warning": "Accepistī monitionem moderationis.", + "notification.moderation_warning.action_disable": "Ratio tua debilitata est.", + "notification.moderation_warning.action_none": "Tua ratiō monitum moderātiōnis accēpit.", + "notification.moderation_warning.action_sensitive": "Tua nuntia hinc sensibiliter notabuntur.", + "notification.moderation_warning.action_silence": "Ratio tua est limitata.", + "notification.moderation_warning.action_suspend": "Ratio tua suspensus est.", + "notification.own_poll": "Suffragium tuum terminatum est.", + "notification.poll": "Electione in quam suffragium dedisti finita est.", + "notification.reblog": "{name} tuum nuntium amplificavit.", + "notification.relationships_severance_event.account_suspension": "Admin ab {from} {target} suspendit, quod significat nōn iam posse tē novitātēs ab eīs accipere aut cum eīs interagere.", + "notification.relationships_severance_event.domain_block": "Admin ab {from} {target} obsēcāvit, includēns {followersCount} ex tuīs sectātōribus et {followingCount, plural, one {# ratione} other {# rationibus}} quās sequeris.", + "notification.relationships_severance_event.user_domain_block": "Bloqueāstī {target}, removēns {followersCount} ex sectātōribus tuīs et {followingCount, plural, one {# rationem} other {# rationēs}} quōs sequeris.", + "notification.status": "{name} nuper publicavit", + "notification.update": "{name} nuntium correxit", + "notification_requests.accept": "Accipe", "notifications.filter.all": "Omnia", "notifications.filter.polls": "Eventus electionis", + "notifications.group": "Notificātiōnēs", "onboarding.actions.go_to_explore": "See what's trending", "onboarding.actions.go_to_home": "Go to your home feed", - "onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!", + "onboarding.follows.lead": "Tua domus feed est principalis via Mastodon experīrī. Quō plūrēs persōnas sequeris, eō actīvior et interessantior erit. Ad tē incipiendum, ecce quaedam suāsiones:", "onboarding.follows.title": "Popular on Mastodon", - "onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:", + "onboarding.profile.display_name_hint": "Tuum nomen completum aut tuum nomen ludens...", + "onboarding.start.lead": "Nunc pars es Mastodonis, singularis, socialis medii platformae decentralis ubi—non algorismus—tuam ipsius experientiam curas. Incipiāmus in nova hac socialis regione:", "onboarding.start.skip": "Want to skip right ahead?", + "onboarding.start.title": "Perfecisti eam!", "onboarding.steps.follow_people.body": "You curate your own feed. Lets fill it with interesting people.", "onboarding.steps.follow_people.title": "Follow {count, plural, one {one person} other {# people}}", "onboarding.steps.publish_status.body": "Say hello to the world.", @@ -107,29 +179,48 @@ "onboarding.steps.setup_profile.title": "Customize your profile", "onboarding.steps.share_profile.body": "Let your friends know how to find you on Mastodon!", "onboarding.steps.share_profile.title": "Share your profile", + "onboarding.tips.2fa": "Scisne? Tūam ratiōnem sēcūrāre potes duōrum elementōrum authentīcātiōnem in ratiōnis tuī praeferentiīs statuendō. Cum ūllā app TOTP ex tuā ēlēctiōne operātur, numerus tēlephōnicus necessārius nōn est!", + "onboarding.tips.accounts_from_other_servers": "Scisne? Quoniam Mastodon dēcentrālis est, nōnnulla profīlia quae invenīs in servīs aliīs quam tuōrum erunt hospitāta. Tamen cum eīs sine impedīmentō interāgere potes! Servus eōrum in alterā parte nōminis eōrum est!", + "onboarding.tips.migration": "Scisne? Sī sentīs {domain} tibi in futūrō nōn esse optimam servī ēlēctiōnem, ad alium servum Mastodon sine amittendō sectātōribus tuīs migrāre potes. Etiam tuum servum hospitārī potes!", + "onboarding.tips.verification": "Scisne? Tūam ratiōnem verificāre potes iungendō nexum ad prōfīlium Mastodon tuum in propriā pāginā interrētiā et addendō pāginam ad prōfīlium tuum. Nullae pecūniae aut documenta necessāria sunt!", "poll.closed": "Clausum", + "poll.total_people": "{count, plural, one {# persona} other {# personae}}", + "poll.total_votes": "{count, plural, one {# suffragium} other {# suffragia}}", "poll.vote": "Eligere", "poll.voted": "Elegisti hoc responsum", + "poll.votes": "{votes, plural, one {# sufragium} other {# sufragia}}", "poll_button.add_poll": "Addere electionem", "poll_button.remove_poll": "Auferre electionem", "privacy.change": "Adjust status privacy", "privacy.public.short": "Coram publico", + "regeneration_indicator.sublabel": "Tua domus feed praeparātur!", + "relative_time.full.days": "{number, plural, one {# ante die} other {# ante dies}}", + "relative_time.full.hours": "{number, plural, one {# ante horam} other {# ante horas}}", "relative_time.full.just_now": "nunc", + "relative_time.full.minutes": "{number, plural, one {# ante minutum} other {# ante minuta}}", + "relative_time.full.seconds": "{number, plural, one {# ante secundum} other {# ante secunda}}", "relative_time.just_now": "nunc", "relative_time.today": "hodie", + "reply_indicator.attachments": "{count, plural, one {# annexus} other {# annexūs}}", "report.block": "Impedimentum", + "report.block_explanation": "Non videbis eorum nuntios. Non poterunt vidēre tuōs nuntios aut tē sequī. Intelligere poterunt sē obstrūctōs esse.", "report.categories.other": "Altera", "report.category.title_account": "notio", "report.category.title_status": "contributum", "report.close": "Confectum", "report.mute": "Confutare", + "report.mute_explanation": "Non videbis eōrum nuntiōs. Possunt adhuc tē sequī et tuōs nuntiōs vidēre, nec sciēbunt sē tacitōs esse.", "report.next": "Secundum", - "report.placeholder": "Type or paste additional comments", + "report.placeholder": "Commentāriī adiūnctī", "report.submit": "Mittere", "report.target": "Report {target}", - "report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached", + "report_notification.attached_statuses": "{count, plural, one {{count} nuntius} other {{count} nuntii}} attachiatus", "report_notification.categories.other": "Altera", "search.placeholder": "Quaerere", + "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", @@ -139,13 +230,29 @@ "status.delete": "Oblitterare", "status.edit": "Recolere", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", + "status.favourites": "{count, plural, one {favoritum} other {favorita}}", + "status.history.created": "{name} creatum {date}", + "status.history.edited": "{name} correxit {date}", "status.open": "Expand this status", - "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", + "status.reblogged_by": "{name} adiuvavit", + "status.reblogs": "{count, plural, one {auctus} other {auctūs}}", + "status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}", "tabs_bar.home": "Domi", + "time_remaining.days": "{number, plural, one {# die} other {# dies}} restant", + "time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant", + "time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant", + "time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant", + "timeline_hint.remote_resource_not_displayed": "{resource} ab aliīs servīs nōn ostenduntur.", "timeline_hint.resources.statuses": "Contributa pristina", - "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}", + "trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}", + "ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.", + "units.short.billion": "{count} millia milionum", + "units.short.million": "{count} milionum", + "units.short.thousand": "{count} millia", + "upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde", "upload_form.audio_description": "Describe for people who are hard of hearing", "upload_form.edit": "Recolere", + "upload_modal.description_placeholder": "A velox brunneis vulpes salit super piger canis", "upload_progress.label": "Uploading…", "video.mute": "Confutare soni" } diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 40541b375..307230036 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -217,7 +217,7 @@ "domain_block_modal.title": "Blokuoti domeną?", "domain_block_modal.you_will_lose_followers": "Visi tavo sekėjai iš šio serverio bus pašalinti.", "domain_block_modal.you_wont_see_posts": "Nematysi naudotojų įrašų ar pranešimų šiame serveryje.", - "domain_pill.activitypub_lets_connect": "Tai leidžia tau sąveikauti su žmonėmis ne tik Mastodon, bet ir įvairiose socialinėse programėlėse.", + "domain_pill.activitypub_lets_connect": "Tai leidžia tau prisijungti ir bendrauti su žmonėmis ne tik Mastodon, bet ir įvairiose socialinėse programėlėse.", "domain_pill.activitypub_like_language": "ActivityPub – tai tarsi kalba, kuria Mastodon kalba su kitais socialiniais tinklais.", "domain_pill.server": "Serveris", "domain_pill.their_handle": "Jų socialinis medijos vardas:", @@ -433,7 +433,15 @@ "loading_indicator.label": "Kraunama…", "media_gallery.toggle_visible": "{number, plural, one {Slėpti vaizdą} few {Slėpti vaizdus} many {Slėpti vaizdo} other {Slėpti vaizdų}}", "moved_to_account_banner.text": "Tavo paskyra {disabledAccount} šiuo metu išjungta, nes persikėlei į {movedToAccount}.", + "mute_modal.hide_from_notifications": "Slėpti nuo pranešimų", + "mute_modal.hide_options": "Slėpti parinktis", + "mute_modal.indefinite": "Kol atšauksiu jų nutildymą", "mute_modal.show_options": "Rodyti parinktis", + "mute_modal.they_can_mention_and_follow": "Jie gali tave paminėti ir sekti, bet tu jų nematysi.", + "mute_modal.they_wont_know": "Jie nežinos, kad buvo nutildyti.", + "mute_modal.title": "Nutildyti naudotoją?", + "mute_modal.you_wont_see_mentions": "Nematysi įrašus, kuriuose jie paminimi.", + "mute_modal.you_wont_see_posts": "Jie vis tiek gali matyti tavo įrašus, bet tu nematysi jų.", "navigation_bar.about": "Apie", "navigation_bar.advanced_interface": "Atidaryti išplėstinę žiniatinklio sąsają", "navigation_bar.blocks": "Užblokuoti naudotojai", @@ -478,6 +486,7 @@ "notification.own_poll": "Tavo apklausa baigėsi", "notification.poll": "Apklausa, kurioje balsavai, pasibaigė", "notification.reblog": "{name} pakėlė tavo įrašą", + "notification.relationships_severance_event": "Prarasti sąryšiai su {name}", "notification.relationships_severance_event.learn_more": "Sužinoti daugiau", "notification.relationships_severance_event.user_domain_block": "Tu užblokavai {target}. Pašalinama {followersCount} savo sekėjų ir {followingCount, plural, one {# paskyrą} few {# paskyrai} many {# paskyros} other {# paskyrų}}, kurios seki.", "notification.status": "{name} ką tik paskelbė", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index bf081ad58..01610b565 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", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 3711cc0ae..2f214bc99 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -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", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 6f67e8f74..26dbbf51a 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ę", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index c389d4f4f..a06e4c6d1 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -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", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index c583b5822..9b5be21f9 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -391,6 +391,7 @@ "limited_account_hint.action": "Aj tak zobraziť profil", "limited_account_hint.title": "Tento profil bol skrytý správcami servera {domain}.", "link_preview.author": "Autor: {name}", + "link_preview.more_from_author": "Viac od {name}", "lists.account.add": "Pridať do zoznamu", "lists.account.remove": "Odstrániť zo zoznamu", "lists.delete": "Vymazať zoznam", @@ -411,6 +412,7 @@ "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálne deaktivovaný, pretože ste sa presunuli na {movedToAccount}.", "mute_modal.hide_from_notifications": "Ukryť z upozornení", "mute_modal.hide_options": "Skryť možnosti", + "mute_modal.indefinite": "Pokiaľ ich neodtíšim", "mute_modal.show_options": "Zobraziť možnosti", "mute_modal.title": "Stíšiť užívateľa?", "navigation_bar.about": "O tomto serveri", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index a8cce3202..46e6b384a 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -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", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index a1d478b7a..4e80c5103 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -415,6 +415,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", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index 37f156c28..1b9f8d969 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -26,8 +26,9 @@ "account.featured_tags.last_status_never": "کوئی مراسلہ نہیں", "account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز", "account.follow": "پیروی کریں", + "account.follow_back": "اکاؤنٹ کو فالو بیک ", "account.followers": "پیروکار", - "account.followers.empty": "\"ہنوز اس صارف کی کوئی پیروی نہیں کرتا\".", + "account.followers.empty": "ہنوز اس صارف کی کوئی پیروی نہیں کرتا.", "account.followers_counter": "{count, plural,one {{counter} پیروکار} other {{counter} پیروکار}}", "account.following": "فالو کر رہے ہیں", "account.following_counter": "{count, plural, one {{counter} پیروی کر رہے ہیں} other {{counter} پیروی کر رہے ہیں}}", @@ -46,6 +47,7 @@ "account.mute_notifications_short": "نوٹیفیکیشنز کو خاموش کریں", "account.mute_short": "خاموش", "account.muted": "خاموش کردہ", + "account.mutual": "میوچول اکاؤنٹ", "account.no_bio": "کوئی تفصیل نہیں دی گئی۔", "account.open_original_page": "اصل صفحہ کھولیں", "account.posts": "ٹوٹ", @@ -64,7 +66,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/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 0f8bcae6f..def950044 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.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": "删除列表", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 1d20034db..70b8d51df 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.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 {{count} 則嘟文}}", "lists.account.add": "新增至列表", "lists.account.remove": "自列表中移除", "lists.delete": "刪除列表", diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 4f36d85aa..73d0e6220 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -903,9 +903,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; @@ -1411,10 +1417,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 { @@ -4129,6 +4140,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%; } @@ -8784,43 +8802,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 { @@ -8885,14 +8940,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 { @@ -8920,6 +8975,9 @@ noscript { } &__description { + font-size: 15px; + line-height: 22px; + color: $darker-text-color; margin-bottom: 20px; } @@ -9891,14 +9949,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); } } @@ -10229,6 +10287,7 @@ noscript { } .more-from-author { + box-sizing: border-box; font-size: 14px; color: $darker-text-color; background: var(--surface-background-color); diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index f6ec44fb5..26bb2bee1 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 58b9dd9b6..2848a42b3 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 000000000..bf8f5ae25 --- /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 4e9585dd1..6e06f988a 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/flag.rb b/app/lib/activitypub/activity/flag.rb index 68ee43d0e..b7a412485 100644 --- a/app/lib/activitypub/activity/flag.rb +++ b/app/lib/activitypub/activity/flag.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::Activity::Flag < ActivityPub::Activity + COMMENT_SIZE_LIMIT = 5000 + def perform return if skip_reports? @@ -38,6 +40,6 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity end def report_comment - (@json['content'] || '')[0...5000] + (@json['content'] || '')[0...COMMENT_SIZE_LIMIT] end end diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index 97cdaf589..a260a66e2 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].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 @@ -71,6 +71,45 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim nil end + def libvips_version + return unless Rails.configuration.x.use_vips + + { + key: 'libvips', + human_key: 'libvips', + value: Vips.version_string, + human_value: Vips.version_string, + } + 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/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb index 3d081fdd9..746780ee7 100644 --- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb +++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb @@ -43,7 +43,7 @@ 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 diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb index 378c6754d..0693d5a64 100644 --- a/app/lib/admin/metrics/measure/instance_followers_measure.rb +++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb @@ -44,7 +44,7 @@ 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 diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb index e213348fb..90d381935 100644 --- a/app/lib/admin/metrics/measure/instance_follows_measure.rb +++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb @@ -44,7 +44,7 @@ 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 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 1d2dbbe41..89f8b4149 100644 --- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb +++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb @@ -53,7 +53,7 @@ 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 diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb index 9da3d53e3..5f58387a6 100644 --- a/app/lib/admin/metrics/measure/instance_reports_measure.rb +++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb @@ -44,7 +44,7 @@ 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 diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb index b918a30a5..5873c6e71 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 diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb index 6837c14c8..32057154d 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 c395c4634..47de38bbe 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 969065f73..5969a96ba 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(timestamp :start_at, :end_at, '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 780db75a1..ecfd779c8 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_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb index f273d739d..5db107606 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/feed_manager.rb b/app/lib/feed_manager.rb index 95a687fa4..1fb224a13 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -420,10 +420,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 list feed diff --git a/app/lib/scope_transformer.rb b/app/lib/scope_transformer.rb index adcb711f8..7dda70922 100644 --- a/app/lib/scope_transformer.rb +++ b/app/lib/scope_transformer.rb @@ -11,6 +11,9 @@ class ScopeTransformer < Parslet::Transform @namespace = scope[:namespace]&.to_s @access = scope[:access] ? [scope[:access].to_s] : DEFAULT_ACCESS.dup @term = scope[:term]&.to_s || DEFAULT_TERM + + # # override for profile scope which is read only + @access = %w(read) if @term == 'profile' end def key diff --git a/app/lib/vacuum/access_tokens_vacuum.rb b/app/lib/vacuum/access_tokens_vacuum.rb index a224f6d63..281ae22bf 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/lib/video_metadata_extractor.rb b/app/lib/video_metadata_extractor.rb index df5409375..215576625 100644 --- a/app/lib/video_metadata_extractor.rb +++ b/app/lib/video_metadata_extractor.rb @@ -41,8 +41,8 @@ class VideoMetadataExtractor @colorspace = video_stream[:pix_fmt] @width = video_stream[:width] @height = video_stream[:height] - @frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate]) - @r_frame_rate = video_stream[:r_frame_rate] == '0/0' ? nil : Rational(video_stream[:r_frame_rate]) + @frame_rate = parse_framerate(video_stream[:avg_frame_rate]) + @r_frame_rate = parse_framerate(video_stream[:r_frame_rate]) # For some video streams the frame_rate reported by `ffprobe` will be 0/0, but for these streams we # should use `r_frame_rate` instead. Video screencast generated by Gnome Screencast have this issue. @frame_rate ||= @r_frame_rate @@ -55,4 +55,10 @@ class VideoMetadataExtractor @invalid = true if @metadata.key?(:error) end + + def parse_framerate(raw) + Rational(raw) + rescue ZeroDivisionError + nil + end end diff --git a/app/models/appeal.rb b/app/models/appeal.rb index 395056b76..fafa75e69 100644 --- a/app/models/appeal.rb +++ b/app/models/appeal.rb @@ -18,6 +18,8 @@ class Appeal < ApplicationRecord MAX_STRIKE_AGE = 20.days + TEXT_LENGTH_LIMIT = 2_000 + belongs_to :account belongs_to :strike, class_name: 'AccountWarning', foreign_key: 'account_warning_id', inverse_of: :appeal @@ -26,7 +28,7 @@ class Appeal < ApplicationRecord belongs_to :rejected_by_account end - validates :text, presence: true, length: { maximum: 2_000 } + validates :text, presence: true, length: { maximum: TEXT_LENGTH_LIMIT } validates :account_warning_id, uniqueness: true validate :validate_time_frame, on: :create diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index f457f5822..a83e178fc 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -69,7 +69,7 @@ module Attachmentable original_extension = Paperclip::Interpolations.extension(attachment, :original) proper_extension = extensions_for_mime_type.first.to_s extension = extensions_for_mime_type.include?(original_extension) ? original_extension : proper_extension - extension = 'jpeg' if extension == 'jpe' + extension = 'jpeg' if ['jpe', 'jfif'].include?(extension) extension end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 1c9b44395..31ba91ad0 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -39,7 +39,7 @@ class CustomEmoji < ApplicationRecord has_one :local_counterpart, -> { where(domain: nil) }, class_name: 'CustomEmoji', primary_key: :shortcode, foreign_key: :shortcode, inverse_of: false, dependent: nil - has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' } }, validate_media_type: false + has_attached_file :image, styles: { static: { format: 'png', convert_options: '-coalesce +profile "!icc,*" +set date:modify +set date:create +set date:timestamp', file_geometry_parser: FastGeometryParser } }, validate_media_type: false, processors: [:lazy_thumbnail] normalizes :domain, with: ->(domain) { domain.downcase } diff --git a/app/models/link_feed.rb b/app/models/link_feed.rb new file mode 100644 index 000000000..32efb331b --- /dev/null +++ b/app/models/link_feed.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class LinkFeed < PublicFeed + # @param [PreviewCard] preview_card + # @param [Account] account + # @param [Hash] options + def initialize(preview_card, account, options = {}) + @preview_card = preview_card + super(account, options) + end + + # @param [Integer] limit + # @param [Integer] max_id + # @param [Integer] since_id + # @param [Integer] min_id + # @return [Array] + def get(limit, max_id = nil, since_id = nil, min_id = nil) + scope = public_scope + + scope.merge!(discoverable) + scope.merge!(attached_to_preview_card) + + scope.to_a_paginated_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id) + end + + private + + def attached_to_preview_card + Status.joins(:preview_cards_status).where(preview_cards_status: { preview_card_id: @preview_card.id }) + end + + def discoverable + Account.discoverable + end +end diff --git a/app/models/mention.rb b/app/models/mention.rb index 2348b2905..af9bb7378 100644 --- a/app/models/mention.rb +++ b/app/models/mention.rb @@ -5,10 +5,10 @@ # Table name: mentions # # id :bigint(8) not null, primary key -# status_id :bigint(8) +# status_id :bigint(8) not null # created_at :datetime not null # updated_at :datetime not null -# account_id :bigint(8) +# account_id :bigint(8) not null # silent :boolean default(FALSE), not null # diff --git a/app/models/notification.rb b/app/models/notification.rb index 7cbab4dc8..01abe74f5 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -13,6 +13,7 @@ # from_account_id :bigint(8) not null # type :string # filtered :boolean default(FALSE), not null +# group_key :string # class Notification < ApplicationRecord @@ -136,6 +137,69 @@ class Notification < ApplicationRecord end end + # This returns notifications from the request page, but with at most one notification per group. + # Notifications that have no `group_key` each count as a separate group. + def paginate_groups_by_max_id(limit, max_id: nil, since_id: nil) + query = reorder(id: :desc) + query = query.where(id: ...max_id) if max_id.present? + query = query.where(id: (since_id + 1)...) if since_id.present? + + unscoped + .with_recursive( + grouped_notifications: [ + query + .select('notifications.*', "ARRAY[COALESCE(notifications.group_key, 'ungrouped-' || notifications.id)] groups") + .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))") + .limit(1), + ] + ) + .from('grouped_notifications AS notifications') + .order(id: :desc) + .limit(limit) + end + + # Differs from :paginate_groups_by_max_id in that it gives the results immediately following min_id, + # whereas since_id gives the items with largest id, but with since_id as a cutoff. + # Results will be in ascending order by id. + def paginate_groups_by_min_id(limit, max_id: nil, min_id: nil) + query = reorder(id: :asc) + query = query.where(id: (min_id + 1)...) if min_id.present? + query = query.where(id: ...max_id) if max_id.present? + + unscoped + .with_recursive( + grouped_notifications: [ + query + .select('notifications.*', "ARRAY[COALESCE(notifications.group_key, 'ungrouped-' || notifications.id)] groups") + .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))") + .limit(1), + ] + ) + .from('grouped_notifications AS notifications') + .order(id: :asc) + .limit(limit) + end + + def to_a_grouped_paginated_by_id(limit, options = {}) + if options[:min_id].present? + paginate_groups_by_min_id(limit, min_id: options[:min_id], max_id: options[:max_id]).reverse + else + paginate_groups_by_max_id(limit, max_id: options[:max_id], since_id: options[:since_id]).to_a + end + end + def preload_cache_collection_target_statuses(notifications, &_block) notifications.group_by(&:type).each do |type, grouped_notifications| associations = TARGET_STATUS_INCLUDES_BY_TYPE[type] diff --git a/app/models/notification_group.rb b/app/models/notification_group.rb new file mode 100644 index 000000000..43612d49b --- /dev/null +++ b/app/models/notification_group.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class NotificationGroup < ActiveModelSerializers::Model + attributes :group_key, :sample_accounts, :notifications_count, :notification, :most_recent_notification_id + + def self.from_notification(notification) + if notification.group_key.present? + # TODO: caching and preloading + most_recent_notifications = notification.account.notifications.where(group_key: notification.group_key).order(id: :desc).take(3) + most_recent_id = most_recent_notifications.first.id + sample_accounts = most_recent_notifications.map(&:from_account) + notifications_count = notification.account.notifications.where(group_key: notification.group_key).count + else + most_recent_id = notification.id + sample_accounts = [notification.from_account] + notifications_count = 1 + end + + NotificationGroup.new( + notification: notification, + group_key: notification.group_key || "ungrouped-#{notification.id}", + sample_accounts: sample_accounts, + notifications_count: notifications_count, + most_recent_notification_id: most_recent_id + ) + end + + delegate :type, + :target_status, + :report, + :account_relationship_severance_event, + to: :notification, prefix: false +end diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 11fdd9d88..cbfc39378 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -57,7 +57,11 @@ class PreviewCard < ApplicationRecord has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy belongs_to :author_account, class_name: 'Account', optional: true - has_attached_file :image, processors: [:thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false + has_attached_file :image, + processors: [Rails.configuration.x.use_vips ? :lazy_thumbnail : :thumbnail, :blurhash_transcoder], + styles: ->(f) { image_styles(f) }, + convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, + validate_media_type: false validates :url, presence: true, uniqueness: true, url: true validates_attachment_content_type :image, content_type: IMAGE_MIME_TYPES diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index a3a2ec3f0..ddfd08146 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -21,10 +21,12 @@ class Web::PushSubscription < ApplicationRecord has_one :session_activation, foreign_key: 'web_push_subscription_id', inverse_of: :web_push_subscription, dependent: nil - validates :endpoint, presence: true + validates :endpoint, presence: true, url: true validates :key_p256dh, presence: true validates :key_auth, presence: true + validates_with WebPushKeyValidator + delegate :locale, to: :associated_user def encrypt(payload) @@ -73,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 4fa31ece5..d7ed1b9d4 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/rest/notification_group_serializer.rb b/app/serializers/rest/notification_group_serializer.rb new file mode 100644 index 000000000..9aa5663f4 --- /dev/null +++ b/app/serializers/rest/notification_group_serializer.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +class REST::NotificationGroupSerializer < ActiveModel::Serializer + attributes :group_key, :notifications_count, :type, :most_recent_notification_id + + attribute :page_min_id, if: :paginated? + attribute :page_max_id, if: :paginated? + attribute :latest_page_notification_at, if: :paginated? + + has_many :sample_accounts, serializer: REST::AccountSerializer + belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer + belongs_to :report, if: :report_type?, serializer: REST::ReportSerializer + belongs_to :account_relationship_severance_event, key: :event, if: :relationship_severance_event?, serializer: REST::AccountRelationshipSeveranceEventSerializer + belongs_to :account_warning, key: :moderation_warning, if: :moderation_warning_event?, serializer: REST::AccountWarningSerializer + + def status_type? + [:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type) + end + + def report_type? + object.type == :'admin.report' + end + + def relationship_severance_event? + object.type == :severed_relationships + end + + def moderation_warning_event? + object.type == :moderation_warning + end + + def page_min_id + range = instance_options[:group_metadata][object.group_key] + range.present? ? range[:min_id].to_s : object.notification.id.to_s + end + + def page_max_id + range = instance_options[:group_metadata][object.group_key] + range.present? ? range[:max_id].to_s : object.notification.id.to_s + end + + def latest_page_notification_at + range = instance_options[:group_metadata][object.group_key] + range.present? ? range[:latest_notification_at] : object.notification.created_at + end + + def paginated? + !instance_options[:group_metadata].nil? + end +end diff --git a/app/services/backup_service.rb b/app/services/backup_service.rb index 886bab1eb..1e9018437 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/notify_service.rb b/app/services/notify_service.rb index 1f01c2d48..d69b5af14 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -3,6 +3,9 @@ class NotifyService < BaseService include Redisable + MAXIMUM_GROUP_SPAN_HOURS = 12 + MAXIMUM_GROUP_GAP_TIME = 4.hours.to_i + NON_EMAIL_TYPES = %i( admin.report admin.sign_up @@ -183,6 +186,7 @@ class NotifyService < BaseService return if dismiss? @notification.filtered = filter? + @notification.group_key = notification_group_key @notification.save! # It's possible the underlying activity has been deleted @@ -202,6 +206,24 @@ class NotifyService < BaseService private + def notification_group_key + return nil if @notification.filtered || %i(favourite reblog).exclude?(@notification.type) + + type_prefix = "#{@notification.type}-#{@notification.target_status.id}" + redis_key = "notif-group/#{@recipient.id}/#{type_prefix}" + hour_bucket = @notification.activity.created_at.utc.to_i / 1.hour.to_i + + # Reuse previous group if it does not span too large an amount of time + previous_bucket = redis.get(redis_key).to_i + hour_bucket = previous_bucket if hour_bucket < previous_bucket + MAXIMUM_GROUP_SPAN_HOURS + + # Do not track groups past a given inactivity time + # We do not concern ourselves with race conditions since we use hour buckets + redis.set(redis_key, hour_bucket, ex: MAXIMUM_GROUP_GAP_TIME) + + "#{type_prefix}-#{hour_bucket}" + end + def dismiss? DismissCondition.new(@notification).dismiss? end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 83a931817..8b18ce038 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -171,7 +171,7 @@ class PostStatusService < BaseService end def scheduled_in_the_past? - @scheduled_at.present? && @scheduled_at <= Time.now.utc + MIN_SCHEDULE_OFFSET + @scheduled_at.present? && @scheduled_at <= Time.now.utc end def bump_potential_friendship! diff --git a/app/validators/web_push_key_validator.rb b/app/validators/web_push_key_validator.rb new file mode 100644 index 000000000..a8ad5c9c6 --- /dev/null +++ b/app/validators/web_push_key_validator.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class WebPushKeyValidator < ActiveModel::Validator + def validate(subscription) + begin + Webpush::Encryption.encrypt('validation_test', subscription.key_p256dh, subscription.key_auth) + rescue ArgumentError, OpenSSL::PKey::EC::Point::Error + subscription.errors.add(:base, I18n.t('crypto.errors.invalid_key')) + end + end +end diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 01b072938..9dd4f0e4e 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 c4929cc42..c02c8f0ad 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 e87dd4128..82fec554b 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 684735c20..4fae6557a 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 52ffc3d46..2b0d2c5eb 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 c8ad653a8..62cd31572 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 7e43b4c53..b5f084f88 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 9eba6c68f..207d23aee 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 c2daefb42..83ffd139d 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 da9ac8931..5fb540931 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 7a737d4f7..79c444453 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 e2a9868aa..dae2c1aa5 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 842aa5159..ca1edea0f 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 a41a6332d..770d972d9 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -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 c503b2d39..647c24b1e 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 706c60701..b43b8dfff 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 66151ad31..4713f8c2a 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 655955f7f..3a44cf3a7 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 964d0e63e..035ac3a86 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/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index 48350f478..07d6c1af5 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -3,7 +3,7 @@ - if self_destruct? .flash-message.warning - = t('auth.status.self_destruct', domain: ENV.fetch('LOCAL_DOMAIN')) + = t('auth.status.self_destruct', domain: Rails.configuration.x.local_domain) - else = render partial: 'status', locals: { user: @user, strikes: @strikes } diff --git a/app/views/errors/self_destruct.html.haml b/app/views/errors/self_destruct.html.haml index 09b17a5a9..b9ff48f68 100644 --- a/app/views/errors/self_destruct.html.haml +++ b/app/views/errors/self_destruct.html.haml @@ -3,7 +3,7 @@ .simple_form %h1.title= t('self_destruct.title') - %p.lead= t('self_destruct.lead_html', domain: ENV.fetch('LOCAL_DOMAIN')) + %p.lead= t('self_destruct.lead_html', domain: Rails.configuration.x.local_domain) .form-footer %ul.no-list diff --git a/app/views/filters/statuses/index.html.haml b/app/views/filters/statuses/index.html.haml index eaa39e170..915ec59ca 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 5957d1dbf..a73287959 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -29,7 +29,7 @@ = stylesheet_pack_tag 'common', media: 'all', crossorigin: 'anonymous' = theme_style_tags current_theme -# Needed for the wicg-inert polyfill. It needs to be on it's own