Merge branch 'main' of https://github.com/mastodon/mastodon into features/main
This commit is contained in:
commit
c0cf1ebdd3
@ -1,3 +0,0 @@
|
|||||||
---
|
|
||||||
ignore:
|
|
||||||
- CVE-2015-9284 # Mitigation following https://github.com/omniauth/omniauth/wiki/Resolving-CVE-2015-9284#mitigating-in-rails-applications
|
|
56
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
56
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
@ -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
|
|
76
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
Normal 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
|
65
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
Normal file
65
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
Normal 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
|
29
.github/renovate.json5
vendored
29
.github/renovate.json5
vendored
@ -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
|
||||||
|
94
.github/workflows/build-container-image.yml
vendored
Normal file
94
.github/workflows/build-container-image.yml
vendored
Normal 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
|
79
.github/workflows/build-image.yml
vendored
79
.github/workflows/build-image.yml
vendored
@ -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
|
|
74
.github/workflows/build-nightly.yml
vendored
74
.github/workflows/build-nightly.yml
vendored
@ -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
41
.github/workflows/build-push-pr.yml
vendored
Normal 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
25
.github/workflows/build-releases.yml
vendored
Normal 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
76
.github/workflows/crowdin-download.yml
vendored
Normal 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
35
.github/workflows/crowdin-upload.yml
vendored
Normal 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
21
.github/workflows/test-image-build.yml
vendored
Normal 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
|
101
.github/workflows/test-ruby.yml
vendored
101
.github/workflows/test-ruby.yml
vendored
@ -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/
|
||||||
|
@ -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:
|
||||||
|
15
.rubocop.yml
15
.rubocop.yml
@ -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
|
||||||
|
@ -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
|
|
||||||
|
262
CHANGELOG.md
262
CHANGELOG.md
@ -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
29
Gemfile
@ -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
|
||||||
|
237
Gemfile.lock
237
Gemfile.lock
@ -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)
|
||||||
|
10
README.md
10
README.md
@ -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 👇
|
|||||||
|
|
||||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283)
|
[](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
|
||||||
|
|
||||||
|
@ -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),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
12
app/chewy/instances_index.rb
Normal file
12
app/chewy/instances_index.rb
Normal 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
|
@ -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|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
21
app/controllers/api/v1/instances/languages_controller.rb
Normal file
21
app/controllers/api/v1/instances/languages_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
45
app/controllers/api/v1/peers/search_controller.rb
Normal file
45
app/controllers/api/v1/peers/search_controller.rb
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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?
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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|
|
||||||
|
@ -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|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
43
app/controllers/remote_interaction_helper_controller.rb
Normal file
43
app/controllers/remote_interaction_helper_controller.rb
Normal 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
|
@ -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)
|
||||||
|
@ -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|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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]))]));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
34
app/javascript/mastodon/components/badge.jsx
Normal file
34
app/javascript/mastodon/components/badge.jsx
Normal 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' />} />
|
||||||
|
);
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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} />);
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
@ -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' },
|
||||||
|
@ -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'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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' },
|
||||||
|
@ -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'>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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')}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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}>
|
||||||
|
@ -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' },
|
||||||
|
@ -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,
|
||||||
|
};
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
|
||||||
|
@ -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} />
|
||||||
|
@ -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' },
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user