Merge branch 'main' of https://github.com/mastodon/mastodon into features/main

This commit is contained in:
YoheiZuho 2023-08-14 14:25:03 +00:00
commit c0cf1ebdd3
764 changed files with 17703 additions and 21383 deletions

View File

@ -1,3 +0,0 @@
---
ignore:
- CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications

View File

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

View File

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

View File

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

View File

@ -1,20 +1,21 @@
{ {
$schema: 'https://docs.renovatebot.com/renovate-schema.json', $schema: 'https://docs.renovatebot.com/renovate-schema.json',
extends: [ extends: [
'config:base', 'config:recommended',
':dependencyDashboard',
':labels(dependencies)', ':labels(dependencies)',
':maintainLockFilesMonthly', // update non-direct dependencies monthly ':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, // 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 // 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 // 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. // 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: [ packageRules: [
{ {
// Ignore major version bumps for these node packages // Require Dependency Dashboard Approval for major version bumps of these node packages
matchManagers: ['npm'], matchManagers: ['npm'],
matchPackageNames: [ matchPackageNames: [
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
@ -41,10 +42,10 @@
'react-router-dom', 'react-router-dom',
], ],
matchUpdateTypes: ['major'], 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'], matchManagers: ['bundler'],
matchPackageNames: [ matchPackageNames: [
'rack', // Needs to be synced with Rails version 'rack', // Needs to be synced with Rails version
@ -55,7 +56,7 @@
'redis', // Requires manual upgrade and sync with Sidekiq version 'redis', // Requires manual upgrade and sync with Sidekiq version
], ],
matchUpdateTypes: ['major'], matchUpdateTypes: ['major'],
enabled: false, "dependencyDashboardApproval": true
}, },
{ {
// Update Github Actions and Docker images weekly // Update Github Actions and Docker images weekly
@ -63,25 +64,25 @@
extends: ['schedule:weekly'], 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'], matchManagers: ['dockerfile'],
matchPackageNames: ['moritzheiber/ruby-jemalloc'], matchPackageNames: ['moritzheiber/ruby-jemalloc'],
matchUpdateTypes: ['minor', 'major'], 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'], matchManagers: ['dockerfile'],
matchPackageNames: ['node'], matchPackageNames: ['node'],
matchUpdateTypes: ['major'], 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'], matchManagers: ['docker-compose'],
matchPackageNames: ['postgres'], matchPackageNames: ['postgres'],
matchUpdateTypes: ['major'], matchUpdateTypes: ['major'],
enabled: false, "dependencyDashboardApproval": true
}, },
{ {
// Update devDependencies every week, with one grouped PR // Update devDependencies every week, with one grouped PR

View File

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

View File

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

View File

@ -3,58 +3,40 @@ on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '0 2 * * *' # run at 2 AM UTC - cron: '0 2 * * *' # run at 2 AM UTC
permissions: permissions:
contents: read contents: read
packages: write packages: write
jobs: jobs:
build-nightly-image: compute-suffix:
runs-on: ubuntu-latest runs-on: ubuntu-latest
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps: steps:
- uses: actions/checkout@v3 - id: version_vars
- uses: hadolint/hadolint-action@v3.1.0 env:
- uses: docker/setup-qemu-action@v2 TZ: Etc/UTC
- 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
run: | 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 build-image:
with: needs: compute-suffix
context: . uses: ./.github/workflows/build-container-image.yml
build-args: MASTODON_VERSION_SUFFIX=${{ steps.version_vars.outputs.mastodon_version_suffix }} with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
provenance: false use_native_arm64_builder: true
builder: ${{ steps.buildx.outputs.name }} push_to_images: |
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }} tootsuite/mastodon
tags: ${{ steps.meta.outputs.tags }} ghcr.io/mastodon/mastodon
labels: ${{ steps.meta.outputs.labels }} # The `+` is important here, result will be v4.1.2+nightly-2022-03-05
cache-from: type=gha version_suffix: +${{ needs.compute-suffix.outputs.suffix }}
cache-to: type=gha,mode=max 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

41
.github/workflows/build-push-pr.yml vendored Normal file
View File

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

25
.github/workflows/build-releases.yml vendored Normal file
View File

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

76
.github/workflows/crowdin-download.yml vendored Normal file
View File

@ -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 <noreply@github.com>'
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

35
.github/workflows/crowdin-upload.yml vendored Normal file
View File

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

21
.github/workflows/test-image-build.yml vendored Normal file
View File

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

View File

@ -107,6 +107,10 @@ jobs:
PAM_ENABLED: true PAM_ENABLED: true
PAM_DEFAULT_SERVICE: pam_test PAM_DEFAULT_SERVICE: pam_test
PAM_CONTROLLED_SERVICE: pam_test_controlled PAM_CONTROLLED_SERVICE: pam_test_controlled
OIDC_ENABLED: true
OIDC_SCOPE: read
SAML_ENABLED: true
CAS_ENABLED: true
BUNDLE_WITH: 'pam_authentication test' BUNDLE_WITH: 'pam_authentication test'
CI_JOBS: ${{ matrix.ci_job }}/4 CI_JOBS: ${{ matrix.ci_job }}/4
@ -149,3 +153,100 @@ jobs:
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'
- run: bundle exec rake rspec_chunked - 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/

View File

@ -1,13 +1,13 @@
# This configuration was generated by # This configuration was generated by
# `haml-lint --auto-gen-config` # `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 # The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base. # one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again. # versions of Haml-Lint, may require this file to be generated again.
linters: linters:
# Offense count: 959 # Offense count: 951
LineLength: LineLength:
enabled: false enabled: false
@ -15,7 +15,7 @@ linters:
UnnecessaryStringOutput: UnnecessaryStringOutput:
enabled: false enabled: false
# Offense count: 67 # Offense count: 57
RuboCop: RuboCop:
enabled: false enabled: false
@ -26,20 +26,18 @@ linters:
- 'app/views/admin/reports/show.html.haml' - 'app/views/admin/reports/show.html.haml'
- 'app/views/disputes/strikes/show.html.haml' - 'app/views/disputes/strikes/show.html.haml'
# Offense count: 41 # Offense count: 32
InstanceVariables: InstanceVariables:
exclude: exclude:
- 'app/views/admin/reports/_actions.html.haml' - 'app/views/admin/reports/_actions.html.haml'
- 'app/views/admin/roles/_form.html.haml' - 'app/views/admin/roles/_form.html.haml'
- 'app/views/admin/webhooks/_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/registrations/_status.html.haml'
- 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml' - 'app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml'
- 'app/views/authorize_interactions/_post_follow_actions.html.haml' - 'app/views/authorize_interactions/_post_follow_actions.html.haml'
- 'app/views/invites/_form.html.haml' - 'app/views/invites/_form.html.haml'
- 'app/views/relationships/_account.html.haml' - 'app/views/relationships/_account.html.haml'
- 'app/views/shared/_og.html.haml' - 'app/views/shared/_og.html.haml'
- 'app/views/statuses/_status.html.haml'
# Offense count: 3 # Offense count: 3
IdNames: IdNames:

View File

@ -38,14 +38,7 @@ Layout/FirstHashElementIndentation:
# Reason: Currently disabled in .rubocop_todo.yml # Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength # https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength: Layout/LineLength:
AllowedPatterns: Max: 320 # Default of 120 causes a duplicate entry in generated todo file
# Allow comments to be long lines
- !ruby/regexp / \# .*$/
- !ruby/regexp /^\# .*$/
Exclude:
- 'lib/mastodon/cli/*.rb'
- db/*migrate/**/*
- db/seeds/**/*
# Reason: # Reason:
# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier # https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier
@ -131,12 +124,6 @@ RSpec/FilePath:
Exclude: Exclude:
- 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder - 'spec/config/initializers/rack_attack_spec.rb' # namespaces usually have separate folder
- 'spec/lib/sanitize_config_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: # Reason:
# https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject # https://docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecnamedsubject

View File

@ -39,6 +39,13 @@ Layout/LeadingCommentSpace:
- 'config/application.rb' - 'config/application.rb'
- 'config/initializers/omniauth.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). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space # SupportedStyles: require_no_space, require_space
@ -112,7 +119,6 @@ Lint/UselessAssignment:
- 'config/initializers/omniauth.rb' - 'config/initializers/omniauth.rb'
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb' - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.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/api/v1/favourites_controller_spec.rb'
- 'spec/controllers/concerns/account_controller_concern_spec.rb' - 'spec/controllers/concerns/account_controller_concern_spec.rb'
- 'spec/helpers/jsonld_helper_spec.rb' - 'spec/helpers/jsonld_helper_spec.rb'
@ -127,15 +133,9 @@ Lint/UselessAssignment:
- 'spec/services/resolve_url_service_spec.rb' - 'spec/services/resolve_url_service_spec.rb'
- 'spec/views/statuses/show.html.haml_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. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 150 Max: 146
# Configuration parameters: CountBlocks, Max. # Configuration parameters: CountBlocks, Max.
Metrics/BlockNesting: Metrics/BlockNesting:
@ -150,13 +150,6 @@ Metrics/CyclomaticComplexity:
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 27 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. # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
# SupportedStyles: snake_case, normalcase, non_integer # SupportedStyles: snake_case, normalcase, non_integer
# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
@ -171,14 +164,6 @@ Naming/VariableNumber:
- 'spec/models/domain_block_spec.rb' - 'spec/models/domain_block_spec.rb'
- 'spec/models/user_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: RSpec/AnyInstance:
Exclude: Exclude:
- 'spec/controllers/activitypub/inboxes_controller_spec.rb' - 'spec/controllers/activitypub/inboxes_controller_spec.rb'
@ -198,41 +183,6 @@ RSpec/AnyInstance:
- 'spec/workers/activitypub/delivery_worker_spec.rb' - 'spec/workers/activitypub/delivery_worker_spec.rb'
- 'spec/workers/web/push_notification_worker_spec.rb' - 'spec/workers/web/push_notification_worker_spec.rb'
# This cop supports unsafe autocorrection (--autocorrect-all).
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. # Configuration parameters: CountAsOne.
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 22 Max: 22
@ -285,7 +235,6 @@ RSpec/LetSetup:
- 'spec/controllers/api/v2/admin/accounts_controller_spec.rb' - 'spec/controllers/api/v2/admin/accounts_controller_spec.rb'
- 'spec/controllers/api/v2/filters/keywords_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/statuses_controller_spec.rb'
- 'spec/controllers/api/v2/filters_controller_spec.rb'
- 'spec/controllers/auth/confirmations_controller_spec.rb' - 'spec/controllers/auth/confirmations_controller_spec.rb'
- 'spec/controllers/auth/passwords_controller_spec.rb' - 'spec/controllers/auth/passwords_controller_spec.rb'
- 'spec/controllers/auth/sessions_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/oauth/tokens_controller_spec.rb'
- 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/controllers/settings/imports_controller_spec.rb'
- 'spec/lib/activitypub/activity/delete_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/lib/vacuum/preview_cards_vacuum_spec.rb'
- 'spec/models/account_spec.rb' - 'spec/models/account_spec.rb'
- 'spec/models/account_statuses_cleanup_policy_spec.rb' - 'spec/models/account_statuses_cleanup_policy_spec.rb'
@ -365,43 +315,6 @@ Rails/ApplicationController:
Exclude: Exclude:
- 'app/controllers/health_controller.rb' - '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. # Configuration parameters: Include.
# Include: db/**/*.rb # Include: db/**/*.rb
Rails/CreateTableWithTimestamps: Rails/CreateTableWithTimestamps:
@ -676,7 +589,7 @@ Style/FetchEnvVar:
- 'app/lib/translation_service.rb' - 'app/lib/translation_service.rb'
- 'config/environments/development.rb' - 'config/environments/development.rb'
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/2_whitelist_mode.rb' - 'config/initializers/2_limited_federation_mode.rb'
- 'config/initializers/blacklists.rb' - 'config/initializers/blacklists.rb'
- 'config/initializers/cache_buster.rb' - 'config/initializers/cache_buster.rb'
- 'config/initializers/content_security_policy.rb' - 'config/initializers/content_security_policy.rb'
@ -838,8 +751,6 @@ Style/RedundantConstantBase:
Exclude: Exclude:
- 'config/environments/production.rb' - 'config/environments/production.rb'
- 'config/initializers/sidekiq.rb' - 'config/initializers/sidekiq.rb'
- 'config/locales/sr-Latn.rb'
- 'config/locales/sr.rb'
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeForConstants. # Configuration parameters: SafeForConstants.
@ -941,9 +852,3 @@ Style/WordArray:
- 'config/initializers/cors.rb' - 'config/initializers/cors.rb'
- 'spec/controllers/settings/imports_controller_spec.rb' - 'spec/controllers/settings/imports_controller_spec.rb'
- 'spec/models/form/import_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

View File

@ -2,6 +2,268 @@
All notable changes to this project will be documented in this file. 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 ## [4.1.4] - 2023-07-07
### Fixed ### Fixed

29
Gemfile
View File

@ -18,6 +18,7 @@ gem 'aws-sdk-s3', '~> 1.123', require: false
gem 'fog-core', '<= 2.4.0' gem 'fog-core', '<= 2.4.0'
gem 'fog-openstack', '~> 0.3', require: false gem 'fog-openstack', '~> 0.3', require: false
gem 'kt-paperclip', '~> 7.2' gem 'kt-paperclip', '~> 7.2'
gem 'md-paperclip-azure', '~> 2.2', require: false
gem 'blurhash', '~> 0.1' gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10' gem 'active_model_serializers', '~> 0.10'
@ -34,11 +35,14 @@ group :pam_authentication, optional: true do
end end
gem 'net-ldap', '~> 0.18' 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_openid_connect', '~> 0.6.1'
gem 'omniauth', '~> 1.9' gem 'omniauth', '~> 2.0'
gem 'omniauth-rails_csrf_protection', '~> 0.1' gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'color_diff', '~> 0.1' gem 'color_diff', '~> 0.1'
gem 'discard', '~> 1.2' gem 'discard', '~> 1.2'
@ -55,8 +59,9 @@ gem 'httplog', '~> 1.6.2'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar' gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
gem 'nokogiri', '~> 1.15' gem 'nokogiri', '~> 1.15'
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.14' gem 'ox', '~> 2.14'
gem 'parslet' gem 'parslet'
@ -98,9 +103,6 @@ gem 'rdf-normalize', '~> 0.5'
gem 'private_address_check', '~> 0.5' gem 'private_address_check', '~> 0.5'
group :test do group :test do
# RSpec runner for rails
gem 'rspec-rails', '~> 6.0'
# Used to split testing into chunks in CI # Used to split testing into chunks in CI
gem 'rspec_chunked', '~> 0.6' gem 'rspec_chunked', '~> 0.6'
@ -112,6 +114,10 @@ group :test do
# Browser integration testing # Browser integration testing
gem 'capybara', '~> 3.39' 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 # Used to mock environment variables
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
@ -172,10 +178,17 @@ group :development do
# Validate missing i18n keys # Validate missing i18n keys
gem 'i18n-tasks', '~> 1.0', require: false gem 'i18n-tasks', '~> 1.0', require: false
end
group :development, :test do
# Profiling tools # Profiling tools
gem 'memory_profiler', require: false gem 'memory_profiler', require: false
gem 'ruby-prof', require: false
gem 'stackprof', require: false gem 'stackprof', require: false
gem 'test-prof'
# RSpec runner for rails
gem 'rspec-rails', '~> 6.0'
end end
group :production do group :production do

View File

@ -7,6 +7,17 @@ GIT
hkdf (~> 0.2) hkdf (~> 0.2)
jwt (~> 2.0) 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 GIT
remote: https://github.com/mastodon/rails-settings-cached.git remote: https://github.com/mastodon/rails-settings-cached.git
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
@ -15,50 +26,60 @@ GIT
rails-settings-cached (0.6.6) rails-settings-cached (0.6.6)
rails (>= 4.2.0) 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 GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.6) actioncable (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.6) actionmailbox (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
activejob (= 7.0.6) activejob (= 7.0.7)
activerecord (= 7.0.6) activerecord (= 7.0.7)
activestorage (= 7.0.6) activestorage (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.6) actionmailer (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
actionview (= 7.0.6) actionview (= 7.0.7)
activejob (= 7.0.6) activejob (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (7.0.6) actionpack (7.0.7)
actionview (= 7.0.6) actionview (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
rack (~> 2.0, >= 2.2.4) rack (~> 2.0, >= 2.2.4)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.6) actiontext (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
activerecord (= 7.0.6) activerecord (= 7.0.7)
activestorage (= 7.0.6) activestorage (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.6) actionview (7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
@ -68,22 +89,22 @@ GEM
activemodel (>= 4.1, < 7.1) activemodel (>= 4.1, < 7.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.0.6) activejob (7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.6) activemodel (7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
activerecord (7.0.6) activerecord (7.0.7)
activemodel (= 7.0.6) activemodel (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
activestorage (7.0.6) activestorage (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
activejob (= 7.0.6) activejob (= 7.0.7)
activerecord (= 7.0.6) activerecord (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (7.0.6) activesupport (7.0.7)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
@ -103,8 +124,8 @@ GEM
attr_required (1.0.1) attr_required (1.0.1)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.786.0) aws-partitions (1.793.0)
aws-sdk-core (3.178.0) aws-sdk-core (3.180.3)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
@ -112,12 +133,20 @@ GEM
aws-sdk-kms (1.71.0) aws-sdk-kms (1.71.0)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.177.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.130.0) aws-sdk-s3 (1.132.1)
aws-sdk-core (~> 3, >= 3.177.0) aws-sdk-core (~> 3, >= 3.179.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.6) aws-sigv4 (~> 1.6)
aws-sigv4 (1.6.0) aws-sigv4 (1.6.0)
aws-eventstream (~> 1, >= 1.0.2) 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) bcrypt (3.1.18)
better_errors (2.10.1) better_errors (2.10.1)
erubi (>= 1.0.0) erubi (>= 1.0.0)
@ -136,7 +165,7 @@ GEM
blurhash (0.1.7) blurhash (0.1.7)
bootsnap (1.16.0) bootsnap (1.16.0)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.0.0) brakeman (6.0.1)
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
concurrent-ruby (~> 1.0, >= 1.0.5) concurrent-ruby (~> 1.0, >= 1.0.5)
@ -191,6 +220,10 @@ GEM
crass (1.0.6) crass (1.0.6)
css_parser (1.14.0) css_parser (1.14.0)
addressable 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) date (3.3.3)
debug_inspector (1.1.0) debug_inspector (1.1.0)
devise (4.9.2) devise (4.9.2)
@ -261,6 +294,8 @@ GEM
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fast_blank (1.0.1) fast_blank (1.0.1)
fastimage (2.2.7) fastimage (2.2.7)
ffi (1.15.5) ffi (1.15.5)
@ -297,7 +332,7 @@ GEM
activesupport (>= 5.1) activesupport (>= 5.1)
haml (>= 4.0.6) haml (>= 4.0.6)
railties (>= 5.1) railties (>= 5.1)
haml_lint (0.48.0) haml_lint (0.49.3)
haml (>= 4.0, < 6.2) haml (>= 4.0, < 6.2)
parallel (~> 1.10) parallel (~> 1.10)
rainbow rainbow
@ -393,7 +428,7 @@ GEM
llhttp-ffi (0.4.0) llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
lograge (0.12.0) lograge (0.13.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
@ -410,20 +445,26 @@ GEM
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
matrix (0.4.2) 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) memory_profiler (1.0.1)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.5.0)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0808)
mini_mime (1.1.2) mini_mime (1.1.5)
mini_portile2 (2.8.2) mini_portile2 (2.8.4)
minitest (5.18.1) minitest (5.19.0)
msgpack (1.7.1) msgpack (1.7.1)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
net-http (0.3.2) net-http (0.3.2)
uri uri
net-imap (0.3.6) net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.3.7)
date date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.18.0)
@ -441,19 +482,16 @@ GEM
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
oj (3.15.0) oj (3.15.0)
omniauth (1.9.2) omniauth (2.1.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 1.6.2, < 3) rack (>= 2.2.3)
omniauth-cas (2.0.0) rack-protection
addressable (~> 2.3) omniauth-rails_csrf_protection (1.0.1)
nokogiri (~> 1.5)
omniauth (~> 1.2)
omniauth-rails_csrf_protection (0.1.2)
actionpack (>= 4.2) actionpack (>= 4.2)
omniauth (>= 1.3.1) omniauth (~> 2.0)
omniauth-saml (1.10.3) omniauth-saml (2.1.0)
omniauth (~> 1.3, >= 1.3.2) omniauth (~> 2.0)
ruby-saml (~> 1.9) ruby-saml (~> 1.12)
omniauth_openid_connect (0.6.1) omniauth_openid_connect (0.6.1)
omniauth (>= 1.9, < 3) omniauth (>= 1.9, < 3)
openid_connect (~> 1.1) openid_connect (~> 1.1)
@ -500,9 +538,9 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.1) racc (1.7.1)
rack (2.2.7) rack (2.2.8)
rack-attack (6.6.1) rack-attack (6.7.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 4)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-oauth2 (1.21.3) rack-oauth2 (1.21.3)
@ -511,30 +549,33 @@ GEM
httpclient httpclient
json-jwt (>= 1.11.0) json-jwt (>= 1.11.0)
rack (>= 2.1.0) rack (>= 2.1.0)
rack-protection (3.0.5)
rack
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.6) rails (7.0.7)
actioncable (= 7.0.6) actioncable (= 7.0.7)
actionmailbox (= 7.0.6) actionmailbox (= 7.0.7)
actionmailer (= 7.0.6) actionmailer (= 7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
actiontext (= 7.0.6) actiontext (= 7.0.7)
actionview (= 7.0.6) actionview (= 7.0.7)
activejob (= 7.0.6) activejob (= 7.0.7)
activemodel (= 7.0.6) activemodel (= 7.0.7)
activerecord (= 7.0.6) activerecord (= 7.0.7)
activestorage (= 7.0.6) activestorage (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.6) railties (= 7.0.7)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3) rails-dom-testing (2.1.1)
activesupport (>= 4.2.0) activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
@ -542,9 +583,9 @@ GEM
rails-i18n (7.0.7) rails-i18n (7.0.7)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.0.6) railties (7.0.7)
actionpack (= 7.0.6) actionpack (= 7.0.7)
activesupport (= 7.0.6) activesupport (= 7.0.7)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -553,7 +594,7 @@ GEM
rake (13.0.6) rake (13.0.6)
rdf (3.2.11) rdf (3.2.11)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.6.0) rdf-normalize (0.6.1)
rdf (~> 3.2) rdf (~> 3.2)
redcarpet (3.6.0) redcarpet (3.6.0)
redis (4.8.1) redis (4.8.1)
@ -567,7 +608,7 @@ GEM
responders (3.1.0) responders (3.1.0)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
rexml (3.2.5) rexml (3.2.6)
rotp (6.2.2) rotp (6.2.2)
rouge (4.1.2) rouge (4.1.2)
rpam2 (4.0.2) rpam2 (4.0.2)
@ -624,6 +665,7 @@ GEM
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
ruby-prof (1.6.3)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.15.0) ruby-saml (1.15.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
@ -640,6 +682,10 @@ GEM
scenic (1.7.0) scenic (1.7.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
railties (>= 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) semantic_range (3.0.0)
sidekiq (6.5.9) sidekiq (6.5.9)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.5, < 3)
@ -680,6 +726,7 @@ GEM
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
stackprof (0.2.25) stackprof (0.2.25)
statsd-ruby (1.5.0)
stoplight (3.0.1) stoplight (3.0.1)
redlock (~> 1.0) redlock (~> 1.0)
strong_migrations (0.8.0) strong_migrations (0.8.0)
@ -694,6 +741,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.2)
thor (1.2.2) thor (1.2.2)
tilt (2.2.0) tilt (2.2.0)
timeout (0.4.0) timeout (0.4.0)
@ -752,14 +800,15 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) 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.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.1) wisper (2.0.1)
xorcist (1.1.3) xorcist (1.1.3)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.8) zeitwerk (2.6.11)
PLATFORMS PLATFORMS
ruby ruby
@ -788,6 +837,7 @@ DEPENDENCIES
color_diff (~> 0.1) color_diff (~> 0.1)
concurrent-ruby concurrent-ruby
connection_pool connection_pool
database_cleaner-active_record
devise (~> 4.9) devise (~> 4.9)
devise-two-factor (~> 4.1) devise-two-factor (~> 4.1)
devise_pam_authenticatable2 (~> 9.2) devise_pam_authenticatable2 (~> 9.2)
@ -822,16 +872,18 @@ DEPENDENCIES
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.12) lograge (~> 0.12)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
mime-types (~> 3.4.1) mime-types (~> 3.5.0)
net-http (~> 0.3.2) net-http (~> 0.3.2)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
nsa!
oj (~> 3.14) oj (~> 3.14)
omniauth (~> 1.9) omniauth (~> 2.0)
omniauth-cas (~> 2.0) omniauth-cas!
omniauth-rails_csrf_protection (~> 0.1) omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 1.10) omniauth-saml (~> 2.0)
omniauth_openid_connect (~> 0.6.1) omniauth_openid_connect (~> 0.6.1)
ox (~> 2.14) ox (~> 2.14)
parslet parslet
@ -864,10 +916,12 @@ DEPENDENCIES
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
rubocop-rspec rubocop-rspec
ruby-prof
ruby-progressbar (~> 1.13) ruby-progressbar (~> 1.13)
rubyzip (~> 2.3) rubyzip (~> 2.3)
sanitize (~> 6.0) sanitize (~> 6.0)
scenic (~> 1.7) scenic (~> 1.7)
selenium-webdriver
sidekiq (~> 6.5) sidekiq (~> 6.5)
sidekiq-bulk (~> 0.2.0) sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 5.0) sidekiq-scheduler (~> 5.0)
@ -880,6 +934,7 @@ DEPENDENCIES
stackprof stackprof
stoplight (~> 3.0.1) stoplight (~> 3.0.1)
strong_migrations (~> 0.8) strong_migrations (~> 0.8)
test-prof
thor (~> 1.2) thor (~> 1.2)
tty-prompt (~> 0.23) tty-prompt (~> 0.23)
twitter-text (~> 3.1.0) twitter-text (~> 3.1.0)

View File

@ -11,7 +11,7 @@
[releases]: https://github.com/mastodon/mastodon/releases [releases]: https://github.com/mastodon/mastodon/releases
[crowdin]: https://crowdin.com/project/mastodon [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: 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 ### 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 ### 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. 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 Vagrant and Virtualbox
- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater` - 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) [![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 ## Contributing

View File

@ -33,7 +33,7 @@ class AccountsIndex < Chewy::Index
}, },
verbatim: { verbatim: {
tokenizer: 'whitespace', tokenizer: 'standard',
filter: %w(lowercase asciifolding cjk_width), filter: %w(lowercase asciifolding cjk_width),
}, },

View File

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

View File

@ -12,7 +12,7 @@ class AccountsController < ApplicationController
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } 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_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 def show
respond_to do |format| respond_to do |format|

View File

@ -40,7 +40,7 @@ module Admin
end end
# Allow transparently upgrading a domain block # 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 = existing_domain_block
@domain_block.assign_attributes(resource_params) @domain_block.assign_attributes(resource_params)
end end

View File

@ -65,7 +65,7 @@ module Admin
end end
def filtered_instances def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results InstanceFilter.new(limited_federation_mode? ? { allowed: true } : filter_params).results
end end
def filter_params def filter_params

View File

@ -8,7 +8,7 @@ class Api::BaseController < ApplicationController
include AccessTokenTrackingConcern include AccessTokenTrackingConcern
include ApiCachingConcern 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_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :require_not_suspended! before_action :require_not_suspended!
@ -150,7 +150,7 @@ class Api::BaseController < ApplicationController
end end
def disallow_unauthenticated_api_access? 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 end
private private

View File

@ -3,7 +3,7 @@
class Api::V1::Instances::ActivityController < Api::BaseController class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! 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 '' vary_by ''
@ -33,6 +33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end end
def require_enabled_api! 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
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::DomainBlocksController < Api::BaseController 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 :require_enabled_api!
before_action :set_domain_blocks before_action :set_domain_blocks

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController 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 skip_around_action :set_locale
before_action :set_extended_description 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 # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if whitelist_mode? super if limited_federation_mode?
end end
def show def show

View File

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

View File

@ -3,24 +3,24 @@
class Api::V1::Instances::PeersController < Api::BaseController class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! 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 skip_around_action :set_locale
vary_by '' vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if whitelist_mode? super if limited_federation_mode?
end end
def index def index
cache_even_if_authenticated! 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 end
private private
def require_enabled_api! 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
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController 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 before_action :set_privacy_policy

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::RulesController < Api::BaseController 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 skip_around_action :set_locale
before_action :set_rules 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 # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if whitelist_mode? super if limited_federation_mode?
end end
def index def index

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::Instances::TranslationLanguagesController < Api::BaseController 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 before_action :set_languages

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController 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 skip_around_action :set_locale
vary_by '' vary_by ''
# Override `current_user` to avoid reading session cookies unless in whitelist mode # Override `current_user` to avoid reading session cookies unless in whitelist mode
def current_user def current_user
super if whitelist_mode? super if limited_federation_mode?
end end
def show def show

View File

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

View File

@ -17,13 +17,16 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
if fav if fav
@status = fav.status @status = fav.status
count = [@status.favourites_count - 1, 0].max
UnfavouriteWorker.perform_async(current_account.id, @status.id) UnfavouriteWorker.perform_async(current_account.id, @status.id)
else else
@status = Status.find(params[:status_id]) @status = Status.find(params[:status_id])
count = @status.favourites_count
authorize @status, :show? authorize @status, :show?
end 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 rescue Mastodon::NotPermittedError
not_found not_found
end end

View File

@ -24,15 +24,18 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
if @status if @status
authorize @status, :unreblog? authorize @status, :unreblog?
@reblog = @status.reblog
count = [@reblog.reblogs_count - 1, 0].max
@status.discard @status.discard
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
@reblog = @status.reblog
else else
@reblog = Status.find(params[:status_id]) @reblog = Status.find(params[:status_id])
count = @reblog.reblogs_count
authorize @reblog, :show? authorize @reblog, :show?
end 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 rescue Mastodon::NotPermittedError
not_found not_found
end end

View File

@ -19,7 +19,7 @@ class ApplicationController < ActionController::Base
helper_method :use_seamless_external_login? helper_method :use_seamless_external_login?
helper_method :omniauth_only? helper_method :omniauth_only?
helper_method :sso_account_settings helper_method :sso_account_settings
helper_method :whitelist_mode? helper_method :limited_federation_mode?
helper_method :body_class_string helper_method :body_class_string
helper_method :skip_csrf_meta_tags? helper_method :skip_csrf_meta_tags?
@ -52,7 +52,7 @@ class ApplicationController < ActionController::Base
private private
def authorized_fetch_mode? def authorized_fetch_mode?
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
end end
def public_fetch_mode? def public_fetch_mode?

View File

@ -5,21 +5,13 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider) def self.provides_callback_for(provider)
define_method provider do define_method provider do
@provider = provider
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user) @user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
if @user.persisted? if @user.persisted?
LoginActivity.create( record_login_activity
user: @user,
success: true,
authentication_method: :omniauth,
provider: provider,
ip: request.remote_ip,
user_agent: request.user_agent
)
sign_in_and_redirect @user, event: :authentication 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_for_provider) if is_navigational_format?
set_flash_message(:notice, :success, kind: label) if is_navigational_format?
else else
session["devise.#{provider}_data"] = request.env['omniauth.auth'] session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url redirect_to new_user_registration_url
@ -38,4 +30,29 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
auth_setup_path(missing_email: '1') auth_setup_path(missing_email: '1')
end end
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 end

View File

@ -108,7 +108,7 @@ class Auth::SessionsController < Devise::SessionsController
end end
def home_paths(resource) 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) paths << short_account_path(username: resource.account) if single_user_mode? && resource.is_a?(User)

View File

@ -3,32 +3,19 @@
class AuthorizeInteractionsController < ApplicationController class AuthorizeInteractionsController < ApplicationController
include Authorization include Authorization
layout 'modal'
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_body_classes
before_action :set_resource before_action :set_resource
def show def show
if @resource.is_a?(Account) if @resource.is_a?(Account)
render :show redirect_to web_url("@#{@resource.pretty_acct}")
elsif @resource.is_a?(Status) elsif @resource.is_a?(Status)
redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}") redirect_to web_url("@#{@resource.account.pretty_acct}/#{@resource.id}")
else else
render :error not_found
end end
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 private
def set_resource def set_resource
@ -61,8 +48,4 @@ class AuthorizeInteractionsController < ApplicationController
def uri_param def uri_param
params[:uri] || params.fetch(:acct, '').delete_prefix('acct:') params[:uri] || params.fetch(:acct, '').delete_prefix('acct:')
end end
def set_body_classes
@body_classes = 'modal-layout'
end
end end

View File

@ -10,7 +10,7 @@ class BackupsController < ApplicationController
def download def download
case Paperclip::Attachment.default_options[:storage] case Paperclip::Attachment.default_options[:storage]
when :s3 when :s3, :azure
redirect_to @backup.dump.expiring_url(10), allow_other_host: true redirect_to @backup.dump.expiring_url(10), allow_other_host: true
when :fog when :fog
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present? if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?

View File

@ -4,7 +4,7 @@ module AccountOwnedConcern
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do 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 :set_account, if: :account_required?
before_action :check_account_approval, if: :account_required? before_action :check_account_approval, if: :account_required?
before_action :check_account_suspension, if: :account_required? before_action :check_account_suspension, if: :account_required?

View File

@ -8,6 +8,6 @@ module ApiCachingConcern
end end
def cache_even_if_authenticated! 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
end end

View File

@ -42,7 +42,7 @@ module CaptchaConcern
end end
def extend_csp_for_captcha! def extend_csp_for_captcha!
policy = request.content_security_policy policy = request.content_security_policy&.clone
return unless captcha_required? && policy.present? return unless captcha_required? && policy.present?
@ -54,6 +54,8 @@ module CaptchaConcern
policy.send(directive, *values) policy.send(directive, *values)
end end
request.content_security_policy = policy
end end
def render_captcha def render_captcha

View File

@ -11,7 +11,7 @@ module WebAppControllerConcern
end end
def skip_csrf_meta_tags? def skip_csrf_meta_tags?
current_user.nil? !(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
end end
def set_app_body_class def set_app_body_class

View File

@ -10,7 +10,7 @@ class FollowerAccountsController < ApplicationController
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
skip_around_action :set_locale, if: -> { request.format == :json } 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 def index
respond_to do |format| respond_to do |format|

View File

@ -10,7 +10,7 @@ class FollowingAccountsController < ApplicationController
before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
skip_around_action :set_locale, if: -> { request.format == :json } 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 def index
respond_to do |format| respond_to do |format|

View File

@ -9,6 +9,8 @@ class MailSubscriptionsController < ApplicationController
before_action :set_user before_action :set_user
before_action :set_type before_action :set_type
protect_from_forgery with: :null_session
def show; end def show; end
def create def create
@ -20,6 +22,7 @@ class MailSubscriptionsController < ApplicationController
def set_user def set_user
@user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe') @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe')
not_found unless @user
end end
def set_body_classes def set_body_classes
@ -35,7 +38,7 @@ class MailSubscriptionsController < ApplicationController
when 'follow', 'reblog', 'favourite', 'mention', 'follow_request' when 'follow', 'reblog', 'favourite', 'mention', 'follow_request'
"notification_emails.#{params[:type]}" "notification_emails.#{params[:type]}"
else else
raise ArgumentError not_found
end end
end end
end end

View File

@ -3,9 +3,9 @@
class MediaController < ApplicationController class MediaController < ApplicationController
include Authorization 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 :set_media_attachment
before_action :verify_permitted_status! before_action :verify_permitted_status!
before_action :check_playable, only: :player before_action :check_playable, only: :player

View File

@ -8,7 +8,7 @@ class MediaProxyController < ApplicationController
skip_before_action :require_functional! 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 ActiveRecord::RecordInvalid, with: :not_found
rescue_from Mastodon::UnexpectedResponseError, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found

View File

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

View File

@ -17,7 +17,7 @@ class StatusesController < ApplicationController
after_action :set_link_headers after_action :set_link_headers
skip_around_action :set_locale, if: -> { request.format == :json } 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| content_security_policy only: :embed do |policy|
policy.frame_ancestors(false) policy.frame_ancestors(false)

View File

@ -10,13 +10,13 @@ class TagsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } 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 :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_local
before_action :set_tag before_action :set_tag
before_action :set_statuses, if: -> { request.format == :rss } before_action :set_statuses, if: -> { request.format == :rss }
before_action :set_instance_presenter before_action :set_instance_presenter
skip_before_action :require_functional!, unless: :whitelist_mode? skip_before_action :require_functional!, unless: :limited_federation_mode?
def show def show
respond_to do |format| respond_to do |format|

View File

@ -19,6 +19,7 @@ module WellKnown
def set_account def set_account
username = username_from_resource username = username_from_resource
@account = begin @account = begin
if username == Rails.configuration.x.local_domain if username == Rails.configuration.x.local_domain
Account.representative Account.representative

View File

@ -235,6 +235,6 @@ module ApplicationHelper
private private
def storage_host_var 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
end end

View File

@ -21,7 +21,14 @@ module ContextHelper
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' }, discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' }, 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' }, suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
quoteUrl: { 'quoteUrl' => 'as:quoteUrl' }, quoteUrl: { 'quoteUrl' => 'as:quoteUrl' },
}.freeze }.freeze

View File

@ -10,14 +10,14 @@ module DomainControlHelper
uri_or_domain uri_or_domain
end end
if whitelist_mode? if limited_federation_mode?
!DomainAllow.allowed?(domain) !DomainAllow.allowed?(domain)
else else
DomainBlock.blocked?(domain) DomainBlock.blocked?(domain)
end end
end end
def whitelist_mode? def limited_federation_mode?
Rails.configuration.x.whitelist_mode Rails.configuration.x.limited_federation_mode
end end
end end

View File

@ -204,7 +204,17 @@ module LanguagesHelper
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
}.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 # 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 # names, but for some translations, we need the names of the
@ -217,9 +227,6 @@ module LanguagesHelper
'pt-BR': 'Português (Brasil)', 'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)', 'pt-PT': 'Português (Portugal)',
'sr-Latn': 'Srpski (latinica)', 'sr-Latn': 'Srpski (latinica)',
'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)',
}.freeze }.freeze
def native_locale_name(locale) def native_locale_name(locale)

View File

@ -65,33 +65,6 @@ module StatusesHelper
embedded_view? ? '_blank' : nil embedded_view? ? '_blank' : nil
end 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) def fa_visibility_icon(status)
case status.visibility case status.visibility
when 'public' when 'public'

View File

@ -85,7 +85,7 @@ export function importFetchedStatuses(statuses) {
} }
if (status.poll && status.poll.id) { 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) { export function importFetchedPoll(poll) {
return dispatch => { return (dispatch, getState) => {
dispatch(importPolls([normalizePoll(poll)])); dispatch(importPolls([normalizePoll(poll, getState().getIn(['polls', poll.id]))]));
}; };
} }

View File

@ -78,6 +78,10 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.hidden = normalOldStatus.get('hidden'); normalStatus.hidden = normalOldStatus.get('hidden');
normalStatus.quote = normalOldStatus.get('quote'); normalStatus.quote = normalOldStatus.get('quote');
normalStatus.quote_hidden = normalOldStatus.get('quote_hidden'); normalStatus.quote_hidden = normalOldStatus.get('quote_hidden');
if (normalOldStatus.get('translation')) {
normalStatus.translation = normalOldStatus.get('translation');
}
} else { } else {
// If the status has a CW but no contents, treat the CW as if it were the // 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. // 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; return normalStatus;
} }
@ -119,15 +135,23 @@ export function normalizeStatusTranslation(translation, status) {
return normalTranslation; return normalTranslation;
} }
export function normalizePoll(poll) { export function normalizePoll(poll, normalOldPoll) {
const normalPoll = { ...poll }; const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(poll.emojis); const emojiMap = makeEmojiMap(poll.emojis);
normalPoll.options = poll.options.map((option, index) => ({ normalPoll.options = poll.options.map((option, index) => {
...option, const normalOption = {
voted: poll.own_votes && poll.own_votes.includes(index), ...option,
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap), 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; return normalPoll;
} }

View File

@ -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 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 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 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 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 expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);

View File

@ -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 }) => (
<div className='account-role'>
{icon}
{label}
{domain && <span className='account-role__domain'>{domain}</span>}
</div>
);
Badge.propTypes = {
icon: PropTypes.node,
label: PropTypes.node,
domain: PropTypes.node,
};
Badge.defaultProps = {
icon: <PersonIcon />,
};
export const GroupBadge = () => (
<Badge icon={<GroupsIcon />} label={<FormattedMessage id='account.badges.group' defaultMessage='Group' />} />
);
export const AutomatedBadge = () => (
<Badge icon={<SmartToyIcon />} label={<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />} />
);

View File

@ -297,7 +297,7 @@ export default class Dropdown extends PureComponent {
onKeyPress: this.handleKeyPress, onKeyPress: this.handleKeyPress,
}) : ( }) : (
<IconButton <IconButton
icon={icon} icon={!open ? icon : 'close'}
title={title} title={title}
active={open} active={open}
disabled={disabled} disabled={disabled}

View File

@ -114,7 +114,7 @@ export default class IntersectionObserverArticle extends Component {
aria-setsize={listLength} aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }} style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id} data-id={id}
tabIndex={0} tabIndex={-1}
> >
{children && cloneElement(children, { hidden: true })} {children && cloneElement(children, { hidden: true })}
</article> </article>
@ -122,7 +122,7 @@ export default class IntersectionObserverArticle extends Component {
} }
return ( return (
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={0}> <article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={-1}>
{children && cloneElement(children, { hidden: false })} {children && cloneElement(children, { hidden: false })}
</article> </article>
); );

View File

@ -12,7 +12,7 @@ import { debounce } from 'lodash';
import { Blurhash } from 'mastodon/components/blurhash'; 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'; import { IconButton } from './icon_button';
@ -209,7 +209,6 @@ class MediaGallery extends PureComponent {
static propTypes = { static propTypes = {
sensitive: PropTypes.bool, sensitive: PropTypes.bool,
standalone: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
lang: PropTypes.string, lang: PropTypes.string,
size: PropTypes.object, size: PropTypes.object,
@ -224,11 +223,6 @@ class MediaGallery extends PureComponent {
quote: PropTypes.bool, quote: PropTypes.bool,
}; };
static defaultProps = {
standalone: false,
quote: false,
};
state = { state = {
visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), visible: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
width: this.props.defaultWidth, width: this.props.defaultWidth,
@ -297,7 +291,7 @@ class MediaGallery extends PureComponent {
} }
render () { 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 { visible } = this.state;
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
@ -305,10 +299,10 @@ class MediaGallery extends PureComponent {
const style = {}; const style = {};
if (this.isFullSizeEligible() && (standalone || !cropImages)) { if (this.isFullSizeEligible()) {
style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`; style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
} else { } else {
style.aspectRatio = '16 / 9'; style.aspectRatio = '3 / 2';
} }
if (quote && style.height) { if (quote && style.height) {
@ -322,7 +316,7 @@ class MediaGallery extends PureComponent {
const size = media.take(4).size; const size = media.take(4).size;
const uncached = media.every(attachment => attachment.get('type') === 'unknown'); const uncached = media.every(attachment => attachment.get('type') === 'unknown');
if (standalone && this.isFullSizeEligible()) { if (this.isFullSizeEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />; children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);

View File

@ -12,6 +12,7 @@ class PictureInPicturePlaceholder extends PureComponent {
static propTypes = { static propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
aspectRatio: PropTypes.string,
}; };
handleClick = () => { handleClick = () => {
@ -20,8 +21,10 @@ class PictureInPicturePlaceholder extends PureComponent {
}; };
render () { render () {
const { aspectRatio } = this.props;
return ( return (
<div className='picture-in-picture-placeholder' role='button' tabIndex={0} onClick={this.handleClick}> <div className='picture-in-picture-placeholder' style={{ aspectRatio }} role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id='window-restore' /> <Icon id='window-restore' />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' /> <FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div> </div>

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { HotKeys } from 'react-hotkeys'; 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 { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { displayMedia } from '../initial_state'; import { displayMedia } from '../initial_state';
import AttachmentList from './attachment_list';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
import { AvatarOverlay } from './avatar_overlay'; import { AvatarOverlay } from './avatar_overlay';
import { DisplayName } from './display_name'; import { DisplayName } from './display_name';
@ -292,17 +290,35 @@ class Status extends ImmutablePureComponent {
this.props.onQuoteToggleHidden(this._properStatus()); this.props.onQuoteToggleHidden(this._properStatus());
} }
renderLoadingMediaGallery () { getAttachmentAspectRatio () {
return <div className='media-gallery' style={{ height: '110px' }} />; 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 () { renderLoadingMediaGallery = () => {
return <div className='video-player' style={{ height: '110px' }} />; return (
} <div className='media-gallery' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
renderLoadingAudioPlayer () { renderLoadingVideoPlayer = () => {
return <div className='audio-player' style={{ height: '110px' }} />; return (
} <div className='video-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
renderLoadingAudioPlayer = () => {
return (
<div className='audio-player' style={{ aspectRatio: this.getAttachmentAspectRatio() }} />
);
};
handleOpenVideo = (options) => { handleOpenVideo = (options) => {
const status = this._properStatus(); const status = this._properStatus();
@ -428,7 +444,7 @@ class Status extends ImmutablePureComponent {
}; };
render () { 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; let { status, account, ...other } = this.props;
@ -525,69 +541,61 @@ class Status extends ImmutablePureComponent {
</div> </div>
); );
} }
const media = (status, quote = false) => { const media = (status, quote = false) => {
if (pictureInPicture.get('inUse')) { if (pictureInPicture.get('inUse')) {
return <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; return <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
if (this.props.muted) { if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
return (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
return ( return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} > <Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => ( {Component => (
<Component <Component
src={attachment.get('url')} src={attachment.get('url')}
alt={description} alt={description}
lang={language} lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])} poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])} backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])} foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
accentColor={attachment.getIn(['meta', 'colors', 'accent'])} accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)} duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
width={this.props.cachedMediaWidth} width={this.props.cachedMediaWidth}
height={110} height={110}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
visible={this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote} quote={quote}
/> />
)} )}
</Bundle> </Bundle>
); );
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]); const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description'); const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
return ( return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}> <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => ( {Component => (
<Component <Component
preview={attachment.get('preview_url')} preview={attachment.get('preview_url')}
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])} frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
blurhash={attachment.get('blurhash')} blurhash={attachment.get('blurhash')}
src={attachment.get('url')} src={attachment.get('url')}
alt={description} alt={description}
lang={language} lang={language}
inline
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
visible={quote ? this.state.showQuoteMedia : this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote} quote={quote}
/> />
)} )}
@ -605,15 +613,15 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
visible={quote ? this.state.showQuoteMedia : this.state.showMedia} visible={this.state.showMedia}
onToggleVisibility={quote ? this.handleToggleQuoteMediaVisibility : this.handleToggleMediaVisibility} onToggleVisibility={this.handleToggleMediaVisibility}
quote={quote} quote={quote}
/> />
)} )}
</Bundle> </Bundle>
); );
} }
} 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 (
<Card <Card
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
@ -624,8 +632,7 @@ class Status extends ImmutablePureComponent {
/> />
); );
} }
return null;
return null;
}; };
const statusAvatar = (status, account) => { const statusAvatar = (status, account) => {
@ -635,7 +642,7 @@ class Status extends ImmutablePureComponent {
return <AvatarOverlay account={status.get('account')} friend={account} />; return <AvatarOverlay account={status.get('account')} friend={account} />;
} }
}; };
const identity = (status, account) => ( const identity = (status, account) => (
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'> <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'> <div className='status__avatar'>
@ -670,6 +677,14 @@ class Status extends ImmutablePureComponent {
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>} <RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
</a> </a>
{/* <a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
</a> */}
{identity(status, account, false)} {identity(status, account, false)}
</div> </div>
@ -683,7 +698,7 @@ class Status extends ImmutablePureComponent {
onCollapsedToggle={this.handleCollapsedToggle} onCollapsedToggle={this.handleCollapsedToggle}
/> />
{media(status)} {media}
{quote(status, this.props.muted, quoteMuted, this.handleQuoteClick, this.handleExpandedQuoteToggle, identity, media, this.context.router, contextType)} {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);

View File

@ -33,7 +33,7 @@ const messages = defineMessages({
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' }, quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, 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' }, bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' }, removeBookmark: { id: 'status.remove_bookmark', defaultMessage: 'Remove bookmark' },
open: { id: 'status.open', defaultMessage: 'Expand this status' }, open: { id: 'status.open', defaultMessage: 'Expand this status' },

View File

@ -57,7 +57,7 @@ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? 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' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
@ -307,7 +307,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
modalProps: { modalProps: {
type, type,
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('url'), url: status.get('uri'),
}, },
})); }));
}, },

View File

@ -10,6 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import Button from 'mastodon/components/button'; import Button from 'mastodon/components/button';
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters'; import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
@ -48,7 +49,7 @@ const messages = defineMessages({
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, 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' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, 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 acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
const isIndexable = !account.get('noindex'); const isIndexable = !account.get('noindex');
let badge; const badges = [];
if (account.get('bot')) { if (account.get('bot')) {
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Automated' /></div>); badges.push(<AutomatedBadge key='bot-badge' />);
} else if (account.get('group')) { } else if (account.get('group')) {
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>); badges.push(<GroupBadge key='group-badge' />);
} else {
badge = null;
} }
account.get('roles', []).forEach((role) => {
badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} />);
});
return ( return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />} {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />}
@ -414,13 +417,19 @@ class Header extends ImmutablePureComponent {
<div className='account__header__tabs__name'> <div className='account__header__tabs__name'>
<h1> <h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge} <span dangerouslySetInnerHTML={displayNameHtml} />
<small> <small>
<span>@{acct}</span> {lockedIcon} <span>@{acct}</span> {lockedIcon}
</small> </small>
</h1> </h1>
</div> </div>
{badges.length > 0 && (
<div className='account__header__badges'>
{badges}
</div>
)}
{!(suspended || hidden) && ( {!(suspended || hidden) && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div className='account__header__bio' ref={this.setRef}> <div className='account__header__bio' ref={this.setRef}>

View File

@ -128,7 +128,11 @@ export default class MediaItem extends ImmutablePureComponent {
<div className='media-gallery__gifv'> <div className='media-gallery__gifv'>
{content} {content}
{label && <span className='media-gallery__gifv__label'>{label}</span>} {label && (
<div className='media-gallery__item__badges'>
<span className='media-gallery__gifv__label'>{label}</span>
</div>
)}
</div> </div>
); );
} }

View File

@ -83,7 +83,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
modalProps: { modalProps: {
type: 'follow', type: 'follow',
accountId: account.get('id'), accountId: account.get('id'),
url: account.get('url'), url: account.get('uri'),
}, },
})); }));
}, },

View File

@ -475,6 +475,7 @@ class Audio extends PureComponent {
const progress = Math.min((currentTime / duration) * 100, 100); const progress = Math.min((currentTime / duration) * 100, 100);
let warning; let warning;
if (sensitive) { if (sensitive) {
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
} else { } else {
@ -520,7 +521,10 @@ class Audio extends PureComponent {
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> <div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> <button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
<span className='spoiler-button__overlay__label'>{warning}</span> <span className='spoiler-button__overlay__label'>
{warning}
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
</span>
</button> </button>
</div> </div>

View File

@ -86,7 +86,6 @@ class Bookmarks extends ImmutablePureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
showBackButton
/> />
<StatusList <StatusList

View File

@ -12,7 +12,7 @@ const messages = defineMessages({
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, 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' }, lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },

View File

@ -291,13 +291,12 @@ class ComposeForm extends ImmutablePureComponent {
autoFocus={autoFocus} autoFocus={autoFocus}
lang={this.props.lang} lang={this.props.lang}
> >
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__modifiers'> <div className='compose-form__modifiers'>
<UploadFormContainer /> <UploadFormContainer />
<PollFormContainer /> <PollFormContainer />
</div> </div>
</AutosuggestTextarea> </AutosuggestTextarea>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<div className='compose-form__buttons-wrapper'> <div className='compose-form__buttons-wrapper'>
<div className='compose-form__buttons'> <div className='compose-form__buttons'>

View File

@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { Avatar } from '../../../components/avatar'; import { Avatar } from '../../../components/avatar';
import { IconButton } from '../../../components/icon_button';
import ActionBar from './action_bar'; import ActionBar from './action_bar';
@ -21,23 +20,27 @@ export default class NavigationBar extends ImmutablePureComponent {
}; };
render () { render () {
const username = this.props.account.get('acct')
return ( return (
<div className='navigation-bar'> <div className='navigation-bar'>
<Link to={`/@${this.props.account.get('acct')}`}> <Link to={`/@${username}`}>
<span style={{ display: 'none' }}>{this.props.account.get('acct')}</span> <span style={{ display: 'none' }}>{username}</span>
<Avatar account={this.props.account} size={46} /> <Avatar account={this.props.account} size={46} />
</Link> </Link>
<div className='navigation-bar__profile'> <div className='navigation-bar__profile'>
<Link to={`/@${this.props.account.get('acct')}`}> <span>
<strong className='navigation-bar__profile-account'>@{this.props.account.get('acct')}</strong> <Link to={`/@${username}`}>
</Link> <strong className='navigation-bar__profile-account'>@{username}</strong>
</Link>
</span>
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> <span>
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
</span>
</div> </div>
<div className='navigation-bar__actions'> <div className='navigation-bar__actions'>
<IconButton className='close' title='' icon='close' onClick={this.props.onClose} />
<ActionBar account={this.props.account} onLogout={this.props.onLogout} /> <ActionBar account={this.props.account} onLogout={this.props.onLogout} />
</div> </div>
</div> </div>

View File

@ -139,10 +139,6 @@ class Search extends PureComponent {
this.setState({ expanded: false, selectedOption: -1 }); this.setState({ expanded: false, selectedOption: -1 });
}; };
findTarget = () => {
return this.searchForm;
};
handleHashtagClick = () => { handleHashtagClick = () => {
const { router } = this.context; const { router } = this.context;
const { value, onClickSearchResult } = this.props; const { value, onClickSearchResult } = this.props;

View File

@ -1,10 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { Blurhash } from 'mastodon/components/blurhash'; import { Blurhash } from 'mastodon/components/blurhash';
import { accountsCountRenderer } from 'mastodon/components/hashtag'; import { accountsCountRenderer } from 'mastodon/components/hashtag';
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton'; import { Skeleton } from 'mastodon/components/skeleton';
@ -13,10 +16,15 @@ export default class Story extends PureComponent {
static propTypes = { static propTypes = {
url: PropTypes.string, url: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
lang: PropTypes.string,
publisher: PropTypes.string, publisher: PropTypes.string,
publishedAt: PropTypes.string,
author: PropTypes.string,
sharedTimes: PropTypes.number, sharedTimes: PropTypes.number,
thumbnail: PropTypes.string, thumbnail: PropTypes.string,
thumbnailDescription: PropTypes.string,
blurhash: PropTypes.string, blurhash: PropTypes.string,
expanded: PropTypes.bool,
}; };
state = { state = {
@ -26,23 +34,23 @@ export default class Story extends PureComponent {
handleImageLoad = () => this.setState({ thumbnailLoaded: true }); handleImageLoad = () => this.setState({ thumbnailLoaded: true });
render () { 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; const { thumbnailLoaded } = this.state;
return ( return (
<a className='story' href={url} target='blank' rel='noopener'> <a className={classNames('story', { expanded })} href={url} target='blank' rel='noopener'>
<div className='story__details'> <div className='story__details'>
<div className='story__details__publisher'>{publisher ? publisher : <Skeleton width={50} />}</div> <div className='story__details__publisher'>{publisher ? <span lang={lang}>{publisher}</span> : <Skeleton width={50} />}{publishedAt && <> · <RelativeTimestamp timestamp={publishedAt} /></>}</div>
<div className='story__details__title'>{title ? title : <Skeleton />}</div> <div className='story__details__title' lang={lang}>{title ? title : <Skeleton />}</div>
<div className='story__details__shared'>{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div> <div className='story__details__shared'>{author && <><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{author}</strong> }} /> · </>}{typeof sharedTimes === 'number' ? <ShortNumber value={sharedTimes} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}</div>
</div> </div>
<div className='story__thumbnail'> <div className='story__thumbnail'>
{thumbnail ? ( {thumbnail ? (
<> <>
<div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div> <div className={classNames('story__thumbnail__preview', { 'story__thumbnail__preview--hidden': thumbnailLoaded })}><Blurhash hash={blurhash} /></div>
<img src={thumbnail} onLoad={this.handleImageLoad} alt='' role='presentation' /> <img src={thumbnail} onLoad={this.handleImageLoad} alt={thumbnailDescription} title={thumbnailDescription} lang={lang} />
</> </>
) : <Skeleton />} ) : <Skeleton />}
</div> </div>

View File

@ -55,14 +55,19 @@ class Links extends PureComponent {
<div className='explore__links'> <div className='explore__links'>
{banner} {banner}
{isLoading ? (<LoadingIndicator />) : links.map(link => ( {isLoading ? (<LoadingIndicator />) : links.map((link, i) => (
<Story <Story
key={link.get('id')} key={link.get('id')}
expanded={i === 0}
lang={link.get('language')}
url={link.get('url')} url={link.get('url')}
title={link.get('title')} title={link.get('title')}
publisher={link.get('provider_name')} publisher={link.get('provider_name')}
publishedAt={link.get('published_at')}
author={link.get('author_name')}
sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1} sharedTimes={link.getIn(['history', 0, 'accounts']) * 1 + link.getIn(['history', 1, 'accounts']) * 1}
thumbnail={link.get('image')} thumbnail={link.get('image')}
thumbnailDescription={link.get('image_description')}
blurhash={link.get('blurhash')} blurhash={link.get('blurhash')}
/> />
))} ))}

View File

@ -47,7 +47,7 @@ class Statuses extends PureComponent {
return ( return (
<> <>
<DismissableBanner id='explore/statuses'> <DismissableBanner id='explore/statuses'>
<FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favourites are ranked higher.' /> <FormattedMessage id='dismissable_banner.explore_statuses' defaultMessage='These are posts from across the social web that are gaining traction today. Newer posts with more boosts and favorites are ranked higher.' />
</DismissableBanner> </DismissableBanner>
<StatusList <StatusList

View File

@ -18,7 +18,7 @@ import Column from 'mastodon/features/ui/components/column';
import { getStatusList } from 'mastodon/selectors'; import { getStatusList } from 'mastodon/selectors';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'column.favourites', defaultMessage: 'Favourites' }, heading: { id: 'column.favourites', defaultMessage: 'Favorites' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@ -74,7 +74,7 @@ class Favourites extends ImmutablePureComponent {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite posts yet. When you favourite one, it will show up here." />; const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favorite posts yet. When you favorite one, it will show up here." />;
return ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
@ -86,7 +86,6 @@ class Favourites extends ImmutablePureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
showBackButton
/> />
<StatusList <StatusList

View File

@ -61,7 +61,7 @@ class Favourites extends ImmutablePureComponent {
); );
} }
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favourited this post yet. When someone does, they will show up here.' />; const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favorited this post yet. When someone does, they will show up here.' />;
return ( return (
<Column bindToDocument={!multiColumn}> <Column bindToDocument={!multiColumn}>

View File

@ -32,7 +32,7 @@ const messages = defineMessages({
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, 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' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' }, domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },

View File

@ -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) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
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 = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
});
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};

View File

@ -1,9 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react'; 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 { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes'; 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 { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon';
import StatusListContainer from '../ui/containers/status_list_container'; import StatusListContainer from '../ui/containers/status_list_container';
import { HashtagHeader } from './components/hashtag_header';
import ColumnSettingsContainer from './containers/column_settings_container'; 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) => ({ const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0, hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]), tag: state.getIn(['tags', props.params.id]),
@ -48,7 +42,6 @@ class HashtagTimeline extends PureComponent {
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map, tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
intl: PropTypes.object,
}; };
handlePin = () => { handlePin = () => {
@ -188,27 +181,11 @@ class HashtagTimeline extends PureComponent {
}; };
render () { render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props; const { hasUnread, columnId, multiColumn, tag } = this.props;
const { id, local } = this.props.params; const { id, local } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const { signedIn } = this.context.identity; const { signedIn } = this.context.identity;
let followButton;
if (tag) {
const following = tag.get('following');
const classes = classNames('column-header__button', {
active: following,
});
followButton = (
<button className={classes} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}>
<Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' />
</button>
);
}
return ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
<ColumnHeader <ColumnHeader
@ -220,13 +197,14 @@ class HashtagTimeline extends PureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={followButton}
showBackButton showBackButton
> >
{columnId && <ColumnSettingsContainer columnId={columnId} />} {columnId && <ColumnSettingsContainer columnId={columnId} />}
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
alwaysPrepend
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`} scrollKey={`hashtag_timeline-${columnId}`}
timelineId={`hashtag:${id}${local ? ':local' : ''}`} timelineId={`hashtag:${id}${local ? ':local' : ''}`}
@ -245,4 +223,4 @@ class HashtagTimeline extends PureComponent {
} }
export default connect(mapStateToProps)(injectIntl(HashtagTimeline)); export default connect(mapStateToProps)(HashtagTimeline);

View File

@ -22,7 +22,7 @@ export const ExplorePrompt = () => (
<p> <p>
<FormattedMessage <FormattedMessage
id='home.explore_prompt.body' id='home.explore_prompt.body'
defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. It's looking pretty quiet right now, so how about:" defaultMessage="Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:"
/> />
</p> </p>

View File

@ -1,15 +1,23 @@
import PropTypes from 'prop-types'; 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 classNames from 'classnames';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { throttle, escapeRegExp } from 'lodash';
import { openModal, closeModal } from 'mastodon/actions/modal'; 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 { 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 }) => ({ const mapStateToProps = (state, { accountId }) => ({
displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']), displayNameHtml: state.getIn(['accounts', accountId, 'display_name_html']),
@ -19,70 +27,271 @@ const mapStateToProps = (state, { accountId }) => ({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onSignupClick() { onSignupClick() {
dispatch(closeModal({ dispatch(closeModal({
modalType: undefined, modalType: undefined,
ignoreFocus: false, ignoreFocus: false,
})); }));
dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })); 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 = { static propTypes = {
value: PropTypes.string, resourceUrl: PropTypes.string,
intl: PropTypes.object.isRequired,
}; };
state = { state = {
copied: false, value: localStorage ? (localStorage.getItem(PERSISTENCE_KEY) || '') : '',
expanded: false,
selectedOption: -1,
isLoading: false,
isSubmitting: false,
error: false,
options: [],
networkOptions: [],
}; };
setRef = c => { setRef = c => {
this.input = c; this.input = c;
}; };
handleInputClick = () => { handleChange = ({ target }) => {
this.setState({ copied: false }); this.setState(state => ({ value: target.value, isLoading: true, error: false, options: addInputToOptions(target.value, state.networkOptions) }), () => this._loadOptions());
this.input.focus();
this.input.select();
this.input.setSelectionRange(0, this.input.value.length);
}; };
handleButtonClick = () => { handleMessage = (event) => {
const { value } = this.props; const { resourceUrl } = this.props;
navigator.clipboard.writeText(value);
this.input.blur(); if (event.origin !== window.origin || event.source !== this.iframeRef.contentWindow) {
this.setState({ copied: true }); return;
this.timeout = setTimeout(() => this.setState({ copied: false }), 700); }
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 () { componentDidMount () {
if (this.timeout) clearTimeout(this.timeout); 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 () { render () {
const { value } = this.props; const { intl } = this.props;
const { copied } = this.state; 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 ( return (
<div className={classNames('copypaste', { copied })}> <div className={classNames('interaction-modal__login', { focused: expanded, expanded: hasPopOut, invalid: error })}>
<input
type='text' <iframe
ref={this.setRef} ref={this.setIFrameRef}
value={value} style={{display: 'none'}}
readOnly src='/remote_interaction_helper'
onClick={this.handleInputClick} sandbox='allow-scripts allow-same-origin'
title='remote interaction helper'
/> />
<button className='button' onClick={this.handleButtonClick}> <div className='interaction-modal__login__input'>
{copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy' defaultMessage='Copy' />} <input
</button> ref={this.setRef}
type='text'
value={value}
placeholder={intl.formatMessage(messages.loginPrompt)}
aria-label={intl.formatMessage(messages.loginPrompt)}
autoFocus
onChange={this.handleChange}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
autocomplete='off'
autocapitalize='off'
spellcheck='false'
/>
<Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>
</div>
{hasPopOut && (
<div className='search__popout'>
<div className='search__popout__menu'>
{options.map((option, i) => (
<button key={option} onMouseDown={this.handleOptionClick} data-index={i} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{option.split(domainRegExp).map((part, i) => (
part.toLowerCase() === domain.toLowerCase() ? (
<mark key={i}>
{part}
</mark>
) : (
<span key={i}>
{part}
</span>
)
))}
</button>
))}
</div>
</div>
)}
</div> </div>
); );
} }
} }
class InteractionModal extends PureComponent { const IntlLoginForm = injectIntl(LoginForm);
class InteractionModal extends React.PureComponent {
static propTypes = { static propTypes = {
displayNameHtml: PropTypes.string, displayNameHtml: PropTypes.string,
@ -116,8 +325,8 @@ class InteractionModal extends PureComponent {
break; break;
case 'favourite': case 'favourite':
icon = <Icon id='star' />; icon = <Icon id='star' />;
title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favourite {name}'s post" values={{ name }} />; title = <FormattedMessage id='interaction_modal.title.favourite' defaultMessage="Favorite {name}'s post" values={{ name }} />;
actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favourite this post to let the author know you appreciate it and save it for later.' />; actionDescription = <FormattedMessage id='interaction_modal.description.favourite' defaultMessage='With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.' />;
break; break;
case 'follow': case 'follow':
icon = <Icon id='user-plus' />; icon = <Icon id='user-plus' />;
@ -128,15 +337,21 @@ class InteractionModal extends PureComponent {
let signupButton; let signupButton;
if (registrationsOpen) { if (sso_redirect) {
signupButton = ( signupButton = (
<a href={signupUrl} className='button button--block button-tertiary'> <a href={sso_redirect} data-method='post' className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else if (registrationsOpen) {
signupButton = (
<a href={signupUrl} className='link-button'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a> </a>
); );
} else { } else {
signupButton = ( signupButton = (
<button className='button button--block button-tertiary' onClick={this.handleSignupClick}> <button className='link-button' onClick={this.handleSignupClick}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /> <FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button> </button>
); );
@ -146,22 +361,13 @@ class InteractionModal extends PureComponent {
<div className='modal-root__modal interaction-modal'> <div className='modal-root__modal interaction-modal'>
<div className='interaction-modal__lead'> <div className='interaction-modal__lead'>
<h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3> <h3><span className='interaction-modal__icon'>{icon}</span> {title}</h3>
<p>{actionDescription} <FormattedMessage id='interaction_modal.preamble' defaultMessage="Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one." /></p> <p>{actionDescription} <strong><FormattedMessage id='interaction_modal.sign_in' defaultMessage='You are not logged in to this server. Where is your account hosted?' /></strong></p>
</div> </div>
<div className='interaction-modal__choices'> <IntlLoginForm resourceUrl={url} />
<div className='interaction-modal__choices__choice'>
<h3><FormattedMessage id='interaction_modal.on_this_server' defaultMessage='On this server' /></h3>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
{signupButton}
</div>
<div className='interaction-modal__choices__choice'> <p className='hint'><FormattedMessage id='interaction_modal.sign_in_hint' defaultMessage="Tip: That's the website where you signed up. If you don't remember, look for the welcome e-mail in your inbox. You can also enter your full username! (e.g. @Mastodon@mastodon.social)" /></p>
<h3><FormattedMessage id='interaction_modal.on_another_server' defaultMessage='On a different server' /></h3> <p><FormattedMessage id='interaction_modal.no_account_yet' defaultMessage='Not on Mastodon?' /> {signupButton}</p>
<p><FormattedMessage id='interaction_modal.other_server_instructions' defaultMessage='Copy and paste this URL into the search field of your favourite Mastodon app or the web interface of your Mastodon server.' /></p>
<Copypaste value={url} />
</div>
</div>
</div> </div>
); );
} }

View File

@ -54,7 +54,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
</tr> </tr>
<tr> <tr>
<td><kbd>f</kbd></td> <td><kbd>f</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favourite' /></td> <td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to favorite' /></td>
</tr> </tr>
<tr> <tr>
<td><kbd>b</kbd></td> <td><kbd>b</kbd></td>
@ -138,7 +138,7 @@ class KeyboardShortcuts extends ImmutablePureComponent {
</tr> </tr>
<tr> <tr>
<td><kbd>g</kbd>+<kbd>f</kbd></td> <td><kbd>g</kbd>+<kbd>f</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.favourites' defaultMessage='to open favourites list' /></td> <td><FormattedMessage id='keyboard_shortcuts.favourites' defaultMessage='to open favorites list' /></td>
</tr> </tr>
<tr> <tr>
<td><kbd>g</kbd>+<kbd>p</kbd></td> <td><kbd>g</kbd>+<kbd>p</kbd></td>

View File

@ -65,7 +65,7 @@ class Lists extends ImmutablePureComponent {
return ( return (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}> <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='list-ul' multiColumn={multiColumn} showBackButton /> <ColumnHeader title={intl.formatMessage(messages.heading)} icon='list-ul' multiColumn={multiColumn} />
<NewListForm /> <NewListForm />

View File

@ -109,7 +109,7 @@ export default class ColumnSettings extends PureComponent {
</div> </div>
<div role='group' aria-labelledby='notifications-favourite'> <div role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favourites:' /></span> <span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> <SettingToggle disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />

View File

@ -7,7 +7,7 @@ import { Icon } from 'mastodon/components/icon';
const tooltips = defineMessages({ const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },

View File

@ -20,7 +20,7 @@ import FollowRequestContainer from '../containers/follow_request_container';
import Report from './report'; import Report from './report';
const messages = defineMessages({ const messages = defineMessages({
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' }, favourite: { id: 'notification.favourite', defaultMessage: '{name} favorited your status' },
follow: { id: 'notification.follow', defaultMessage: '{name} followed you' }, follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' }, ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' }, poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
@ -198,7 +198,7 @@ class Notification extends ImmutablePureComponent {
</div> </div>
<span title={notification.get('created_at')}> <span title={notification.get('created_at')}>
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} /> <FormattedMessage id='notification.favourite' defaultMessage='{name} favorited your status' values={{ name: link }} />
</span> </span>
</div> </div>

View File

@ -25,7 +25,7 @@ const messages = defineMessages({
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
quote: { id: 'status.quote', defaultMessage: 'Quote' }, quote: { id: 'status.quote', defaultMessage: 'Quote' },
cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' }, cannot_quote: { id: 'status.cannot_quote', defaultMessage: 'This post cannot be quoted' },
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' }, favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' }, quoteConfirm: { id: 'confirmations.quote.confirm', defaultMessage: 'Quote' },
@ -95,7 +95,7 @@ class Footer extends ImmutablePureComponent {
modalProps: { modalProps: {
type: 'reply', type: 'reply',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('url'), url: status.get('uri'),
}, },
})); }));
} }
@ -117,7 +117,7 @@ class Footer extends ImmutablePureComponent {
modalProps: { modalProps: {
type: 'favourite', type: 'favourite',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('url'), url: status.get('uri'),
}, },
})); }));
} }
@ -146,7 +146,7 @@ class Footer extends ImmutablePureComponent {
modalProps: { modalProps: {
type: 'reblog', type: 'reblog',
accountId: status.getIn(['account', 'id']), accountId: status.getIn(['account', 'id']),
url: status.get('url'), url: status.get('uri'),
}, },
})); }));
} }

View File

@ -29,7 +29,7 @@ const mapStateToProps = (state, { columnId }) => {
const index = columns.findIndex(c => c.get('uuid') === uuid); const index = columns.findIndex(c => c.get('uuid') === uuid);
const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']); const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); const timelineState = state.getIn(['timelines', `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: !!timelineState && timelineState.get('unread') > 0, hasUnread: !!timelineState && timelineState.get('unread') > 0,

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