diff --git a/.bundler-audit.yml b/.bundler-audit.yml deleted file mode 100644 index f84ec8087..000000000 --- a/.bundler-audit.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -ignore: - - CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml deleted file mode 100644 index 22f51f7bd..000000000 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Bug Report -description: If something isn't working as expected -labels: [bug] -body: - - type: markdown - attributes: - value: | - Make sure that you are submitting a new bug that was not previously reported or already fixed. - - Please use a concise and distinct title for the issue. - - type: textarea - attributes: - label: Steps to reproduce the problem - description: What were you trying to do? - value: | - 1. - 2. - 3. - ... - validations: - required: true - - type: input - attributes: - label: Expected behaviour - description: What should have happened? - validations: - required: true - - type: input - attributes: - label: Actual behaviour - description: What happened? - validations: - required: true - - type: textarea - attributes: - label: Detailed description - validations: - required: false - - type: textarea - attributes: - label: Specifications - description: | - What version or commit hash of Mastodon did you find this bug in? - - If a front-end issue, what browser and operating systems were you using? - placeholder: | - Mastodon 3.5.3 (or Edge) - Ruby 2.7.6 (or v3.1.2) - Node.js 16.18.0 - - Google Chrome 106.0.5249.119 - Firefox 105.0.3 - - etc... - validations: - required: true diff --git a/.github/ISSUE_TEMPLATE/1.web_bug_report.yml b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml new file mode 100644 index 000000000..20e27d103 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.web_bug_report.yml @@ -0,0 +1,76 @@ +name: Bug Report (Web Interface) +description: If you are using Mastodon's web interface and something is not working as expected +labels: [bug, 'status/to triage', 'area/web interface'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: true + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: true + - type: input + attributes: + label: Browser name and version + description: | + What browser are you using when getting this bug? Please specify the version as well. + placeholder: Firefox 105.0.3 + validations: + required: true + - type: input + attributes: + label: Operating system + description: | + What OS are you running? Please specify the version as well. + placeholder: macOS 13.4.1 + validations: + required: true + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have. This can include the full error log, inspector's output… + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2.server_bug_report.yml b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml new file mode 100644 index 000000000..49d5f5720 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2.server_bug_report.yml @@ -0,0 +1,65 @@ +name: Bug Report (server / API) +description: | + If something is not working as expected, but is not from using the web interface. +labels: [bug, 'status/to triage'] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: true + - type: input + attributes: + label: Expected behaviour + description: What should have happened? + validations: + required: true + - type: input + attributes: + label: Actual behaviour + description: What happened? + validations: + required: true + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: input + attributes: + label: Mastodon instance + description: The address of the Mastodon instance where you experienced the issue + placeholder: mastodon.social + validations: + required: false + - type: input + attributes: + label: Mastodon version + description: | + This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` + placeholder: v4.1.2 + validations: + required: false + - type: textarea + attributes: + label: Technical details + description: | + Any additional technical details you may have, like logs or error traces + value: | + If this is happening on your own Mastodon server, please fill out those: + - Ruby version: (from `ruby --version`, eg. v3.1.2) + - Node.js version: (from `node --version`, eg. v18.16.0) + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/3.feature_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/2.feature_request.yml rename to .github/ISSUE_TEMPLATE/3.feature_request.yml diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 78530d65b..3131083ca 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -1,20 +1,21 @@ { $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ - 'config:base', - ':dependencyDashboard', + 'config:recommended', ':labels(dependencies)', ':maintainLockFilesMonthly', // update non-direct dependencies monthly - ':prConcurrentLimit10', // only 10 open PRs at the same time + ':prConcurrentLimitNone', // Remove limit for open PRs at any time. + ':prHourlyLimit2' // Rate limit PR creation to a maximum of two per hour. ], - stabilityDays: 3, // Wait 3 days after the package has been published before upgrading it + minimumReleaseAge: "3", // Wait 3 days after the package has been published before upgrading it // packageRules order is important, they are applied from top to bottom and are merged, // meaning the most important ones must be at the bottom, for example grouping rules // If we do not want a package to be grouped with others, we need to set its groupName // to `null` after any other rule set it to something. + dependencyDashboardHeader: 'This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more. Before approving any upgrade: read the description and comments in the [`renovate.json5` file](https://github.com/mastodon/mastodon/blob/main/.github/renovate.json5).', packageRules: [ { - // Ignore major version bumps for these node packages + // Require Dependency Dashboard Approval for major version bumps of these node packages matchManagers: ['npm'], matchPackageNames: [ 'tesseract.js', // Requires code changes @@ -41,10 +42,10 @@ 'react-router-dom', ], matchUpdateTypes: ['major'], - enabled: false, + "dependencyDashboardApproval": true }, { - // Ignore major version bumps for these Ruby packages + // Require Dependency Dashboard Approval for major version bumps of these Ruby packages matchManagers: ['bundler'], matchPackageNames: [ 'rack', // Needs to be synced with Rails version @@ -55,7 +56,7 @@ 'redis', // Requires manual upgrade and sync with Sidekiq version ], matchUpdateTypes: ['major'], - enabled: false, + "dependencyDashboardApproval": true }, { // Update Github Actions and Docker images weekly @@ -63,25 +64,25 @@ extends: ['schedule:weekly'], }, { - // Ignore major & minor bumps for the ruby image, this needs to be synced with .ruby-version + // Require Dependency Dashboard Approval for major & minor bumps for the ruby image, this needs to be synced with .ruby-version matchManagers: ['dockerfile'], matchPackageNames: ['moritzheiber/ruby-jemalloc'], matchUpdateTypes: ['minor', 'major'], - enabled: false, + "dependencyDashboardApproval": true }, { - // Ignore major bump for the node image, this needs to be synced with .nvmrc + // Require Dependency Dashboard Approval for major bumps for the node image, this needs to be synced with .nvmrc matchManagers: ['dockerfile'], matchPackageNames: ['node'], matchUpdateTypes: ['major'], - enabled: false, + "dependencyDashboardApproval": true }, { - // Ignore major postgres bumps in the docker-compose file, as those break dev environments + // Require Dependency Dashboard Approval for major postgres bumps in the docker-compose file, as those break dev environments matchManagers: ['docker-compose'], matchPackageNames: ['postgres'], matchUpdateTypes: ['major'], - enabled: false, + "dependencyDashboardApproval": true }, { // Update devDependencies every week, with one grouped PR diff --git a/.github/workflows/build-container-image.yml b/.github/workflows/build-container-image.yml new file mode 100644 index 000000000..1b15d1988 --- /dev/null +++ b/.github/workflows/build-container-image.yml @@ -0,0 +1,94 @@ +on: + workflow_call: + inputs: + platforms: + required: true + type: string + use_native_arm64_builder: + type: boolean + push_to_images: + type: string + version_suffix: + type: string + flavor: + type: string + tags: + type: string + labels: + type: string + +jobs: + build-image: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: docker/setup-qemu-action@v2 + if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder + + - uses: docker/setup-buildx-action@v2 + id: buildx + if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }} + + - name: Start a local Docker Builder + if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') + run: | + docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234 + + - uses: docker/setup-buildx-action@v2 + id: buildx-native + if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64') + with: + driver: remote + endpoint: tcp://localhost:1234 + platforms: linux/amd64 + append: | + - endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865 + platforms: linux/arm64 + name: mastodon-docker-builder-arm64-01 + driver-opts: + - servername=mastodon-docker-builder-arm64-01 + env: + BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }} + BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }} + BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }} + + - name: Log in to Docker Hub + if: contains(inputs.push_to_images, 'tootsuite') + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to the Github Container registry + if: contains(inputs.push_to_images, 'ghcr.io') + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: docker/metadata-action@v4 + id: meta + if: ${{ inputs.push_to_images != '' }} + with: + images: ${{ inputs.push_to_images }} + # Only tag with latest when ran against the latest stable branch + # This needs to be updated after each minor version release + flavor: ${{ inputs.flavor }} + tags: ${{ inputs.tags }} + labels: ${{ inputs.labels }} + + - uses: docker/build-push-action@v4 + with: + context: . + build-args: MASTODON_VERSION_SUFFIX=${{ inputs.version_suffix }} + platforms: ${{ inputs.platforms }} + provenance: false + builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }} + push: ${{ inputs.push_to_images != '' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml deleted file mode 100644 index c01914a97..000000000 --- a/.github/workflows/build-image.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Build container image -on: - workflow_dispatch: - push: - branches: - - 'main' - tags: - - '*' - pull_request: - paths: - - .github/workflows/build-image.yml - - Dockerfile -permissions: - contents: read - packages: write - -jobs: - build-image: - runs-on: ubuntu-latest - - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - - steps: - - uses: actions/checkout@v3 - - uses: hadolint/hadolint-action@v3.1.0 - - uses: docker/setup-qemu-action@v2 - - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' - - - name: Log in to the Github Container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' - - - uses: docker/metadata-action@v4 - id: meta - with: - images: | - tootsuite/mastodon - ghcr.io/mastodon/mastodon - # Only tag with latest when ran against the latest stable branch - # This needs to be updated after each minor version release - flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }} - tags: | - type=edge,branch=main - type=pep440,pattern={{raw}} - type=pep440,pattern=v{{major}}.{{minor}} - type=ref,event=pr - - - name: Generate version suffix - id: version_vars - if: github.repository == 'mastodon/mastodon' && github.event_name == 'push' && github.ref_name == 'main' - run: | - echo mastodon_version_suffix=+edge-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT - - - uses: docker/build-push-action@v4 - with: - context: . - build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} - platforms: linux/amd64,linux/arm64 - provenance: false - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/build-nightly.yml b/.github/workflows/build-nightly.yml index f07f7447c..b700790d8 100644 --- a/.github/workflows/build-nightly.yml +++ b/.github/workflows/build-nightly.yml @@ -3,58 +3,40 @@ on: workflow_dispatch: schedule: - cron: '0 2 * * *' # run at 2 AM UTC + permissions: contents: read packages: write jobs: - build-nightly-image: + compute-suffix: runs-on: ubuntu-latest - - concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - steps: - - uses: actions/checkout@v3 - - uses: hadolint/hadolint-action@v3.1.0 - - uses: docker/setup-qemu-action@v2 - - uses: docker/setup-buildx-action@v2 - - - name: Log in to the Github Container registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: docker/metadata-action@v4 - id: meta - with: - images: | - ghcr.io/mastodon/mastodon - flavor: | - latest=auto - tags: | - type=raw,value=nightly - type=schedule,pattern=nightly-{{date 'YYYY-MM-DD' tz='Etc/UTC'}} - labels: | - org.opencontainers.image.description=Nightly build image used for testing purposes - - - name: Generate version suffix - id: version_vars + - id: version_vars + env: + TZ: Etc/UTC run: | - echo mastodon_version_suffix=+nightly-$(date +'%Y%m%d') >> $GITHUB_OUTPUT + echo mastodon_version_suffix=nightly-$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT + outputs: + suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }} - - uses: docker/build-push-action@v4 - with: - context: . - build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} - platforms: linux/amd64,linux/arm64 - provenance: false - builder: ${{ steps.buildx.outputs.name }} - push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max + build-image: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + tootsuite/mastodon + ghcr.io/mastodon/mastodon + # The `+` is important here, result will be v4.1.2+nightly-2022-03-05 + version_suffix: +${{ needs.compute-suffix.outputs.suffix }} + labels: | + org.opencontainers.image.description=Nightly build image used for testing purposes + flavor: | + latest=auto + tags: | + type=raw,value=edge + type=raw,value=nightly + type=schedule,pattern=${{ needs.compute-suffix.outputs.suffix }} + secrets: inherit diff --git a/.github/workflows/build-push-pr.yml b/.github/workflows/build-push-pr.yml new file mode 100644 index 000000000..2d2026128 --- /dev/null +++ b/.github/workflows/build-push-pr.yml @@ -0,0 +1,41 @@ +name: Build container image for PR +on: + pull_request: + types: [labeled, synchronize, reopened, ready_for_review, opened] + +permissions: + contents: read + packages: write + +jobs: + compute-suffix: + runs-on: ubuntu-latest + # This is only allowed to run if: + # - the PR branch is in the `mastodon/mastodon` repository + # - the PR is not a draft + # - the PR has the "build-image" label + if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'build-image') }} + steps: + # Repository needs to be cloned so `git rev-parse` below works + - name: Clone repository + uses: actions/checkout@v3 + - id: version_vars + run: | + echo mastodon_version_suffix=+pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT + outputs: + suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }} + + build-image: + needs: compute-suffix + uses: ./.github/workflows/build-container-image.yml + with: + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + ghcr.io/mastodon/mastodon + version_suffix: ${{ needs.compute-suffix.outputs.suffix }} + flavor: | + latest=auto + tags: | + type=ref,event=pr + secrets: inherit diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml new file mode 100644 index 000000000..b40817468 --- /dev/null +++ b/.github/workflows/build-releases.yml @@ -0,0 +1,25 @@ +name: Build container release images +on: + push: + tags: + - '*' + +permissions: + contents: read + packages: write + +jobs: + build-image: + uses: ./.github/workflows/build-container-image.yml + with: + platforms: linux/amd64,linux/arm64 + use_native_arm64_builder: true + push_to_images: | + tootsuite/mastodon + ghcr.io/mastodon/mastodon + flavor: | + latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }} + tags: | + type=pep440,pattern={{raw}} + type=pep440,pattern=v{{major}}.{{minor}} + secrets: inherit diff --git a/.github/workflows/crowdin-download.yml b/.github/workflows/crowdin-download.yml new file mode 100644 index 000000000..f6b2579ce --- /dev/null +++ b/.github/workflows/crowdin-download.yml @@ -0,0 +1,76 @@ +name: Crowdin / Download translations +on: + schedule: + - cron: '17 4 * * *' # Every day + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + download-translations: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Increase Git http.postBuffer + # This is needed due to a bug in Ubuntu's cURL version? + # See https://github.com/orgs/community/discussions/55820 + run: | + git config --global http.version HTTP/1.1 + git config --global http.postBuffer 157286400 + + # Download the translation files from Crowdin + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: false + upload_translations: false + download_translations: true + crowdin_branch_name: main + push_translations: false + create_pull_request: false + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + # As the files are extracted from a Docker container, they belong to root:root + # We need to fix this before the next steps + - name: Fix file permissions + run: sudo chown -R runner:docker . + + # This is needed to run the normalize step + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run i18n normalize task + run: bundle exec i18n-tasks normalize + + # Create or update the pull request + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5.0.2 + with: + commit-message: 'New Crowdin translations' + title: 'New Crowdin Translations (automated)' + author: 'GitHub Actions ' + body: | + 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. + If you want to run the checks, then close and re-open it. + branch: i18n/crowdin/translations + base: main + labels: i18n diff --git a/.github/workflows/crowdin-upload.yml b/.github/workflows/crowdin-upload.yml new file mode 100644 index 000000000..8bc9f5193 --- /dev/null +++ b/.github/workflows/crowdin-upload.yml @@ -0,0 +1,35 @@ +name: Crowdin / Upload translations + +on: + push: + branches: + - main + paths: + - crowdin.yml + - app/javascript/mastodon/locales/en.json + - config/locales/en.yml + - config/locales/simple_form.en.yml + - config/locales/activerecord.en.yml + - config/locales/devise.en.yml + - config/locales/doorkeeper.en.yml + - .github/workflows/crowdin-upload.yml + +jobs: + upload-translations: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: true + upload_translations: false + download_translations: false + crowdin_branch_name: main + + env: + CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/test-image-build.yml b/.github/workflows/test-image-build.yml new file mode 100644 index 000000000..778e34177 --- /dev/null +++ b/.github/workflows/test-image-build.yml @@ -0,0 +1,21 @@ +name: Test container image build +on: + pull_request: + paths: + - .github/workflows/build-nightly.yml + - .github/workflows/build-push-pr.yml + - .github/workflows/build-releases.yml + - .github/workflows/test-image-build.yml + - Dockerfile +permissions: + contents: read + +jobs: + build-image: + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + + uses: ./.github/workflows/build-container-image.yml + with: + platforms: linux/amd64 # Testing only on native platform so it is performant diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 07cb1d41f..ff135867f 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -107,6 +107,10 @@ jobs: 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' CI_JOBS: ${{ matrix.ci_job }}/4 @@ -149,3 +153,100 @@ jobs: run: './bin/rails db:create db:schema:load db:seed' - run: bundle exec rake rspec_chunked + + test-e2e: + name: End to End testing + runs-on: ubuntu-latest + + needs: + - build + + services: + postgres: + image: postgres:14-alpine + 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: + DB_HOST: localhost + DB_USER: postgres + DB_PASS: postgres + DISABLE_SIMPLECOV: true + RAILS_ENV: test + BUNDLE_WITH: test + + strategy: + fail-fast: false + matrix: + ruby-version: + - '3.0' + - '3.1' + - '.ruby-version' + + steps: + - uses: actions/checkout@v3 + + - uses: actions/download-artifact@v3 + with: + path: './public' + name: ${{ github.sha }} + + - name: Update package index + run: sudo apt-get update + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version-file: '.nvmrc' + + - name: Install native Ruby dependencies + run: sudo apt-get install -y libicu-dev libidn11-dev + + - name: Install additional system dependencies + run: sudo apt-get install -y ffmpeg imagemagick + + - name: Set up bundler cache + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version}} + bundler-cache: true + + - run: yarn --frozen-lockfile + + - name: Load database schema + run: './bin/rails db:create db:schema:load db:seed' + + - run: bundle exec rake spec:system + + - name: Archive logs + uses: actions/upload-artifact@v3 + if: failure() + with: + name: e2e-logs-${{ matrix.ruby-version }} + path: log/ + + - name: Archive test screenshots + uses: actions/upload-artifact@v3 + if: failure() + with: + name: e2e-screenshots + path: tmp/screenshots/ diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml index dd5e8cf7f..6d2aa0641 100644 --- a/.haml-lint_todo.yml +++ b/.haml-lint_todo.yml @@ -1,13 +1,13 @@ # This configuration was generated by # `haml-lint --auto-gen-config` -# on 2023-07-17 15:30:11 -0400 using Haml-Lint version 0.48.0. +# on 2023-07-20 09:47:50 -0400 using Haml-Lint version 0.48.0. # The point is for the user to remove these configuration records # one by one as the lints are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of Haml-Lint, may require this file to be generated again. linters: - # Offense count: 959 + # Offense count: 951 LineLength: enabled: false @@ -15,7 +15,7 @@ linters: UnnecessaryStringOutput: enabled: false - # Offense count: 67 + # Offense count: 57 RuboCop: enabled: false @@ -26,20 +26,18 @@ linters: - 'app/views/admin/reports/show.html.haml' - 'app/views/disputes/strikes/show.html.haml' - # Offense count: 41 + # Offense count: 32 InstanceVariables: exclude: - 'app/views/admin/reports/_actions.html.haml' - 'app/views/admin/roles/_form.html.haml' - 'app/views/admin/webhooks/_form.html.haml' - - 'app/views/auth/registrations/_sessions.html.haml' - 'app/views/auth/registrations/_status.html.haml' - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' - 'app/views/authorize_interactions/_post_follow_actions.html.haml' - 'app/views/invites/_form.html.haml' - 'app/views/relationships/_account.html.haml' - 'app/views/shared/_og.html.haml' - - 'app/views/statuses/_status.html.haml' # Offense count: 3 IdNames: diff --git a/.rubocop.yml b/.rubocop.yml index 597dfcb9e..c8a433c72 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -38,14 +38,7 @@ Layout/FirstHashElementIndentation: # Reason: Currently disabled in .rubocop_todo.yml # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength Layout/LineLength: - AllowedPatterns: - # Allow comments to be long lines - - !ruby/regexp / \# .*$/ - - !ruby/regexp /^\# .*$/ - Exclude: - - 'lib/mastodon/cli/*.rb' - - db/*migrate/**/* - - db/seeds/**/* + Max: 320 # Default of 120 causes a duplicate entry in generated todo file # Reason: # https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier @@ -131,12 +124,6 @@ RSpec/FilePath: Exclude: - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder - 'spec/lib/sanitize_config_spec.rb' # namespaces usually have separate folder - - 'spec/controllers/concerns/account_controller_concern_spec.rb' # Concerns describe ApplicationController and don't fit naming - - 'spec/controllers/concerns/export_controller_concern_spec.rb' - - 'spec/controllers/concerns/localized_spec.rb' - - 'spec/controllers/concerns/rate_limit_headers_spec.rb' - - 'spec/controllers/concerns/signature_verification_spec.rb' - - 'spec/controllers/concerns/user_tracking_concern_spec.rb' # Reason: # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 960d548f0..92c7f9cdc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -39,6 +39,13 @@ Layout/LeadingCommentSpace: - 'config/application.rb' - 'config/initializers/omniauth.rb' +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. +# URISchemes: http, https +Layout/LineLength: + Exclude: + - 'app/models/account.rb' + # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforcedStyle. # SupportedStyles: require_no_space, require_space @@ -112,7 +119,6 @@ Lint/UselessAssignment: - 'config/initializers/omniauth.rb' - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb' - - 'spec/controllers/api/v1/bookmarks_controller_spec.rb' - 'spec/controllers/api/v1/favourites_controller_spec.rb' - 'spec/controllers/concerns/account_controller_concern_spec.rb' - 'spec/helpers/jsonld_helper_spec.rb' @@ -127,15 +133,9 @@ Lint/UselessAssignment: - 'spec/services/resolve_url_service_spec.rb' - 'spec/views/statuses/show.html.haml_spec.rb' -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: CheckForMethodsWithNoSideEffects. -Lint/Void: - Exclude: - - 'spec/services/resolve_account_service_spec.rb' - # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: - Max: 150 + Max: 146 # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: @@ -150,13 +150,6 @@ Metrics/CyclomaticComplexity: Metrics/PerceivedComplexity: Max: 27 -# Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. -# CheckDefinitionPathHierarchyRoots: lib, spec, test, src -# AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS -Naming/FileName: - Exclude: - - 'config/locales/sr-Latn.rb' - # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. # SupportedStyles: snake_case, normalcase, non_integer # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 @@ -171,14 +164,6 @@ Naming/VariableNumber: - 'spec/models/domain_block_spec.rb' - 'spec/models/user_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/UnfreezeString: - Exclude: - - 'app/lib/rss/builder.rb' - - 'app/lib/text_formatter.rb' - - 'app/validators/status_length_validator.rb' - - 'lib/tasks/mastodon.rake' - RSpec/AnyInstance: Exclude: - 'spec/controllers/activitypub/inboxes_controller_spec.rb' @@ -198,41 +183,6 @@ RSpec/AnyInstance: - 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb' -# This cop supports unsafe autocorrection (--autocorrect-all). -RSpec/EmptyExampleGroup: - Exclude: - - 'spec/helpers/admin/action_logs_helper_spec.rb' - - 'spec/models/account_alias_spec.rb' - - 'spec/models/account_deletion_request_spec.rb' - - 'spec/models/account_moderation_note_spec.rb' - - 'spec/models/announcement_mute_spec.rb' - - 'spec/models/announcement_reaction_spec.rb' - - 'spec/models/announcement_spec.rb' - - 'spec/models/backup_spec.rb' - - 'spec/models/conversation_mute_spec.rb' - - 'spec/models/custom_filter_keyword_spec.rb' - - 'spec/models/custom_filter_spec.rb' - - 'spec/models/device_spec.rb' - - 'spec/models/encrypted_message_spec.rb' - - 'spec/models/featured_tag_spec.rb' - - 'spec/models/follow_recommendation_suppression_spec.rb' - - 'spec/models/list_account_spec.rb' - - 'spec/models/list_spec.rb' - - 'spec/models/login_activity_spec.rb' - - 'spec/models/mute_spec.rb' - - 'spec/models/preview_card_spec.rb' - - 'spec/models/preview_card_trend_spec.rb' - - 'spec/models/relay_spec.rb' - - 'spec/models/scheduled_status_spec.rb' - - 'spec/models/status_stat_spec.rb' - - 'spec/models/status_trend_spec.rb' - - 'spec/models/system_key_spec.rb' - - 'spec/models/tag_follow_spec.rb' - - 'spec/models/unavailable_domain_spec.rb' - - 'spec/models/user_invite_request_spec.rb' - - 'spec/models/web/setting_spec.rb' - - 'spec/services/unmute_service_spec.rb' - # Configuration parameters: CountAsOne. RSpec/ExampleLength: Max: 22 @@ -285,7 +235,6 @@ RSpec/LetSetup: - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/filters/keywords_controller_spec.rb' - 'spec/controllers/api/v2/filters/statuses_controller_spec.rb' - - 'spec/controllers/api/v2/filters_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/sessions_controller_spec.rb' @@ -295,6 +244,7 @@ RSpec/LetSetup: - 'spec/controllers/oauth/tokens_controller_spec.rb' - 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/lib/activitypub/activity/delete_spec.rb' + - 'spec/lib/vacuum/applications_vacuum_spec.rb' - 'spec/lib/vacuum/preview_cards_vacuum_spec.rb' - 'spec/models/account_spec.rb' - 'spec/models/account_statuses_cleanup_policy_spec.rb' @@ -365,43 +315,6 @@ Rails/ApplicationController: Exclude: - 'app/controllers/health_controller.rb' -# Configuration parameters: Database, Include. -# SupportedDatabases: mysql, postgresql -# Include: db/**/*.rb -Rails/BulkChangeTable: - Exclude: - - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb' - - 'db/migrate/20160223162837_add_metadata_to_statuses.rb' - - 'db/migrate/20160305115639_add_devise_to_users.rb' - - 'db/migrate/20160314164231_add_owner_to_application.rb' - - 'db/migrate/20160926213048_remove_owner_from_application.rb' - - 'db/migrate/20161003142332_add_confirmable_to_users.rb' - - 'db/migrate/20170112154826_migrate_settings.rb' - - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb' - - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb' - - 'db/migrate/20170330021336_add_counter_caches.rb' - - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb' - - 'db/migrate/20170427011934_re_add_owner_to_application.rb' - - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb' - - 'db/migrate/20170624134742_add_description_to_session_activations.rb' - - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb' - - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb' - - 'db/migrate/20180812123222_change_relays_enabled.rb' - - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - - 'db/migrate/20190805123746_add_capabilities_to_tags.rb' - - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb' - - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb' - - 'db/migrate/20190901035623_add_max_score_to_tags.rb' - - 'db/migrate/20200417125749_add_storage_schema_version.rb' - - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb' - - 'db/migrate/20211112011713_add_language_to_preview_cards.rb' - - 'db/migrate/20211231080958_add_category_to_reports.rb' - - 'db/migrate/20220202200743_add_trendable_to_accounts.rb' - - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb' - - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb' - - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb' - - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb' - # Configuration parameters: Include. # Include: db/**/*.rb Rails/CreateTableWithTimestamps: @@ -676,7 +589,7 @@ Style/FetchEnvVar: - 'app/lib/translation_service.rb' - 'config/environments/development.rb' - 'config/environments/production.rb' - - 'config/initializers/2_whitelist_mode.rb' + - 'config/initializers/2_limited_federation_mode.rb' - 'config/initializers/blacklists.rb' - 'config/initializers/cache_buster.rb' - 'config/initializers/content_security_policy.rb' @@ -838,8 +751,6 @@ Style/RedundantConstantBase: Exclude: - 'config/environments/production.rb' - 'config/initializers/sidekiq.rb' - - 'config/locales/sr-Latn.rb' - - 'config/locales/sr.rb' # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: SafeForConstants. @@ -941,9 +852,3 @@ Style/WordArray: - 'config/initializers/cors.rb' - 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/models/form/import_spec.rb' - -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. -# URISchemes: http, https -Layout/LineLength: - Max: 701 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5383d426b..cfcc18836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,268 @@ All notable changes to this project will be documented in this file. +## [4.2.0] - UNRELEASED + +The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by [@danielmbrasil](https://github.com/danielmbrasil), [@mjankowski](https://github.com/mjankowski), [@nschonni](https://github.com/nschonni), [@renchap](https://github.com/renchap), and [@takayamaki](https://github.com/takayamaki). + +### Added + +- **Add role badges to the web interface** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25649), [Gargron](https://github.com/mastodon/mastodon/pull/26281)) +- **Add ability to pick domains to forward reports to using the `forward_to_domains` parameter in `POST /api/v1/reports`** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25866)) + The `forward_to_domains` REST API parameter is a list of strings. If it is empty or omitted, the previous behavior is maintained. + The `forward` parameter still needs to be set for `forward_to_domains` to be taken into account. + The forwarded-to domains can only include that of the original author and people being replied to. +- **Add forwarding of reported replies to servers being replied to** ([Gargron](https://github.com/mastodon/mastodon/pull/25341), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26189)) +- Add direct link to the Single-Sign On provider if there is only one sign up method available ([CSDUMMI](https://github.com/mastodon/mastodon/pull/26083), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26368)) +- **Add webhook templating** ([Gargron](https://github.com/mastodon/mastodon/pull/23289)) +- **Add webhooks for local `status.created`, `status.updated`, `account.updated` and `report.updated`** ([VyrCossont](https://github.com/mastodon/mastodon/pull/24133), [VyrCossont](https://github.com/mastodon/mastodon/pull/24243), [VyrCossont](https://github.com/mastodon/mastodon/pull/24211)) +- **Add exclusive lists** ([dariusk](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324)) +- **Add a confirmation screen when suspending a domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25144), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25603)) +- **Add support for importing lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25203), [mgmn](https://github.com/mastodon/mastodon/pull/26120), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26372)) +- **Add optional hCaptcha support** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25019), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25057), [Gargron](https://github.com/mastodon/mastodon/pull/25395), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26388)) +- **Add lines to threads in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24549), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24677), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24696), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24711), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24713), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24715), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24800), [teeerevor](https://github.com/mastodon/mastodon/pull/25706), [renchap](https://github.com/mastodon/mastodon/pull/25807)) +- **Add new onboarding flow to web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24619), [Gargron](https://github.com/mastodon/mastodon/pull/24646), [Gargron](https://github.com/mastodon/mastodon/pull/24705), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24872), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24883), [Gargron](https://github.com/mastodon/mastodon/pull/24954), [stevenjlm](https://github.com/mastodon/mastodon/pull/24959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25010), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25275), [Gargron](https://github.com/mastodon/mastodon/pull/25559), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25561)) +- Add `GET /api/v1/instance/languages` to REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24443)) +- Add primary key to `preview_cards_statuses` join table ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25243), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26384)) +- Add client-side timeout on resend confirmation button ([Gargron](https://github.com/mastodon/mastodon/pull/26300)) +- Add published date and author to news on the explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26155)) +- Add `lang` attribute to various UI components ([c960657](https://github.com/mastodon/mastodon/pull/23869), [c960657](https://github.com/mastodon/mastodon/pull/23891), [c960657](https://github.com/mastodon/mastodon/pull/26111), [c960657](https://github.com/mastodon/mastodon/pull/26149)) +- Add stricter protocol fields validation for accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25937)) +- Add support for Azure blob storage ([mistydemeo](https://github.com/mastodon/mastodon/pull/23607), [mistydemeo](https://github.com/mastodon/mastodon/pull/26080)) +- Add toast with option to open post after publishing in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25564), [Signez](https://github.com/mastodon/mastodon/pull/25919)) +- Add canonical link tags in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25715)) +- Add button to see results for polls in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25726)) +- Add at-symbol prepended to mention span title ([forsamori](https://github.com/mastodon/mastodon/pull/25684)) +- Add users index on `unconfirmed_email` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25672), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25702)) +- Add superapp index on `oauth_applications` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25670)) +- Add index to backups on `user_id` column ([mjankowski](https://github.com/mastodon/mastodon/pull/25647)) +- Add onboarding prompt when home feed too slow in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25267), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25556), [Gargron](https://github.com/mastodon/mastodon/pull/25579), [renchap](https://github.com/mastodon/mastodon/pull/25580), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25581), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25917)) +- Add `POST /api/v1/conversations/:id/unread` API endpoint to mark a conversation as unread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25509)) +- Add `translate="no"` to outgoing mentions and links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25524)) +- Add unsubscribe link and headers to e-mails ([Gargron](https://github.com/mastodon/mastodon/pull/25378), [c960657](https://github.com/mastodon/mastodon/pull/26085)) +- Add logging of websocket send errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25280)) +- Add time zone preference ([Gargron](https://github.com/mastodon/mastodon/pull/25342), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26025)) +- Add `legal` as report category ([Gargron](https://github.com/mastodon/mastodon/pull/23941), [renchap](https://github.com/mastodon/mastodon/pull/25400)) +- Add `data-nosnippet` so Google doesn't use trending posts in snippets for `/` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25279)) +- Add card with who invited you to join when displaying rules on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23475)) +- Add missing primary keys to `accounts_tags` and `statuses_tags` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25210)) +- Add support for custom sign-up URLs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25014), [renchap](https://github.com/mastodon/mastodon/pull/25108), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25190), [mgmn](https://github.com/mastodon/mastodon/pull/25531)) + This is set using `SSO_ACCOUNT_SIGN_UP` and reflected in the REST API by adding `registrations.sign_up_url` to the `/api/v2/instance` endpoint. +- Add polling and automatic redirection to `/start` on email confirmation ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25013)) +- Add ability to block sign-ups from IP using the CLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24870)) +- Add ALT badges to media that has alternative text in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24782), [c960657](https://github.com/mastodon/mastodon/pull/26166) +- Add ability to include accounts with pending follow requests in lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/19727), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24810)) +- Add trend management to admin API ([rrgeorge](https://github.com/mastodon/mastodon/pull/24257)) + - `POST /api/v1/admin/trends/statuses/:id/approve` + - `POST /api/v1/admin/trends/statuses/:id/reject` + - `POST /api/v1/admin/trends/links/:id/approve` + - `POST /api/v1/admin/trends/links/:id/reject` + - `POST /api/v1/admin/trends/tags/:id/approve` + - `POST /api/v1/admin/trends/tags/:id/reject` + - `GET /api/v1/admin/trends/links/publishers` + - `POST /api/v1/admin/trends/links/publishers/:id/approve` + - `POST /api/v1/admin/trends/links/publishers/:id/reject` +- Add user handle to notification mail recipient address ([HeitorMC](https://github.com/mastodon/mastodon/pull/24240)) +- Add progress indicator to sign-up flow ([Gargron](https://github.com/mastodon/mastodon/pull/24545)) +- Add client-side validation for taken username in sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24546)) +- Add `--approve` option to `tootctl accounts create` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24533)) +- Add “In Memoriam” banner back to profiles ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23591), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/23614)) + This adds the `memorial` attribute to the `Account` REST API entity. +- Add colour to follow button when hashtag is being followed ([c960657](https://github.com/mastodon/mastodon/pull/24361)) +- Add further explanations to the profile link verification instructions ([drzax](https://github.com/mastodon/mastodon/pull/19723)) +- Add a link to Identity provider's account settings from the account settings ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24628)) +- Add support for streaming server to connect to postgres with self-signed certs through the `sslmode` URL parameter ([ramuuns](https://github.com/mastodon/mastodon/pull/21431)) +- Add support for specifying S3 storage classes through the `S3_STORAGE_CLASS` environment variable ([hyl](https://github.com/mastodon/mastodon/pull/22480)) +- Add support for incoming rich text ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23913)) +- Add support for Ruby 3.2 ([tenderlove](https://github.com/mastodon/mastodon/pull/22928), [casperisfine](https://github.com/mastodon/mastodon/pull/24142), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24202)) +- Add API parameter to safeguard unexpected mentions in new posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/18350)) + +### Changed + +- **Change reblogs to be excluded from "Posts and replies" tab in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26302)) +- **Change interaction modal in web interface** ([Gargron, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26075), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26269), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26268), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26267)) +- **Change design of link previews in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/26136), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26151), [Gargron](https://github.com/mastodon/mastodon/pull/26153), [Gargron](https://github.com/mastodon/mastodon/pull/26250), [Gargron](https://github.com/mastodon/mastodon/pull/26287), [Gargron](https://github.com/mastodon/mastodon/pull/26286), [c960657](https://github.com/mastodon/mastodon/pull/26184)) +- **Change "direct message" nomenclature to "private mention" in web UI** ([Gargron](https://github.com/mastodon/mastodon/pull/24248)) +- **Change translation feature to cover Content Warnings, poll options and media descriptions** ([c960657](https://github.com/mastodon/mastodon/pull/24175), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25251), [c960657](https://github.com/mastodon/mastodon/pull/26168)) +- **Change account search to match by text when opted-in** ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25599), [Gargron](https://github.com/mastodon/mastodon/pull/26378)) +- **Change import feature to be clearer, less error-prone and more reliable** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21054), [mgmn](https://github.com/mastodon/mastodon/pull/24874)) +- **Change local and federated timelines to be in a single “Live feeds” column** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25641), [Gargron](https://github.com/mastodon/mastodon/pull/25683), [mgmn](https://github.com/mastodon/mastodon/pull/25694), [Plastikmensch](https://github.com/mastodon/mastodon/pull/26247)) +- **Change user archive export to be faster and more reliable, and export `.zip` archives instead of `.tar.gz` ones** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23360), [TheEssem](https://github.com/mastodon/mastodon/pull/25034)) +- **Change `mastodon-streaming` systemd unit files to be templated** ([e-nomem](https://github.com/mastodon/mastodon/pull/24751)) +- **Change `statsd` integration to disable sidekiq metrics by default** ([mjankowski](https://github.com/mastodon/mastodon/pull/25265), [mjankowski](https://github.com/mastodon/mastodon/pull/25336), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26310)) + This deprecates `statsd` support and disables the sidekiq integration unless `STATSD_SIDEKIQ` is set to `true`. + This is because the `nsa` gem is unmaintained, and its sidekiq integration is known to add very significant overhead. + Later versions of Mastodon will have other ways to get the same metrics. +- **Change replica support to native Rails adapter** ([krainboltgreene](https://github.com/mastodon/mastodon/pull/25693), [Gargron](https://github.com/mastodon/mastodon/pull/25849), [Gargron](https://github.com/mastodon/mastodon/pull/25874), [Gargron](https://github.com/mastodon/mastodon/pull/25851), [Gargron](https://github.com/mastodon/mastodon/pull/25977), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26074), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26386)) + This is a breaking change, dropping `makara` support, and requiring you to update your database configuration if you are using replicas. + To tell Mastodon to use a read replica, you can either set the `REPLICA_DB_NAME` environment variable (along with `REPLICA_DB_USER`, `REPLICA_DB_PASS`, `REPLICA_DB_HOST`, and `REPLICA_DB_PORT`, if they differ from the primary database), or the `REPLICA_DATABASE_URL` environment variable if your configuration is based on `DATABASE_URL`. +- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362)) +- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299)) +- Change indexing frequency from 5 minutes to 1 minute, add locks to schedulers ([Gargron](https://github.com/mastodon/mastodon/pull/26304)) +- Change column link to add a better keyboard focus indicator ([teeerevor](https://github.com/mastodon/mastodon/pull/26278)) +- Change poll form element colors to fit with the rest of the ui ([teeerevor](https://github.com/mastodon/mastodon/pull/26139), [teeerevor](https://github.com/mastodon/mastodon/pull/26162), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26164)) +- Change 'favourite' to 'favorite' for American English ([marekr](https://github.com/mastodon/mastodon/pull/24667), [gunchleoc](https://github.com/mastodon/mastodon/pull/26009), [nabijaczleweli](https://github.com/mastodon/mastodon/pull/26109)) +- Change ActivityStreams representation of suspended accounts to not use a blank `name` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25276)) +- Change focus UI for keyboard only input ([teeerevor](https://github.com/mastodon/mastodon/pull/25935), [Gargron](https://github.com/mastodon/mastodon/pull/26125)) +- Change thread view to scroll to the selected post rather than the post being replied to ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24685)) +- Change links in multi-column mode so tabs are open in single-column mode ([Signez](https://github.com/mastodon/mastodon/pull/25893), [Signez](https://github.com/mastodon/mastodon/pull/26070), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25973)) +- Change searching with `#` to include account index ([jsgoldstein](https://github.com/mastodon/mastodon/pull/25638)) +- Change label and design of sensitive and unavailable media in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25712), [Gargron](https://github.com/mastodon/mastodon/pull/26135), [Gargron](https://github.com/mastodon/mastodon/pull/26330)) +- Change button colors to increase hover/focus contrast and consistency ([teeerevor](https://github.com/mastodon/mastodon/pull/25677), [Gargron](https://github.com/mastodon/mastodon/pull/25679)) +- Change dropdown icon above compose form from ellipsis to bars in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25661)) +- Change header backgrounds to use fewer different colors in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25577)) +- Change files to be deleted in batches instead of one-by-one ([Gargron](https://github.com/mastodon/mastodon/pull/23302), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/25586), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25587)) +- Change emoji picker icon ([iparr](https://github.com/mastodon/mastodon/pull/25479)) +- Change edit profile page ([Gargron](https://github.com/mastodon/mastodon/pull/25413)) +- Change "bot" label to "automated" ([Gargron](https://github.com/mastodon/mastodon/pull/25356)) +- Change design of dropdowns in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25107)) +- Change wording of “Content cache retention period” setting to highlight destructive implications ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23261)) +- Change autolinking to allow carets in URL search params ([renchap](https://github.com/mastodon/mastodon/pull/25216)) +- Change share action from being in action bar to being in dropdown in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25105)) +- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028)) +- Change sessions to be ordered from most-recent to least-recently updated ([frankieroberto](https://github.com/mastodon/mastodon/pull/25005)) +- Change vacuum scheduler to also delete expired tokens and unused application records ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24868), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24871)) +- Change "Sign in" to "Login" ([Gargron](https://github.com/mastodon/mastodon/pull/24942)) +- Change domain suspensions to also be checked before trying to fetch unknown remote resources ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24535)) +- Change media components to use aspect-ratio rather than compute height themselves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24686), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24943)) +- Change logo version in header based on screen size in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24707)) +- Change label from "For you" to "People" on explore screen in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24706)) +- Change logged-out WebUI HTML pages to be cached for a few seconds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24708)) +- Change unauthenticated responses to be cached in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/24348), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24665)) +- Change HTTP caching logic ([Gargron](https://github.com/mastodon/mastodon/pull/24347), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24604)) +- Change hashtags and mentions in bios to open in-app in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24643)) +- Change styling of the recommended accounts to allow bio to be more visible ([chike00](https://github.com/mastodon/mastodon/pull/24480)) +- Change account search in moderation interface to allow searching by username including the leading `@` ([HeitorMC](https://github.com/mastodon/mastodon/pull/24242)) +- Change all components to use the same error page in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24512)) +- Change search pop-out in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24305)) +- Change user settings to be stored in a more optimal way ([Gargron](https://github.com/mastodon/mastodon/pull/23630), [c960657](https://github.com/mastodon/mastodon/pull/24321), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24453), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24460), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24558), [Gargron](https://github.com/mastodon/mastodon/pull/24761), [Gargron](https://github.com/mastodon/mastodon/pull/24783), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25508), [jsgoldstein](https://github.com/mastodon/mastodon/pull/25340)) +- Change media upload limits and remove client-side resizing ([Gargron](https://github.com/mastodon/mastodon/pull/23726)) +- Change design of account rows in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24247), [Gargron](https://github.com/mastodon/mastodon/pull/24343), [Gargron](https://github.com/mastodon/mastodon/pull/24956), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25131)) +- Change log-out to use Single Logout when using external log-in through OIDC ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24020)) +- Change sidekiq-bulk's batch size from 10,000 to 1,000 jobs in one Redis call ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24034)) +- Change translation to only be offered for supported languages ([c960657](https://github.com/mastodon/mastodon/pull/23879), [c960657](https://github.com/mastodon/mastodon/pull/24037)) + This adds the `/api/v1/instance/translation_languages` REST API endpoint that returns an object with the supported translation language pairs in the form: + ```json + { + "fr": ["en", "de"] + } + ``` + (where `fr` is a supported source language and `en` and `de` or supported output language when translating a `fr` string) +- Change compose form checkbox to native input with `appearance: none` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/22949)) +- Change posts' clickable area to be larger ([c960657](https://github.com/mastodon/mastodon/pull/23621)) +- Change `followed_by` link to `location=all` if account is local on /admin/accounts/:id page ([tribela](https://github.com/mastodon/mastodon/pull/23467)) + +### Removed + +- **Remove support for Node.js 14** ([renchap](https://github.com/mastodon/mastodon/pull/25198)) +- **Remove support for Ruby 2.7** ([nschonni](https://github.com/mastodon/mastodon/pull/24237)) +- **Remove clustering from streaming API** ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/24655)) +- **Remove anonymous access to the streaming API** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23989)) +- Remove 16:9 cropping from web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26132)) +- Remove back button from bookmarks, favourites and lists screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26126)) +- Remove display name input from sign-up form ([Gargron](https://github.com/mastodon/mastodon/pull/24704)) +- Remove `tai` locale ([c960657](https://github.com/mastodon/mastodon/pull/23880)) +- Remove empty Kushubian (csb) local files ([nschonni](https://github.com/mastodon/mastodon/pull/24151)) +- Remove `Permissions-Policy` header from all responses ([Gargron](https://github.com/mastodon/mastodon/pull/24124)) + +### Fixed + +- **Fix filters not being applying in the explore page** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25887)) +- **Fix being unable to load past a full page of filtered posts in Home timeline** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24930)) +- **Fix log-in flow when involving both OAuth and external authentication** ([CSDUMMI](https://github.com/mastodon/mastodon/pull/24073)) +- **Fix broken links in account gallery** ([c960657](https://github.com/mastodon/mastodon/pull/24218)) +- Fix adding column with default value taking longer on Postgres >= 11 ([Gargron](https://github.com/mastodon/mastodon/pull/26375)) +- Fix light theme select option for hashtags ([teeerevor](https://github.com/mastodon/mastodon/pull/26311)) +- Fix AVIF attachments ([c960657](https://github.com/mastodon/mastodon/pull/26264)) +- Fix incorrect URL normalization when fetching remote resources ([c960657](https://github.com/mastodon/mastodon/pull/26219), [c960657](https://github.com/mastodon/mastodon/pull/26285)) +- Fix being unable to filter posts for individual Chinese languages ([gunchleoc](https://github.com/mastodon/mastodon/pull/26066)) +- Fix preview card sometimes linking to 4xx error pages ([c960657](https://github.com/mastodon/mastodon/pull/26200)) +- Fix emoji picker button scrolling with textarea content in single-column view ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25304)) +- Fix missing border on error screen in light theme in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26152)) +- Fix UI overlap with the loupe icon in the Explore Tab ([gol-cha](https://github.com/mastodon/mastodon/pull/26113)) +- Fix unexpected redirection to `/explore` after sign-in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26143)) +- Fix `/api/v1/statuses/:id/unfavourite` and `/api/v1/statuses/:id/unreblog` returning non-updated counts ([c960657](https://github.com/mastodon/mastodon/pull/24365)) +- Fix clicking the “Back” button sometimes leading out of Mastodon ([c960657](https://github.com/mastodon/mastodon/pull/23953), [CSFlorin](https://github.com/mastodon/mastodon/pull/24835), [S-H-GAMELINKS](https://github.com/mastodon/mastodon/pull/24867), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25281)) +- Fix processing of `null` ActivityPub activities ([tribela](https://github.com/mastodon/mastodon/pull/26021)) +- Fix hashtag posts not being removed from home feed on hashtag unfollow ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26028)) +- Fix for "follows you" indicator in light web UI not readable ([vmstan](https://github.com/mastodon/mastodon/pull/25993)) +- Fix incorrect line break between icon and number of reposts & favourites ([edent](https://github.com/mastodon/mastodon/pull/26004)) +- Fix sounds not being loaded from assets host ([Signez](https://github.com/mastodon/mastodon/pull/25931)) +- Fix buttons showing inconsistent styles ([teeerevor](https://github.com/mastodon/mastodon/pull/25903), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25965), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26341)) +- Fix trend calculation working on too many items at a time ([Gargron](https://github.com/mastodon/mastodon/pull/25835)) +- Fix dropdowns being disabled for logged out users in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25714), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25964)) +- Fix explore page being inaccessible when opted-out of trends in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25716)) +- Fix re-activated accounts possibly getting deleted by `AccountDeletionWorker` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25711)) +- Fix `/api/v2/search` not working with following query param ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25681)) +- Fix inefficient query when requesting a new confirmation email from a logged-in account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25669)) +- Fix unnecessary concurrent calls to `/api/*/instance` in web UI ([mgmn](https://github.com/mastodon/mastodon/pull/25663)) +- Fix resolving local URL for remote content ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25637)) +- Fix search not being easily findable on smaller screens in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25576), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25631)) +- Fix j/k keyboard shortcuts on some status lists ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25554)) +- Fix missing validation on `default_privacy` setting ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25513)) +- Fix incorrect pagination headers in `/api/v2/admin/accounts` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25477)) +- Fix non-interactive upload container being given a `button` role and tabIndex ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25462)) +- Fix always redirecting to onboarding in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/25396)) +- Fix inconsistent use of middle dot (·) instead of bullet (•) to separate items ([j-f1](https://github.com/mastodon/mastodon/pull/25248)) +- Fix spacing of middle dots in the detailed status meta section ([j-f1](https://github.com/mastodon/mastodon/pull/25247)) +- Fix prev/next buttons color in media viewer ([renchap](https://github.com/mastodon/mastodon/pull/25231)) +- Fix email addresses not being properly updated in `tootctl maintenance fix-duplicates` ([mjankowski](https://github.com/mastodon/mastodon/pull/25118)) +- Fix unicode surrogate pairs sometimes being broken in page title ([eai04191](https://github.com/mastodon/mastodon/pull/25148)) +- Fix various inefficient queries against account domains ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25126)) +- Fix video player offering to expand in a lightbox when it's in an `iframe` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25067)) +- Fix post embed previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25071)) +- Fix inadequate error handling in several API controllers when given invalid parameters ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24947), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24958), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25063), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25072), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25386), [danielmbrasil](https://github.com/mastodon/mastodon/pull/25595)) +- Fix uncaught `ActiveRecord::StatementInvalid` in Mastodon::IpBlocksCLI ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24861)) +- Fix various edge cases with local moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24812)) +- Fix `tootctl accounts cull` crashing when encountering a domain resolving to a private address ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23378)) +- Fix `tootctl accounts approve --number N` not aproving the N earliest registrations ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24605)) +- Fix being unable to clear media description when editing posts ([c960657](https://github.com/mastodon/mastodon/pull/24720)) +- Fix unavailable translations not falling back to English ([mgmn](https://github.com/mastodon/mastodon/pull/24727)) +- Fix anonymous visitors getting a session cookie on first visit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24584), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24664)) +- Fix cutting off first letter of hashtag links sometimes in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/24623)) +- Fix crash in `tootctl accounts create --reattach --force` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24557), [danielmbrasil](https://github.com/mastodon/mastodon/pull/24680)) +- Fix characters being emojified even when using Variation Selector 15 (text) ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/20949), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24615)) +- Fix uncaught ActiveRecord::StatementInvalid exception in `Mastodon::AccountsCLI#approve` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24590)) +- Fix email confirmation skip option in `tootctl accounts modify USERNAME --email EMAIL --confirm` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/24578)) +- Fix tooltip for dates without time ([c960657](https://github.com/mastodon/mastodon/pull/24244)) +- Fix missing loading spinner and loading more on scroll in Private Mentions column ([c960657](https://github.com/mastodon/mastodon/pull/24446)) +- Fix account header image missing from `/settings/profile` on narrow screens ([c960657](https://github.com/mastodon/mastodon/pull/24433)) +- Fix height of announcements not being updated when using reduced animations ([c960657](https://github.com/mastodon/mastodon/pull/24354)) +- Fix inconsistent radius in advanced interface drawer ([thislight](https://github.com/mastodon/mastodon/pull/24407)) +- Fix loading more trending posts on scroll in the advanced interface ([OmmyZhang](https://github.com/mastodon/mastodon/pull/24314)) +- Fix poll ending notification for edited polls ([c960657](https://github.com/mastodon/mastodon/pull/24311)) +- Fix max width of media in `/about` and `/privacy-policy` ([mgmn](https://github.com/mastodon/mastodon/pull/24180)) +- Fix streaming API not being usable without `DATABASE_URL` ([Gargron](https://github.com/mastodon/mastodon/pull/23960)) +- Fix external authentication not running onboarding code for new users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23458)) + +## [4.1.6] - 2023-07-31 + +### Fixed + +- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228)) +- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233)) +- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116)) + +## [4.1.5] - 2023-07-21 + +### Added + +- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850)) + +### Changed + +- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055)) + +### Fixed + +- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25885)) +- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886)) +- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945)) + +### Security + +- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105)) + ## [4.1.4] - 2023-07-07 ### Fixed diff --git a/Gemfile b/Gemfile index 074697066..4c3e5a368 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ gem 'aws-sdk-s3', '~> 1.123', require: false gem 'fog-core', '<= 2.4.0' gem 'fog-openstack', '~> 0.3', require: false gem 'kt-paperclip', '~> 7.2' +gem 'md-paperclip-azure', '~> 2.2', require: false gem 'blurhash', '~> 0.1' gem 'active_model_serializers', '~> 0.10' @@ -34,11 +35,14 @@ group :pam_authentication, optional: true do end gem 'net-ldap', '~> 0.18' -gem 'omniauth-cas', '~> 2.0' -gem 'omniauth-saml', '~> 1.10' + +# TODO: Point back at released omniauth-cas gem when PR merged +# https://github.com/dlindahl/omniauth-cas/pull/68 +gem 'omniauth-cas', github: 'stanhu/omniauth-cas', ref: '4211e6d05941b4a981f9a36b49ec166cecd0e271' +gem 'omniauth-saml', '~> 2.0' gem 'omniauth_openid_connect', '~> 0.6.1' -gem 'omniauth', '~> 1.9' -gem 'omniauth-rails_csrf_protection', '~> 0.1' +gem 'omniauth', '~> 2.0' +gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'color_diff', '~> 0.1' gem 'discard', '~> 1.2' @@ -55,8 +59,9 @@ gem 'httplog', '~> 1.6.2' gem 'idn-ruby', require: 'idn' gem 'kaminari', '~> 1.2' gem 'link_header', '~> 0.0' -gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' +gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar' gem 'nokogiri', '~> 1.15' +gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b' gem 'oj', '~> 3.14' gem 'ox', '~> 2.14' gem 'parslet' @@ -98,9 +103,6 @@ gem 'rdf-normalize', '~> 0.5' gem 'private_address_check', '~> 0.5' group :test do - # RSpec runner for rails - gem 'rspec-rails', '~> 6.0' - # Used to split testing into chunks in CI gem 'rspec_chunked', '~> 0.6' @@ -112,6 +114,10 @@ group :test do # Browser integration testing gem 'capybara', '~> 3.39' + gem 'selenium-webdriver' + + # Used to reset the database between system tests + gem 'database_cleaner-active_record' # Used to mock environment variables gem 'climate_control', '~> 0.2' @@ -172,10 +178,17 @@ group :development do # Validate missing i18n keys gem 'i18n-tasks', '~> 1.0', require: false +end +group :development, :test do # Profiling tools gem 'memory_profiler', require: false + gem 'ruby-prof', require: false gem 'stackprof', require: false + gem 'test-prof' + + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' end group :production do diff --git a/Gemfile.lock b/Gemfile.lock index a75746355..9eef03855 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,17 @@ GIT hkdf (~> 0.2) jwt (~> 2.0) +GIT + remote: https://github.com/jhawthorn/nsa.git + revision: e020fcc3a54d993ab45b7194d89ab720296c111b + ref: e020fcc3a54d993ab45b7194d89ab720296c111b + specs: + nsa (0.2.8) + activesupport (>= 4.2, < 7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + sidekiq (>= 3.5) + statsd-ruby (~> 1.4, >= 1.4.0) + GIT remote: https://github.com/mastodon/rails-settings-cached.git revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab @@ -15,50 +26,60 @@ GIT rails-settings-cached (0.6.6) rails (>= 4.2.0) +GIT + remote: https://github.com/stanhu/omniauth-cas.git + revision: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + ref: 4211e6d05941b4a981f9a36b49ec166cecd0e271 + specs: + omniauth-cas (2.0.0) + addressable (~> 2.3) + nokogiri (~> 1.5) + omniauth (>= 1.2, < 3) + GEM remote: https://rubygems.org/ specs: - actioncable (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + actioncable (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actionmailbox (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.6) - actionpack (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activesupport (= 7.0.6) + actionmailer (7.0.7) + actionpack (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activesupport (= 7.0.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.6) - actionview (= 7.0.6) - activesupport (= 7.0.6) + actionpack (7.0.7) + actionview (= 7.0.7) + activesupport (= 7.0.7) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.6) - actionpack (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + actiontext (7.0.7) + actionpack (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.6) - activesupport (= 7.0.6) + actionview (7.0.7) + activesupport (= 7.0.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -68,22 +89,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.6) - activesupport (= 7.0.6) + activejob (7.0.7) + activesupport (= 7.0.7) globalid (>= 0.3.6) - activemodel (7.0.6) - activesupport (= 7.0.6) - activerecord (7.0.6) - activemodel (= 7.0.6) - activesupport (= 7.0.6) - activestorage (7.0.6) - actionpack (= 7.0.6) - activejob (= 7.0.6) - activerecord (= 7.0.6) - activesupport (= 7.0.6) + activemodel (7.0.7) + activesupport (= 7.0.7) + activerecord (7.0.7) + activemodel (= 7.0.7) + activesupport (= 7.0.7) + activestorage (7.0.7) + actionpack (= 7.0.7) + activejob (= 7.0.7) + activerecord (= 7.0.7) + activesupport (= 7.0.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.6) + activesupport (7.0.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -103,8 +124,8 @@ GEM attr_required (1.0.1) awrence (1.2.1) aws-eventstream (1.2.0) - aws-partitions (1.786.0) - aws-sdk-core (3.178.0) + aws-partitions (1.793.0) + aws-sdk-core (3.180.3) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) @@ -112,12 +133,20 @@ GEM aws-sdk-kms (1.71.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.130.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-s3 (1.132.1) + aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) aws-sigv4 (1.6.0) aws-eventstream (~> 1, >= 1.0.2) + azure-storage-blob (2.0.3) + azure-storage-common (~> 2.0) + nokogiri (~> 1, >= 1.10.8) + azure-storage-common (2.0.4) + faraday (~> 1.0) + faraday_middleware (~> 1.0, >= 1.0.0.rc1) + net-http-persistent (~> 4.0) + nokogiri (~> 1, >= 1.10.8) bcrypt (3.1.18) better_errors (2.10.1) erubi (>= 1.0.0) @@ -136,7 +165,7 @@ GEM blurhash (0.1.7) bootsnap (1.16.0) msgpack (~> 1.2) - brakeman (6.0.0) + brakeman (6.0.1) browser (5.3.1) brpoplpush-redis_script (0.1.3) concurrent-ruby (~> 1.0, >= 1.0.5) @@ -191,6 +220,10 @@ GEM crass (1.0.6) css_parser (1.14.0) addressable + database_cleaner-active_record (2.1.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) date (3.3.3) debug_inspector (1.1.0) devise (4.9.2) @@ -261,6 +294,8 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) fast_blank (1.0.1) fastimage (2.2.7) ffi (1.15.5) @@ -297,7 +332,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.48.0) + haml_lint (0.49.3) haml (>= 4.0, < 6.2) parallel (~> 1.10) rainbow @@ -393,7 +428,7 @@ GEM llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) - lograge (0.12.0) + lograge (0.13.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -410,20 +445,26 @@ GEM mario-redis-lock (1.2.1) redis (>= 3.0.5) matrix (0.4.2) + md-paperclip-azure (2.2.0) + addressable (~> 2.5) + azure-storage-blob (~> 2.0.1) + hashie (~> 5.0) memory_profiler (1.0.1) method_source (1.0.0) - mime-types (3.4.1) + mime-types (3.5.0) mime-types-data (~> 3.2015) - mime-types-data (3.2023.0218.1) - mini_mime (1.1.2) - mini_portile2 (2.8.2) - minitest (5.18.1) + mime-types-data (3.2023.0808) + mini_mime (1.1.5) + mini_portile2 (2.8.4) + minitest (5.19.0) msgpack (1.7.1) multi_json (1.15.0) multipart-post (2.3.0) net-http (0.3.2) uri - net-imap (0.3.6) + net-http-persistent (4.0.2) + connection_pool (~> 2.2) + net-imap (0.3.7) date net-protocol net-ldap (0.18.0) @@ -441,19 +482,16 @@ GEM mini_portile2 (~> 2.8.2) racc (~> 1.4) oj (3.15.0) - omniauth (1.9.2) + omniauth (2.1.1) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) - omniauth-cas (2.0.0) - addressable (~> 2.3) - nokogiri (~> 1.5) - omniauth (~> 1.2) - omniauth-rails_csrf_protection (0.1.2) + rack (>= 2.2.3) + rack-protection + omniauth-rails_csrf_protection (1.0.1) actionpack (>= 4.2) - omniauth (>= 1.3.1) - omniauth-saml (1.10.3) - omniauth (~> 1.3, >= 1.3.2) - ruby-saml (~> 1.9) + omniauth (~> 2.0) + omniauth-saml (2.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) omniauth_openid_connect (0.6.1) omniauth (>= 1.9, < 3) openid_connect (~> 1.1) @@ -500,9 +538,9 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.7.1) - rack (2.2.7) - rack-attack (6.6.1) - rack (>= 1.0, < 3) + rack (2.2.8) + rack-attack (6.7.0) + rack (>= 1.0, < 4) rack-cors (2.0.1) rack (>= 2.0.0) rack-oauth2 (1.21.3) @@ -511,30 +549,33 @@ GEM httpclient json-jwt (>= 1.11.0) rack (>= 2.1.0) + rack-protection (3.0.5) + rack rack-proxy (0.7.6) rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.6) - actioncable (= 7.0.6) - actionmailbox (= 7.0.6) - actionmailer (= 7.0.6) - actionpack (= 7.0.6) - actiontext (= 7.0.6) - actionview (= 7.0.6) - activejob (= 7.0.6) - activemodel (= 7.0.6) - activerecord (= 7.0.6) - activestorage (= 7.0.6) - activesupport (= 7.0.6) + rails (7.0.7) + actioncable (= 7.0.7) + actionmailbox (= 7.0.7) + actionmailer (= 7.0.7) + actionpack (= 7.0.7) + actiontext (= 7.0.7) + actionview (= 7.0.7) + activejob (= 7.0.7) + activemodel (= 7.0.7) + activerecord (= 7.0.7) + activestorage (= 7.0.7) + activesupport (= 7.0.7) bundler (>= 1.15.0) - railties (= 7.0.6) + railties (= 7.0.7) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.0.3) - activesupport (>= 4.2.0) + rails-dom-testing (2.1.1) + activesupport (>= 5.0.0) + minitest nokogiri (>= 1.6) rails-html-sanitizer (1.6.0) loofah (~> 2.21) @@ -542,9 +583,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.6) - actionpack (= 7.0.6) - activesupport (= 7.0.6) + railties (7.0.7) + actionpack (= 7.0.7) + activesupport (= 7.0.7) method_source rake (>= 12.2) thor (~> 1.0) @@ -553,7 +594,7 @@ GEM rake (13.0.6) rdf (3.2.11) link_header (~> 0.0, >= 0.0.8) - rdf-normalize (0.6.0) + rdf-normalize (0.6.1) rdf (~> 3.2) redcarpet (3.6.0) redis (4.8.1) @@ -567,7 +608,7 @@ GEM responders (3.1.0) actionpack (>= 5.2) railties (>= 5.2) - rexml (3.2.5) + rexml (3.2.6) rotp (6.2.2) rouge (4.1.2) rpam2 (4.0.2) @@ -624,6 +665,7 @@ GEM rubocop (~> 1.33) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) + ruby-prof (1.6.3) ruby-progressbar (1.13.0) ruby-saml (1.15.0) nokogiri (>= 1.13.10) @@ -640,6 +682,10 @@ GEM scenic (1.7.0) activerecord (>= 4.0.0) railties (>= 4.0.0) + selenium-webdriver (4.11.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) semantic_range (3.0.0) sidekiq (6.5.9) connection_pool (>= 2.2.5, < 3) @@ -680,6 +726,7 @@ GEM net-scp (>= 1.1.2) net-ssh (>= 2.8.0) stackprof (0.2.25) + statsd-ruby (1.5.0) stoplight (3.0.1) redlock (~> 1.0) strong_migrations (0.8.0) @@ -694,6 +741,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) + test-prof (1.2.2) thor (1.2.2) tilt (2.2.0) timeout (0.4.0) @@ -752,14 +800,15 @@ GEM rack-proxy (>= 0.6.1) railties (>= 5.2) semantic_range (>= 2.3.0) - websocket-driver (0.7.5) + websocket (1.2.9) + websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) wisper (2.0.1) xorcist (1.1.3) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.8) + zeitwerk (2.6.11) PLATFORMS ruby @@ -788,6 +837,7 @@ DEPENDENCIES color_diff (~> 0.1) concurrent-ruby connection_pool + database_cleaner-active_record devise (~> 4.9) devise-two-factor (~> 4.1) devise_pam_authenticatable2 (~> 9.2) @@ -822,16 +872,18 @@ DEPENDENCIES link_header (~> 0.0) lograge (~> 0.12) mario-redis-lock (~> 1.2) + md-paperclip-azure (~> 2.2) memory_profiler - mime-types (~> 3.4.1) + mime-types (~> 3.5.0) net-http (~> 0.3.2) net-ldap (~> 0.18) nokogiri (~> 1.15) + nsa! oj (~> 3.14) - omniauth (~> 1.9) - omniauth-cas (~> 2.0) - omniauth-rails_csrf_protection (~> 0.1) - omniauth-saml (~> 1.10) + omniauth (~> 2.0) + omniauth-cas! + omniauth-rails_csrf_protection (~> 1.0) + omniauth-saml (~> 2.0) omniauth_openid_connect (~> 0.6.1) ox (~> 2.14) parslet @@ -864,10 +916,12 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + ruby-prof ruby-progressbar (~> 1.13) rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) + selenium-webdriver sidekiq (~> 6.5) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 5.0) @@ -880,6 +934,7 @@ DEPENDENCIES stackprof stoplight (~> 3.0.1) strong_migrations (~> 0.8) + test-prof thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) diff --git a/README.md b/README.md index 52ba7988d..37cd3dfb4 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [releases]: https://github.com/mastodon/mastodon/releases [crowdin]: https://crowdin.com/project/mastodon -Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) +Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) Click below to **learn more** in a video: @@ -51,7 +51,7 @@ Upload and view images and WebM/MP4 videos attached to the updates. Videos with ### Safety and moderation tools -Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) +Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) ### OAuth2 and a straightforward REST API @@ -74,7 +74,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre 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. -A **Vagrant** configuration is included for development purposes. To use it, complete following steps: +A **Vagrant** configuration is included for development purposes. To use it, complete the following steps: - Install Vagrant and Virtualbox - Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater` @@ -100,9 +100,9 @@ To get started, create a codespace for this repository by clicking this 👇 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283) -A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with software needed for this project. +A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with the software needed for this project. -**Note**: Dev containers is an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting). +**Note**: Dev containers are an open spec that is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting). ## Contributing diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index abde8e92f..61f5277d2 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -33,7 +33,7 @@ class AccountsIndex < Chewy::Index }, verbatim: { - tokenizer: 'whitespace', + tokenizer: 'standard', filter: %w(lowercase asciifolding cjk_width), }, diff --git a/app/chewy/instances_index.rb b/app/chewy/instances_index.rb new file mode 100644 index 000000000..2bce4043c --- /dev/null +++ b/app/chewy/instances_index.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class InstancesIndex < Chewy::Index + settings index: { refresh_interval: '30s' } + + index_scope ::Instance.searchable + + root date_detection: false do + field :domain, type: 'text', index_prefixes: { min_chars: 1 } + field :accounts_count, type: 'long' + end +end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0090ef7ec..936973fb2 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -12,7 +12,7 @@ class AccountsController < ApplicationController before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) } - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? def show respond_to do |format| diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index b9691c5a3..96c31a38f 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -40,7 +40,7 @@ module Admin end # Allow transparently upgrading a domain block - if existing_domain_block.present? + if existing_domain_block.present? && existing_domain_block.domain == TagManager.instance.normalize_domain(@domain_block.domain.strip) @domain_block = existing_domain_block @domain_block.assign_attributes(resource_params) end diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 519405726..e5a55de06 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -65,7 +65,7 @@ module Admin end def filtered_instances - InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results + InstanceFilter.new(limited_federation_mode? ? { allowed: true } : filter_params).results end def filter_params diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2629ab782..c764b4510 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -8,7 +8,7 @@ class Api::BaseController < ApplicationController include AccessTokenTrackingConcern include ApiCachingConcern - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access? before_action :require_not_suspended! @@ -150,7 +150,7 @@ class Api::BaseController < ApplicationController end def disallow_unauthenticated_api_access? - ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode + ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.limited_federation_mode end private diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 3d55d990a..9da77f8da 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController before_action :require_enabled_api! - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? vary_by '' @@ -33,6 +33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController end def require_enabled_api! - head 404 unless Setting.activity_api_enabled && !whitelist_mode? + head 404 unless Setting.activity_api_enabled && !limited_federation_mode? end end diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index e954c4589..c91234e08 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Instances::DomainBlocksController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? before_action :require_enabled_api! before_action :set_domain_blocks diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index a0665725b..376fec906 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale before_action :set_extended_description @@ -10,7 +10,7 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user - super if whitelist_mode? + super if limited_federation_mode? end def show diff --git a/app/controllers/api/v1/instances/languages_controller.rb b/app/controllers/api/v1/instances/languages_controller.rb new file mode 100644 index 000000000..17509e748 --- /dev/null +++ b/app/controllers/api/v1/instances/languages_controller.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Api::V1::Instances::LanguagesController < Api::BaseController + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale + + before_action :set_languages + + vary_by '' + + def show + cache_even_if_authenticated! + render json: @languages, each_serializer: REST::LanguageSerializer + end + + private + + def set_languages + @languages = LanguagesHelper::SUPPORTED_LOCALES.keys.map { |code| LanguagePresenter.new(code) } + end +end diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 70281362a..08a982f22 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -3,24 +3,24 @@ class Api::V1::Instances::PeersController < Api::BaseController before_action :require_enabled_api! - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale vary_by '' # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user - super if whitelist_mode? + super if limited_federation_mode? end def index cache_even_if_authenticated! - render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) } + render_with_cache(expires_in: 1.day) { Instance.searchable.pluck(:domain) } end private def require_enabled_api! - head 404 unless Setting.peers_api_enabled && !whitelist_mode? + head 404 unless Setting.peers_api_enabled && !limited_federation_mode? end end diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index 36889f733..f5b1b4ec5 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? before_action :set_privacy_policy diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index d3eeca326..2f71984b0 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Instances::RulesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale before_action :set_rules @@ -10,7 +10,7 @@ class Api::V1::Instances::RulesController < Api::BaseController # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user - super if whitelist_mode? + super if limited_federation_mode? end def index diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb index c4680cccb..78423e40e 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Api::V1::Instances::TranslationLanguagesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? before_action :set_languages diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5a6701ff9..df4a14af1 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true class Api::V1::InstancesController < Api::BaseController - skip_before_action :require_authenticated_user!, unless: :whitelist_mode? + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? skip_around_action :set_locale vary_by '' # Override `current_user` to avoid reading session cookies unless in whitelist mode def current_user - super if whitelist_mode? + super if limited_federation_mode? end def show diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb new file mode 100644 index 000000000..2c0eacdca --- /dev/null +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class Api::V1::Peers::SearchController < Api::BaseController + before_action :require_enabled_api! + before_action :set_domains + + skip_before_action :require_authenticated_user!, unless: :limited_federation_mode? + skip_around_action :set_locale + + vary_by '' + + def index + cache_even_if_authenticated! + render json: @domains + end + + private + + def require_enabled_api! + head 404 unless Setting.peers_api_enabled && !limited_federation_mode? + end + + def set_domains + return if params[:q].blank? + + if Chewy.enabled? + @domains = InstancesIndex.query(function_score: { + query: { + prefix: { + domain: TagManager.instance.normalize_domain(params[:q].strip), + }, + }, + + field_value_factor: { + field: 'accounts_count', + modifier: 'log2p', + }, + }).limit(10).pluck(:domain) + else + domain = params[:q].strip + domain = TagManager.instance.normalize_domain(domain) + @domains = Instance.searchable.where(Instance.arel_table[:domain].matches("#{Instance.sanitize_sql_like(domain)}%", false, true)).limit(10).pluck(:domain) + end + end +end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index 2e21ce6a0..f3428e3df 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -17,13 +17,16 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController if fav @status = fav.status + count = [@status.favourites_count - 1, 0].max UnfavouriteWorker.perform_async(current_account.id, @status.id) else @status = Status.find(params[:status_id]) + count = @status.favourites_count authorize @status, :show? end - render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }) + relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } }) + render json: @status, serializer: REST::StatusSerializer, relationships: relationships rescue Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index e3769437b..3ca623117 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -24,15 +24,18 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController if @status authorize @status, :unreblog? + @reblog = @status.reblog + count = [@reblog.reblogs_count - 1, 0].max @status.discard RemovalWorker.perform_async(@status.id) - @reblog = @status.reblog else @reblog = Status.find(params[:status_id]) + count = @reblog.reblogs_count authorize @reblog, :show? end - render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }) + relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } }) + render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships rescue Mastodon::NotPermittedError not_found end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 66886b451..975315e24 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,7 +19,7 @@ class ApplicationController < ActionController::Base helper_method :use_seamless_external_login? helper_method :omniauth_only? helper_method :sso_account_settings - helper_method :whitelist_mode? + helper_method :limited_federation_mode? helper_method :body_class_string helper_method :skip_csrf_meta_tags? @@ -52,7 +52,7 @@ class ApplicationController < ActionController::Base private def authorized_fetch_mode? - ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode + ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode end def public_fetch_mode? diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb index 9e0fb942a..4723806b9 100644 --- a/app/controllers/auth/omniauth_callbacks_controller.rb +++ b/app/controllers/auth/omniauth_callbacks_controller.rb @@ -5,21 +5,13 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController def self.provides_callback_for(provider) define_method provider do + @provider = provider @user = User.find_for_oauth(request.env['omniauth.auth'], current_user) if @user.persisted? - LoginActivity.create( - user: @user, - success: true, - authentication_method: :omniauth, - provider: provider, - ip: request.remote_ip, - user_agent: request.user_agent - ) - + record_login_activity sign_in_and_redirect @user, event: :authentication - label = Devise.omniauth_configs[provider]&.strategy&.display_name.presence || I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize) - set_flash_message(:notice, :success, kind: label) if is_navigational_format? + set_flash_message(:notice, :success, kind: label_for_provider) if is_navigational_format? else session["devise.#{provider}_data"] = request.env['omniauth.auth'] redirect_to new_user_registration_url @@ -38,4 +30,29 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController auth_setup_path(missing_email: '1') end end + + private + + def record_login_activity + LoginActivity.create( + user: @user, + success: true, + authentication_method: :omniauth, + provider: @provider, + ip: request.remote_ip, + user_agent: request.user_agent + ) + end + + def label_for_provider + provider_display_name || configured_provider_name + end + + def provider_display_name + Devise.omniauth_configs[@provider]&.strategy&.display_name.presence + end + + def configured_provider_name + I18n.t("auth.providers.#{@provider}", default: @provider.to_s.chomp('_oauth2').capitalize) + end end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index 1380e6f28..06a3deee2 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -108,7 +108,7 @@ class Auth::SessionsController < Devise::SessionsController end def home_paths(resource) - paths = [about_path] + paths = [about_path, '/explore'] paths << short_account_path(username: resource.account) if single_user_mode? && resource.is_a?(User) diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index bf28d1842..99eed018b 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -3,32 +3,19 @@ class AuthorizeInteractionsController < ApplicationController include Authorization - layout 'modal' - before_action :authenticate_user! - before_action :set_body_classes before_action :set_resource def show if @resource.is_a?(Account) - render :show + redirect_to web_url("@#{@resource.pretty_acct}") elsif @resource.is_a?(Status) redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}") else - render :error + not_found end end - def create - if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource, with_rate_limit: true) - render :success - else - render :error - end - rescue ActiveRecord::RecordNotFound - render :error - end - private def set_resource @@ -61,8 +48,4 @@ class AuthorizeInteractionsController < ApplicationController def uri_param params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') end - - def set_body_classes - @body_classes = 'modal-layout' - end end diff --git a/app/controllers/backups_controller.rb b/app/controllers/backups_controller.rb index 205df48d4..db23fefbb 100644 --- a/app/controllers/backups_controller.rb +++ b/app/controllers/backups_controller.rb @@ -10,7 +10,7 @@ class BackupsController < ApplicationController def download case Paperclip::Attachment.default_options[:storage] - when :s3 + when :s3, :azure redirect_to @backup.dump.expiring_url(10), allow_other_host: true when :fog if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 25149d03f..3fc0938bf 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -4,7 +4,7 @@ module AccountOwnedConcern extend ActiveSupport::Concern included do - before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json } + before_action :authenticate_user!, if: -> { limited_federation_mode? && request.format != :json } before_action :set_account, if: :account_required? before_action :check_account_approval, if: :account_required? before_action :check_account_suspension, if: :account_required? diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb index 705abce80..12264d514 100644 --- a/app/controllers/concerns/api_caching_concern.rb +++ b/app/controllers/concerns/api_caching_concern.rb @@ -8,6 +8,6 @@ module ApiCachingConcern end def cache_even_if_authenticated! - expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode? + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless limited_federation_mode? end end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb index 576304d1c..170c8f5e0 100644 --- a/app/controllers/concerns/captcha_concern.rb +++ b/app/controllers/concerns/captcha_concern.rb @@ -42,7 +42,7 @@ module CaptchaConcern end def extend_csp_for_captcha! - policy = request.content_security_policy + policy = request.content_security_policy&.clone return unless captcha_required? && policy.present? @@ -54,6 +54,8 @@ module CaptchaConcern policy.send(directive, *values) end + + request.content_security_policy = policy end def render_captcha diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 6cd32a377..3a40ea382 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -11,7 +11,7 @@ module WebAppControllerConcern end def skip_csrf_meta_tags? - current_user.nil? + !(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil? end def set_app_body_class diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f35af5903..ffdbd0180 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -10,7 +10,7 @@ class FollowerAccountsController < ApplicationController before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } skip_around_action :set_locale, if: -> { request.format == :json } - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? def index respond_to do |format| diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 2aa31bdf0..cce296f9f 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -10,7 +10,7 @@ class FollowingAccountsController < ApplicationController before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } skip_around_action :set_locale, if: -> { request.format == :json } - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? def index respond_to do |format| diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb index b071a8060..1caeaaacf 100644 --- a/app/controllers/mail_subscriptions_controller.rb +++ b/app/controllers/mail_subscriptions_controller.rb @@ -9,6 +9,8 @@ class MailSubscriptionsController < ApplicationController before_action :set_user before_action :set_type + protect_from_forgery with: :null_session + def show; end def create @@ -20,6 +22,7 @@ class MailSubscriptionsController < ApplicationController def set_user @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe') + not_found unless @user end def set_body_classes @@ -35,7 +38,7 @@ class MailSubscriptionsController < ApplicationController when 'follow', 'reblog', 'favourite', 'mention', 'follow_request' "notification_emails.#{params[:type]}" else - raise ArgumentError + not_found end end end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 3bf5b7eba..53eee4001 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -3,9 +3,9 @@ class MediaController < ApplicationController include Authorization - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? - before_action :authenticate_user!, if: :whitelist_mode? + before_action :authenticate_user!, if: :limited_federation_mode? before_action :set_media_attachment before_action :verify_permitted_status! before_action :check_playable, only: :player diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8d480d704..c4230d62c 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -8,7 +8,7 @@ class MediaProxyController < ApplicationController skip_before_action :require_functional! - before_action :authenticate_user!, if: :whitelist_mode? + before_action :authenticate_user!, if: :limited_federation_mode? rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found diff --git a/app/controllers/remote_interaction_helper_controller.rb b/app/controllers/remote_interaction_helper_controller.rb new file mode 100644 index 000000000..90c853f47 --- /dev/null +++ b/app/controllers/remote_interaction_helper_controller.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class RemoteInteractionHelperController < ApplicationController + vary_by '' + + skip_before_action :require_functional! + skip_around_action :set_locale + skip_before_action :update_user_sign_in + + content_security_policy do |p| + # We inherit the normal `script-src` + + # Set every directive that does not have a fallback + p.default_src :none + p.form_action :none + p.base_uri :none + + # Disable every directive with a fallback to cut on response size + p.base_uri false + p.font_src false + p.img_src false + p.style_src false + p.media_src false + p.frame_src false + p.manifest_src false + p.connect_src false + p.child_src false + p.worker_src false + + # Widen the directives that we do need + p.frame_ancestors :self + p.connect_src :https + end + + def index + expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) + + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Referrer-Policy'] = 'no-referrer' + + render layout: 'helper_frame' + end +end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 1ff0fbd60..effaba363 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -17,7 +17,7 @@ class StatusesController < ApplicationController after_action :set_link_headers skip_around_action :set_locale, if: -> { request.format == :json } - skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode? + skip_before_action :require_functional!, only: [:show, :embed], unless: :limited_federation_mode? content_security_policy only: :embed do |policy| policy.frame_ancestors(false) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 7e249dbea..2007fe846 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -10,13 +10,13 @@ class TagsController < ApplicationController vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } - before_action :authenticate_user!, if: :whitelist_mode? + before_action :authenticate_user!, if: :limited_federation_mode? before_action :set_local before_action :set_tag before_action :set_statuses, if: -> { request.format == :rss } before_action :set_instance_presenter - skip_before_action :require_functional!, unless: :whitelist_mode? + skip_before_action :require_functional!, unless: :limited_federation_mode? def show respond_to do |format| diff --git a/app/controllers/well_known/webfinger_controller.rb b/app/controllers/well_known/webfinger_controller.rb index 0d897e8e2..4748940f7 100644 --- a/app/controllers/well_known/webfinger_controller.rb +++ b/app/controllers/well_known/webfinger_controller.rb @@ -19,6 +19,7 @@ module WellKnown def set_account username = username_from_resource + @account = begin if username == Rails.configuration.x.local_domain Account.representative diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 281219186..5f9d7e7c4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -235,6 +235,6 @@ module ApplicationHelper private def storage_host_var - ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) + ENV.fetch('S3_ALIAS_HOST', nil) || ENV.fetch('S3_CLOUDFRONT_HOST', nil) || ENV.fetch('AZURE_ALIAS_HOST', nil) end end diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 9303add35..95a36ccdf 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -21,7 +21,14 @@ module ContextHelper blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, - olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' }, + olm: { + 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', + 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, + 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, + 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, + 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, + 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' + }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' }, quoteUrl: { 'quoteUrl' => 'as:quoteUrl' }, }.freeze diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index 6fce7eb1f..2703c1b8f 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -10,14 +10,14 @@ module DomainControlHelper uri_or_domain end - if whitelist_mode? + if limited_federation_mode? !DomainAllow.allowed?(domain) else DomainBlock.blocked?(domain) end end - def whitelist_mode? - Rails.configuration.x.whitelist_mode + def limited_federation_mode? + Rails.configuration.x.limited_federation_mode end end diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 840a18d3e..1f400b477 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -204,7 +204,17 @@ module LanguagesHelper zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, }.freeze - SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze + # e.g. For Chinese, which is not a language, + # but a language family in spite of sharing the main locale code + # We need to be able to filter these + ISO_639_1_REGIONAL = { + 'zh-CN': ['Chinese (China)', '简体中文'].freeze, + 'zh-HK': ['Chinese (Hong Kong)', '繁體中文(香港)'].freeze, + 'zh-TW': ['Chinese (Taiwan)', '繁體中文(臺灣)'].freeze, + 'zh-YUE': ['Cantonese', '廣東話'].freeze, + }.freeze + + SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).freeze # For ISO-639-1 and ISO-639-3 language codes, we have their official # names, but for some translations, we need the names of the @@ -217,9 +227,6 @@ module LanguagesHelper 'pt-BR': 'Português (Brasil)', 'pt-PT': 'Português (Portugal)', 'sr-Latn': 'Srpski (latinica)', - 'zh-CN': '简体中文', - 'zh-HK': '繁體中文(香港)', - 'zh-TW': '繁體中文(臺灣)', }.freeze def native_locale_name(locale) diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb index f1f1ea872..286c53d83 100644 --- a/app/helpers/statuses_helper.rb +++ b/app/helpers/statuses_helper.rb @@ -65,33 +65,6 @@ module StatusesHelper embedded_view? ? '_blank' : nil end - def style_classes(status, is_predecessor, is_successor, include_threads) - classes = ['entry'] - classes << 'entry-predecessor' if is_predecessor - classes << 'entry-reblog' if status.reblog? - classes << 'entry-successor' if is_successor - classes << 'entry-center' if include_threads - classes.join(' ') - end - - def microformats_classes(status, is_direct_parent, is_direct_child) - classes = [] - classes << 'p-in-reply-to' if is_direct_parent - classes << 'p-repost-of' if status.reblog? && is_direct_parent - classes << 'p-comment' if is_direct_child - classes.join(' ') - end - - def microformats_h_class(status, is_predecessor, is_successor, include_threads) - if is_predecessor || status.reblog? || is_successor - 'h-cite' - elsif include_threads - '' - else - 'h-entry' - end - end - def fa_visibility_icon(status) case status.visibility when 'public' diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js index a1dadff52..50b90ef65 100644 --- a/app/javascript/mastodon/actions/importer/index.js +++ b/app/javascript/mastodon/actions/importer/index.js @@ -85,7 +85,7 @@ export function importFetchedStatuses(statuses) { } if (status.poll && status.poll.id) { - pushUnique(polls, normalizePoll(status.poll)); + pushUnique(polls, normalizePoll(status.poll, getState().getIn(['polls', status.poll.id]))); } } @@ -99,7 +99,7 @@ export function importFetchedStatuses(statuses) { } export function importFetchedPoll(poll) { - return dispatch => { - dispatch(importPolls([normalizePoll(poll)])); + return (dispatch, getState) => { + dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))])); }; } diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index dc163c5f7..203ac5351 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -78,6 +78,10 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.quote = normalOldStatus.get('quote'); normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); + + if (normalOldStatus.get('translation')) { + normalStatus.translation = normalOldStatus.get('translation'); + } } else { // If the status has a CW but no contents, treat the CW as if it were the // status' contents, to avoid having a CW toggle with seemingly no effect. @@ -101,6 +105,18 @@ export function normalizeStatus(status, normalOldStatus) { } } + if (normalOldStatus) { + const list = normalOldStatus.get('media_attachments'); + if (normalStatus.media_attachments && list) { + normalStatus.media_attachments.forEach(item => { + const oldItem = list.find(i => i.get('id') === item.id); + if (oldItem && oldItem.get('description') === item.description) { + item.translation = oldItem.get('translation') + } + }); + } + } + return normalStatus; } @@ -119,15 +135,23 @@ export function normalizeStatusTranslation(translation, status) { return normalTranslation; } -export function normalizePoll(poll) { +export function normalizePoll(poll, normalOldPoll) { const normalPoll = { ...poll }; const emojiMap = makeEmojiMap(poll.emojis); - normalPoll.options = poll.options.map((option, index) => ({ - ...option, - voted: poll.own_votes && poll.own_votes.includes(index), - titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), - })); + normalPoll.options = poll.options.map((option, index) => { + const normalOption = { + ...option, + voted: poll.own_votes && poll.own_votes.includes(index), + titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), + } + + if (normalOldPoll && normalOldPoll.getIn(['options', index, 'title']) === option.title) { + normalOption.translation = normalOldPoll.getIn(['options', index, 'translation']); + } + + return normalOption + }); return normalPoll; } diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 87c8bbc75..868ee907d 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -150,7 +150,7 @@ export function fillTimelineGaps(timelineId, path, params = {}, done = noOp) { export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, tagged, max_id: maxId }); +export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); diff --git a/app/javascript/mastodon/components/badge.jsx b/app/javascript/mastodon/components/badge.jsx new file mode 100644 index 000000000..235aef0c2 --- /dev/null +++ b/app/javascript/mastodon/components/badge.jsx @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; + +import { FormattedMessage } from 'react-intl'; + +import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg'; +import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg'; +import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg'; + + +export const Badge = ({ icon, label, domain }) => ( +
+ {icon} + {label} + {domain && {domain}} +
+); + +Badge.propTypes = { + icon: PropTypes.node, + label: PropTypes.node, + domain: PropTypes.node, +}; + +Badge.defaultProps = { + icon: , +}; + +export const GroupBadge = () => ( + } label={} /> +); + +export const AutomatedBadge = () => ( + } label={} /> +); \ No newline at end of file diff --git a/app/javascript/mastodon/components/dropdown_menu.jsx b/app/javascript/mastodon/components/dropdown_menu.jsx index 0416df5d4..fd66310e8 100644 --- a/app/javascript/mastodon/components/dropdown_menu.jsx +++ b/app/javascript/mastodon/components/dropdown_menu.jsx @@ -297,7 +297,7 @@ export default class Dropdown extends PureComponent { onKeyPress: this.handleKeyPress, }) : ( {children && cloneElement(children, { hidden: true })} @@ -122,7 +122,7 @@ export default class IntersectionObserverArticle extends Component { } return ( -
+
{children && cloneElement(children, { hidden: false })}
); diff --git a/app/javascript/mastodon/components/media_gallery.jsx b/app/javascript/mastodon/components/media_gallery.jsx index 960942d85..16aa8c505 100644 --- a/app/javascript/mastodon/components/media_gallery.jsx +++ b/app/javascript/mastodon/components/media_gallery.jsx @@ -12,7 +12,7 @@ import { debounce } from 'lodash'; import { Blurhash } from 'mastodon/components/blurhash'; -import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state'; import { IconButton } from './icon_button'; @@ -209,7 +209,6 @@ class MediaGallery extends PureComponent { static propTypes = { sensitive: PropTypes.bool, - standalone: PropTypes.bool, media: ImmutablePropTypes.list.isRequired, lang: PropTypes.string, size: PropTypes.object, @@ -224,11 +223,6 @@ class MediaGallery extends PureComponent { quote: PropTypes.bool, }; - static defaultProps = { - standalone: false, - quote: false, - }; - state = { visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), width: this.props.defaultWidth, @@ -297,7 +291,7 @@ class MediaGallery extends PureComponent { } render () { - const { media, lang, intl, sensitive, defaultWidth, standalone, autoplay, quote } = this.props; + const { media, lang, intl, sensitive, defaultWidth, autoplay, quote } = this.props; const { visible } = this.state; const width = this.state.width || defaultWidth; @@ -305,10 +299,10 @@ class MediaGallery extends PureComponent { const style = {}; - if (this.isFullSizeEligible() && (standalone || !cropImages)) { + if (this.isFullSizeEligible()) { style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; } else { - style.aspectRatio = '16 / 9'; + style.aspectRatio = '3 / 2'; } if (quote && style.height) { @@ -322,7 +316,7 @@ class MediaGallery extends PureComponent { const size = media.take(4).size; const uncached = media.every(attachment => attachment.get('type') === 'unknown'); - if (standalone && this.isFullSizeEligible()) { + if (this.isFullSizeEligible()) { children = ; } else { children = media.take(4).map((attachment, i) => ); diff --git a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx index 756a97722..c65bd494f 100644 --- a/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx +++ b/app/javascript/mastodon/components/picture_in_picture_placeholder.jsx @@ -12,6 +12,7 @@ class PictureInPicturePlaceholder extends PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + aspectRatio: PropTypes.string, }; handleClick = () => { @@ -20,8 +21,10 @@ class PictureInPicturePlaceholder extends PureComponent { }; render () { + const { aspectRatio } = this.props; + return ( -
+
diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 0cab602b8..8b1cc17b8 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -6,7 +6,6 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; import { HotKeys } from 'react-hotkeys'; @@ -20,7 +19,6 @@ import Bundle from '../features/ui/components/bundle'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { displayMedia } from '../initial_state'; -import AttachmentList from './attachment_list'; import { Avatar } from './avatar'; import { AvatarOverlay } from './avatar_overlay'; import { DisplayName } from './display_name'; @@ -292,17 +290,35 @@ class Status extends ImmutablePureComponent { this.props.onQuoteToggleHidden(this._properStatus()); } - renderLoadingMediaGallery () { - return
; + getAttachmentAspectRatio () { + const attachments = this._properStatus().get('media_attachments'); + + if (attachments.getIn([0, 'type']) === 'video') { + return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`; + } else if (attachments.getIn([0, 'type']) === 'audio') { + return '16 / 9'; + } else { + return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2' + } } - renderLoadingVideoPlayer () { - return
; - } + renderLoadingMediaGallery = () => { + return ( +
+ ); + }; - renderLoadingAudioPlayer () { - return
; - } + renderLoadingVideoPlayer = () => { + return ( +
+ ); + }; + + renderLoadingAudioPlayer = () => { + return ( +
+ ); + }; handleOpenVideo = (options) => { const status = this._properStatus(); @@ -428,7 +444,7 @@ class Status extends ImmutablePureComponent { }; render () { - const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, quoteMuted, contextType } = this.props; + const { intl, hidden, featured, unread, showThread, scrollKey, pictureInPicture, previousId, quoteMuted, nextInReplyToId, rootId, contextType } = this.props; let { status, account, ...other } = this.props; @@ -525,69 +541,61 @@ class Status extends ImmutablePureComponent {
); } - const media = (status, quote = false) => { if (pictureInPicture.get('inUse')) { - return ; + return ; } else if (status.get('media_attachments').size > 0) { const language = status.getIn(['translation', 'language']) || status.get('language'); - if (this.props.muted) { - return ( - - ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { const attachment = status.getIn(['media_attachments', 0]); const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); - return ( - - {Component => ( - - )} - - ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const attachment = status.getIn(['media_attachments', 0]); - const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); + return ( + + {Component => ( + + )} + + ); + } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + const attachment = status.getIn(['media_attachments', 0]); + const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); return ( - + {Component => ( )} @@ -605,15 +613,15 @@ class Status extends ImmutablePureComponent { onOpenMedia={this.handleOpenMedia} cacheWidth={this.props.cacheMediaWidth} defaultWidth={this.props.cachedMediaWidth} - visible={quote ? this.state.showQuoteMedia : this.state.showMedia} - onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility} + visible={this.state.showMedia} + onToggleVisibility={this.handleToggleMediaVisibility} quote={quote} /> )} ); } - } else if (status.get('spoiler_text').length === 0 && status.get('card') && !this.props.muted) { + } else if (status.get('spoiler_text').length === 0 && status.get('card')) { return ( ); } - - return null; + return null; }; const statusAvatar = (status, account) => { @@ -635,7 +642,7 @@ class Status extends ImmutablePureComponent { return ; } }; - + const identity = (status, account) => ( @@ -683,7 +698,7 @@ class Status extends ImmutablePureComponent { onCollapsedToggle={this.handleCollapsedToggle} /> - {media(status)} + {media} {quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router, contextType)} @@ -696,4 +711,4 @@ class Status extends ImmutablePureComponent { } -export default connect(mapStateToProps)(injectIntl(Status)); +export default injectIntl(Status); \ No newline at end of file diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index 2d894d333..e6c66597f 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -33,7 +33,7 @@ const messages = defineMessages({ cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, quote: { id: 'status.quote', defaultMessage: 'Quote' }, cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, - favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, + favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, open: { id: 'status.open', defaultMessage: 'Expand this status' }, diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index eb728b97e..fea108c23 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -57,7 +57,7 @@ const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, - redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, + redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, @@ -307,7 +307,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ modalProps: { type, accountId: status.getIn(['account', 'id']), - url: status.get('url'), + url: status.get('uri'), }, })); }, diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index ececb86c0..d351e210f 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -10,6 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Avatar } from 'mastodon/components/avatar'; +import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; import Button from 'mastodon/components/button'; import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters'; import { Icon } from 'mastodon/components/icon'; @@ -48,7 +49,7 @@ const messages = defineMessages({ pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, @@ -370,16 +371,18 @@ class Header extends ImmutablePureComponent { const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); const isIndexable = !account.get('noindex'); - let badge; + const badges = []; if (account.get('bot')) { - badge = (
); + badges.push(); } else if (account.get('group')) { - badge = (
); - } else { - badge = null; + badges.push(); } + account.get('roles', []).forEach((role) => { + badges.push({role.get('name')}} domain={domain} />); + }); + return (
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && } @@ -414,13 +417,19 @@ class Header extends ImmutablePureComponent {

- {badge} + @{acct} {lockedIcon}

+ {badges.length > 0 && ( +
+ {badges} +
+ )} + {!(suspended || hidden) && (
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.jsx b/app/javascript/mastodon/features/account_gallery/components/media_item.jsx index f60be5d54..63fbac679 100644 --- a/app/javascript/mastodon/features/account_gallery/components/media_item.jsx +++ b/app/javascript/mastodon/features/account_gallery/components/media_item.jsx @@ -128,7 +128,11 @@ export default class MediaItem extends ImmutablePureComponent {
{content} - {label && {label}} + {label && ( +
+ {label} +
+ )}
); } diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx index 2b3a66c55..df5427c30 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.jsx @@ -83,7 +83,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ modalProps: { type: 'follow', accountId: account.get('id'), - url: account.get('url'), + url: account.get('uri'), }, })); }, diff --git a/app/javascript/mastodon/features/audio/index.jsx b/app/javascript/mastodon/features/audio/index.jsx index f1718d98c..ce693a8c5 100644 --- a/app/javascript/mastodon/features/audio/index.jsx +++ b/app/javascript/mastodon/features/audio/index.jsx @@ -475,6 +475,7 @@ class Audio extends PureComponent { const progress = Math.min((currentTime / duration) * 100, 100); let warning; + if (sensitive) { warning = ; } else { @@ -520,7 +521,10 @@ class Audio extends PureComponent {
diff --git a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx index 936dee12e..b0c90a430 100644 --- a/app/javascript/mastodon/features/bookmarked_statuses/index.jsx +++ b/app/javascript/mastodon/features/bookmarked_statuses/index.jsx @@ -86,7 +86,6 @@ class Bookmarks extends ImmutablePureComponent { onClick={this.handleHeaderClick} pinned={pinned} multiColumn={multiColumn} - showBackButton /> - -
+
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx index 371bed905..5af38da43 100644 --- a/app/javascript/mastodon/features/compose/components/navigation_bar.jsx +++ b/app/javascript/mastodon/features/compose/components/navigation_bar.jsx @@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { Avatar } from '../../../components/avatar'; -import { IconButton } from '../../../components/icon_button'; import ActionBar from './action_bar'; @@ -21,23 +20,27 @@ export default class NavigationBar extends ImmutablePureComponent { }; render () { + const username = this.props.account.get('acct') return (
- - {this.props.account.get('acct')} + + {username}
- - @{this.props.account.get('acct')} - + + + @{username} + + - + + +
-
diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index 7badb0774..682f8d3c8 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -139,10 +139,6 @@ class Search extends PureComponent { this.setState({ expanded: false, selectedOption: -1 }); }; - findTarget = () => { - return this.searchForm; - }; - handleHashtagClick = () => { const { router } = this.context; const { value, onClickSearchResult } = this.props; diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx index 73ec99c14..80dd5200f 100644 --- a/app/javascript/mastodon/features/explore/components/story.jsx +++ b/app/javascript/mastodon/features/explore/components/story.jsx @@ -1,10 +1,13 @@ import PropTypes from 'prop-types'; import { PureComponent } 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'; @@ -13,10 +16,15 @@ export default class Story extends PureComponent { 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, }; state = { @@ -26,23 +34,23 @@ export default class Story extends PureComponent { handleImageLoad = () => this.setState({ thumbnailLoaded: true }); render () { - const { url, title, publisher, sharedTimes, thumbnail, blurhash } = this.props; + const { expanded, url, title, lang, publisher, author, publishedAt, sharedTimes, thumbnail, thumbnailDescription, blurhash } = this.props; const { thumbnailLoaded } = this.state; return ( - +
-
{publisher ? publisher : }
-
{title ? title : }
-
{typeof sharedTimes === 'number' ? : }
+
{publisher ? {publisher} : }{publishedAt && <> · }
+
{title ? title : }
+
{author && <>{author} }} /> · }{typeof sharedTimes === 'number' ? : }
{thumbnail ? ( <>
- + {thumbnailDescription} ) : }
diff --git a/app/javascript/mastodon/features/explore/links.jsx b/app/javascript/mastodon/features/explore/links.jsx index 8b199bf47..663aa6d80 100644 --- a/app/javascript/mastodon/features/explore/links.jsx +++ b/app/javascript/mastodon/features/explore/links.jsx @@ -55,14 +55,19 @@ class Links extends PureComponent {
{banner} - {isLoading ? () : links.map(link => ( + {isLoading ? () : links.map((link, i) => ( ))} diff --git a/app/javascript/mastodon/features/explore/statuses.jsx b/app/javascript/mastodon/features/explore/statuses.jsx index 043492fb8..f32a4a536 100644 --- a/app/javascript/mastodon/features/explore/statuses.jsx +++ b/app/javascript/mastodon/features/explore/statuses.jsx @@ -47,7 +47,7 @@ class Statuses extends PureComponent { return ( <> - + ({ @@ -74,7 +74,7 @@ class Favourites extends ImmutablePureComponent { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; - const emptyMessage = ; + const emptyMessage = ; return ( @@ -86,7 +86,6 @@ class Favourites extends ImmutablePureComponent { onClick={this.handleHeaderClick} pinned={pinned} multiColumn={multiColumn} - showBackButton /> ; + const emptyMessage = ; return ( diff --git a/app/javascript/mastodon/features/getting_started/index.jsx b/app/javascript/mastodon/features/getting_started/index.jsx index 85fd6f120..f0cd70d7a 100644 --- a/app/javascript/mastodon/features/getting_started/index.jsx +++ b/app/javascript/mastodon/features/getting_started/index.jsx @@ -32,7 +32,7 @@ const messages = defineMessages({ bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx new file mode 100644 index 000000000..46050309f --- /dev/null +++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.jsx @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; + +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; + +import ImmutablePropTypes from 'react-immutable-proptypes'; + +import Button from 'mastodon/components/button'; +import { ShortNumber } from 'mastodon/components/short_number'; + +const messages = defineMessages({ + followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, + unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, +}); + +const usesRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +const peopleRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +const usesTodayRenderer = (displayNumber, pluralReady) => ( + {displayNumber}, + }} + /> +); + +export const HashtagHeader = injectIntl(({ tag, intl, disabled, onClick }) => { + if (!tag) { + return null; + } + + const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]); + const dividingCircle = {' · '}; + + return ( +
+
+

#{tag.get('name')}

+
+ +
+ + {dividingCircle} + + {dividingCircle} + +
+
+ ); +}); + +HashtagHeader.propTypes = { + tag: ImmutablePropTypes.map, + disabled: PropTypes.bool, + onClick: PropTypes.func, + intl: PropTypes.object, +}; \ No newline at end of file diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.jsx b/app/javascript/mastodon/features/hashtag_timeline/index.jsx index b6ed29d8d..7079f55e9 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.jsx +++ b/app/javascript/mastodon/features/hashtag_timeline/index.jsx @@ -1,9 +1,8 @@ import PropTypes from 'prop-types'; import { PureComponent } from 'react'; -import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; @@ -17,17 +16,12 @@ import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/t import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines'; import Column from 'mastodon/components/column'; import ColumnHeader from 'mastodon/components/column_header'; -import { Icon } from 'mastodon/components/icon'; import StatusListContainer from '../ui/containers/status_list_container'; +import { HashtagHeader } from './components/hashtag_header'; import ColumnSettingsContainer from './containers/column_settings_container'; -const messages = defineMessages({ - followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' }, - unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' }, -}); - const mapStateToProps = (state, props) => ({ hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, tag: state.getIn(['tags', props.params.id]), @@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent { hasUnread: PropTypes.bool, tag: ImmutablePropTypes.map, multiColumn: PropTypes.bool, - intl: PropTypes.object, }; handlePin = () => { @@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent { }; render () { - const { hasUnread, columnId, multiColumn, tag, intl } = this.props; + const { hasUnread, columnId, multiColumn, tag } = this.props; const { id, local } = this.props.params; const pinned = !!columnId; const { signedIn } = this.context.identity; - let followButton; - - if (tag) { - const following = tag.get('following'); - - const classes = classNames('column-header__button', { - active: following, - }); - - followButton = ( - - ); - } - return ( {columnId && } } + alwaysPrepend trackScroll={!pinned} scrollKey={`hashtag_timeline-${columnId}`} timelineId={`hashtag:${id}${local ? ':local' : ''}`} @@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent { } -export default connect(mapStateToProps)(injectIntl(HashtagTimeline)); +export default connect(mapStateToProps)(HashtagTimeline); diff --git a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx index 47113d9b8..9eeec00e3 100644 --- a/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/explore_prompt.tsx @@ -22,7 +22,7 @@ export const ExplorePrompt = () => (

diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index 0f17a0896..84e430925 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -1,15 +1,23 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import classNames from 'classnames'; import { connect } from 'react-redux'; +import { throttle, escapeRegExp } from 'lodash'; + import { openModal, closeModal } from 'mastodon/actions/modal'; +import api from 'mastodon/api'; +import Button from 'mastodon/components/button'; import { Icon } from 'mastodon/components/icon'; -import { registrationsOpen } from 'mastodon/initial_state'; +import { registrationsOpen, sso_redirect } from 'mastodon/initial_state'; + +const messages = defineMessages({ + loginPrompt: { id: 'interaction_modal.login.prompt', defaultMessage: 'Domain of your home server, e.g. mastodon.social' }, +}); const mapStateToProps = (state, { accountId }) => ({ displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), @@ -19,70 +27,271 @@ const mapStateToProps = (state, { accountId }) => ({ const mapDispatchToProps = (dispatch) => ({ onSignupClick() { dispatch(closeModal({ - modalType: undefined, - ignoreFocus: false, - })); + modalType: undefined, + ignoreFocus: false, + })); dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); }, }); -class Copypaste extends PureComponent { +const PERSISTENCE_KEY = 'mastodon_home'; + +const isValidDomain = value => { + const url = new URL('https:///path'); + url.hostname = value; + return url.hostname === value; +}; + +const valueToDomain = value => { + // If the user starts typing an URL + if (/^https?:\/\//.test(value)) { + try { + const url = new URL(value); + + // Consider that if there is a path, the URL is more meaningful than a bare domain + if (url.pathname.length > 1) { + return ''; + } + + return url.host; + } catch { + return undefined; + } + // If the user writes their full handle including username + } else if (value.includes('@')) { + if (value.replace(/^@/, '').split('@').length > 2) { + return undefined; + } + return ''; + } + + return value; +}; + +const addInputToOptions = (value, options) => { + value = value.trim(); + + if (value.includes('.') && isValidDomain(value)) { + return [value].concat(options.filter((x) => x !== value)); + } + + return options; +}; + +class LoginForm extends React.PureComponent { static propTypes = { - value: PropTypes.string, + resourceUrl: PropTypes.string, + intl: PropTypes.object.isRequired, }; state = { - copied: false, + value: localStorage ? (localStorage.getItem(PERSISTENCE_KEY) || '') : '', + expanded: false, + selectedOption: -1, + isLoading: false, + isSubmitting: false, + error: false, + options: [], + networkOptions: [], }; setRef = c => { this.input = c; }; - handleInputClick = () => { - this.setState({ copied: false }); - this.input.focus(); - this.input.select(); - this.input.setSelectionRange(0, this.input.value.length); + handleChange = ({ target }) => { + this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions()); }; - handleButtonClick = () => { - const { value } = this.props; - navigator.clipboard.writeText(value); - this.input.blur(); - this.setState({ copied: true }); - this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + handleMessage = (event) => { + const { resourceUrl } = this.props; + + if (event.origin !== window.origin || event.source !== this.iframeRef.contentWindow) { + return; + } + + if (event.data?.type === 'fetchInteractionURL-failure') { + this.setState({ isSubmitting: false, error: true }); + } else if (event.data?.type === 'fetchInteractionURL-success') { + if (/^https?:\/\//.test(event.data.template)) { + if (localStorage) { + localStorage.setItem(PERSISTENCE_KEY, event.data.uri_or_domain); + } + + window.location.href = event.data.template.replace('{uri}', encodeURIComponent(resourceUrl)); + } else { + this.setState({ isSubmitting: false, error: true }); + } + } }; - componentWillUnmount () { - if (this.timeout) clearTimeout(this.timeout); + componentDidMount () { + window.addEventListener('message', this.handleMessage); } + componentWillUnmount () { + window.removeEventListener('message', this.handleMessage); + } + + handleSubmit = () => { + const { value } = this.state; + + this.setState({ isSubmitting: true }); + + this.iframeRef.contentWindow.postMessage({ + type: 'fetchInteractionURL', + uri_or_domain: value.trim(), + }, window.origin); + }; + + setIFrameRef = (iframe) => { + this.iframeRef = iframe; + } + + handleFocus = () => { + this.setState({ expanded: true }); + }; + + handleBlur = () => { + this.setState({ expanded: false }); + }; + + handleKeyDown = (e) => { + const { options, selectedOption } = this.state; + + switch(e.key) { + case 'ArrowDown': + e.preventDefault(); + + if (options.length > 0) { + this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) }); + } + + break; + case 'ArrowUp': + e.preventDefault(); + + if (options.length > 0) { + this.setState({ selectedOption: Math.max(selectedOption - 1, -1) }); + } + + break; + case 'Enter': + e.preventDefault(); + + if (selectedOption === -1) { + this.handleSubmit(); + } else if (options.length > 0) { + this.setState({ value: options[selectedOption], error: false }, () => this.handleSubmit()); + } + + break; + } + }; + + handleOptionClick = e => { + const index = Number(e.currentTarget.getAttribute('data-index')); + const option = this.state.options[index]; + + e.preventDefault(); + this.setState({ selectedOption: index, value: option, error: false }, () => this.handleSubmit()); + }; + + _loadOptions = throttle(() => { + const { value } = this.state; + + const domain = valueToDomain(value.trim()); + + if (typeof domain === 'undefined') { + this.setState({ options: [], networkOptions: [], isLoading: false, error: true }); + return; + } + + if (domain.length === 0) { + this.setState({ options: [], networkOptions: [], isLoading: false }); + return; + } + + api().get('/api/v1/peers/search', { params: { q: domain } }).then(({ data }) => { + if (!data) { + data = []; + } + + this.setState((state) => ({ networkOptions: data, options: addInputToOptions(state.value, data), isLoading: false })); + }).catch(() => { + this.setState({ isLoading: false }); + }); + }, 200, { leading: true, trailing: true }); + render () { - const { value } = this.props; - const { copied } = this.state; + const { intl } = this.props; + const { value, expanded, options, selectedOption, error, isSubmitting } = this.state; + const domain = (valueToDomain(value) || '').trim(); + const domainRegExp = new RegExp(`(${escapeRegExp(domain)})`, 'gi'); + const hasPopOut = domain.length > 0 && options.length > 0; return ( -
- + +