Compare commits
No commits in common. "41e76b79025fc424f0d82d15263505dcb239b3b8" and "8115b5ec45f6b0c1e40824877f56be16a6dd3cce" have entirely different histories.
41e76b7902
...
8115b5ec45
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
ignore:
|
||||
# devise-two-factor advisory about brute-forcing TOTP
|
||||
# We have rate-limits on authentication endpoints in place (including second
|
||||
# factor verification) since Mastodon v3.2.0
|
||||
- CVE-2024-0227
|
|
@ -4,6 +4,10 @@ FROM mcr.microsoft.com/devcontainers/ruby:1-3.2-bullseye
|
|||
# Install Rails
|
||||
# RUN gem install rails webdrivers
|
||||
|
||||
# Default value to allow debug server to serve content over GitHub Codespace's port forwarding service
|
||||
# The value is a comma-separated list of allowed domains
|
||||
ENV RAILS_DEVELOPMENT_HOSTS=".githubpreview.dev,.preview.app.github.dev,.app.github.dev"
|
||||
|
||||
ARG NODE_VERSION="16"
|
||||
RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
|
||||
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
"name": "Mastodon on GitHub Codespaces",
|
||||
"dockerComposeFile": "../docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||
},
|
||||
|
||||
"runServices": ["app", "db", "redis"],
|
||||
|
||||
"forwardPorts": [3000, 4000],
|
||||
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "web",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"4000": {
|
||||
"label": "stream",
|
||||
"onAutoForward": "silent"
|
||||
}
|
||||
},
|
||||
|
||||
"otherPortsAttributes": {
|
||||
"onAutoForward": "silent"
|
||||
},
|
||||
|
||||
"remoteEnv": {
|
||||
"LOCAL_DOMAIN": "${localEnv:CODESPACE_NAME}-3000.app.github.dev",
|
||||
"LOCAL_HTTPS": "true",
|
||||
"STREAMING_API_BASE_URL": "https://${localEnv:CODESPACE_NAME}-4000.app.github.dev",
|
||||
"DISABLE_FORGERY_REQUEST_PROTECTION": "true",
|
||||
"ES_ENABLED": "",
|
||||
"LIBRE_TRANSLATE_ENDPOINT": ""
|
||||
},
|
||||
|
||||
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
||||
"postCreateCommand": ".devcontainer/post-create.sh",
|
||||
"waitFor": "postCreateCommand",
|
||||
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
"extensions": ["EditorConfig.EditorConfig", "webben.browserslist"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "Mastodon on local machine",
|
||||
"name": "Mastodon",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
@ -8,23 +8,13 @@
|
|||
"ghcr.io/devcontainers/features/sshd:1": {}
|
||||
},
|
||||
|
||||
"runServices": ["app", "db", "redis"],
|
||||
|
||||
"forwardPorts": [3000, 4000],
|
||||
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "web",
|
||||
"onAutoForward": "notify",
|
||||
"requireLocalPort": true
|
||||
},
|
||||
"4000": {
|
||||
"label": "stream",
|
||||
"onAutoForward": "silent",
|
||||
"requireLocalPort": true
|
||||
}
|
||||
},
|
||||
|
||||
"otherPortsAttributes": {
|
||||
"onAutoForward": "silent"
|
||||
"containerEnv": {
|
||||
"ES_ENABLED": "",
|
||||
"LIBRE_TRANSLATE_ENDPOINT": ""
|
||||
},
|
||||
|
||||
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
||||
|
|
|
@ -25,7 +25,6 @@ services:
|
|||
command: sleep infinity
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000'
|
||||
- '127.0.0.1:3035:3035'
|
||||
- '127.0.0.1:4000:4000'
|
||||
networks:
|
||||
- external_network
|
||||
|
|
|
@ -2,7 +2,3 @@ VAGRANT=true
|
|||
LOCAL_DOMAIN=mastodon.local
|
||||
BIND=0.0.0.0
|
||||
DB_HOST=/var/run/postgresql/
|
||||
|
||||
ES_ENABLED=true
|
||||
ES_HOST=localhost
|
||||
ES_PORT=9200
|
|
@ -4,16 +4,11 @@ on:
|
|||
platforms:
|
||||
required: true
|
||||
type: string
|
||||
cache:
|
||||
type: boolean
|
||||
default: true
|
||||
use_native_arm64_builder:
|
||||
type: boolean
|
||||
push_to_images:
|
||||
type: string
|
||||
version_prerelease:
|
||||
type: string
|
||||
version_metadata:
|
||||
version_suffix:
|
||||
type: string
|
||||
flavor:
|
||||
type: string
|
||||
|
@ -27,7 +22,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
||||
|
@ -79,6 +74,8 @@ jobs:
|
|||
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 }}
|
||||
|
@ -86,14 +83,12 @@ jobs:
|
|||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||
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: ${{ inputs.cache && 'type=gha' || '' }}
|
||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
@ -16,9 +16,9 @@ jobs:
|
|||
env:
|
||||
TZ: Etc/UTC
|
||||
run: |
|
||||
echo mastodon_version_prerelease=nightly.$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
|
||||
echo mastodon_version_suffix=nightly-$(date +'%Y-%m-%d')>> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
prerelease: ${{ steps.version_vars.outputs.mastodon_version_prerelease }}
|
||||
suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
|
||||
|
||||
build-image:
|
||||
needs: compute-suffix
|
||||
|
@ -26,11 +26,11 @@ jobs:
|
|||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
use_native_arm64_builder: true
|
||||
cache: false
|
||||
push_to_images: |
|
||||
tootsuite/mastodon
|
||||
ghcr.io/mastodon/mastodon
|
||||
version_prerelease: ${{ needs.compute-suffix.outputs.prerelease }}
|
||||
# The `+` is important here, result will be v4.1.2+nightly-2022-03-05
|
||||
version_suffix: +${{ needs.compute-suffix.outputs.suffix }}
|
||||
labels: |
|
||||
org.opencontainers.image.description=Nightly build image used for testing purposes
|
||||
flavor: |
|
||||
|
@ -38,5 +38,5 @@ jobs:
|
|||
tags: |
|
||||
type=raw,value=edge
|
||||
type=raw,value=nightly
|
||||
type=schedule,pattern=${{ needs.compute-suffix.outputs.prerelease }}
|
||||
type=schedule,pattern=${{ needs.compute-suffix.outputs.suffix }}
|
||||
secrets: inherit
|
||||
|
|
|
@ -18,12 +18,12 @@ jobs:
|
|||
steps:
|
||||
# Repository needs to be cloned so `git rev-parse` below works
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- id: version_vars
|
||||
run: |
|
||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
|
||||
echo mastodon_version_suffix=+pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
|
||||
suffix: ${{ steps.version_vars.outputs.mastodon_version_suffix }}
|
||||
|
||||
build-image:
|
||||
needs: compute-suffix
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
use_native_arm64_builder: true
|
||||
push_to_images: |
|
||||
ghcr.io/mastodon/mastodon
|
||||
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||
version_suffix: ${{ needs.compute-suffix.outputs.suffix }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
|
|
|
@ -17,12 +17,8 @@ jobs:
|
|||
push_to_images: |
|
||||
tootsuite/mastodon
|
||||
ghcr.io/mastodon/mastodon
|
||||
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||
cache: false
|
||||
# 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.2.') }}
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.1.') }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
|
|
|
@ -27,7 +27,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Increase Git http.postBuffer
|
||||
# This is needed due to a bug in Ubuntu's cURL version?
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: crowdin action
|
||||
uses: crowdin/github-action@v1
|
||||
|
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
|
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: sudo apt-get install -y libicu-dev libidn11-dev
|
||||
|
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
BUNDLE_RETRY: 3
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
|
|
|
@ -69,7 +69,7 @@ jobs:
|
|||
BUNDLE_RETRY: 3
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install native Ruby dependencies
|
||||
run: |
|
||||
|
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
SECRET_KEY_BASE: precompile_placeholder
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
|||
- 3
|
||||
- 4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
@ -202,7 +202,7 @@ jobs:
|
|||
- '.ruby-version'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
@ -250,116 +250,3 @@ jobs:
|
|||
with:
|
||||
name: e2e-screenshots
|
||||
path: tmp/screenshots/
|
||||
|
||||
test-search:
|
||||
name: Testing search
|
||||
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
|
||||
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||
env:
|
||||
discovery.type: single-node
|
||||
xpack.security.enabled: false
|
||||
options: >-
|
||||
--health-cmd "curl http://localhost:9200/_cluster/health"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
ports:
|
||||
- 9200:9200
|
||||
|
||||
env:
|
||||
DB_HOST: localhost
|
||||
DB_USER: postgres
|
||||
DB_PASS: postgres
|
||||
DISABLE_SIMPLECOV: true
|
||||
RAILS_ENV: test
|
||||
BUNDLE_WITH: test
|
||||
ES_ENABLED: true
|
||||
ES_HOST: localhost
|
||||
ES_PORT: 9200
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
ruby-version:
|
||||
- '3.0'
|
||||
- '3.1'
|
||||
- '.ruby-version'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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:search
|
||||
|
||||
- name: Archive logs
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-logs-${{ matrix.ruby-version }}
|
||||
path: log/
|
||||
|
||||
- name: Archive test screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: test-search-screenshots
|
||||
path: tmp/screenshots/
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.56.1.
|
||||
# using RuboCop version 1.54.2.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
@ -37,7 +37,7 @@ Layout/HashAlignment:
|
|||
Layout/LeadingCommentSpace:
|
||||
Exclude:
|
||||
- 'config/application.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
- 'config/initializers/omniauth.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
|
||||
|
@ -61,8 +61,38 @@ Lint/EmptyBlock:
|
|||
- 'spec/fabricators/access_token_fabricator.rb'
|
||||
- 'spec/fabricators/conversation_fabricator.rb'
|
||||
- 'spec/fabricators/system_key_fabricator.rb'
|
||||
- 'spec/helpers/admin/action_logs_helper_spec.rb'
|
||||
- 'spec/lib/activitypub/adapter_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/user_role_spec.rb'
|
||||
- 'spec/models/web/setting_spec.rb'
|
||||
|
||||
Lint/NonLocalExitFromIterator:
|
||||
Exclude:
|
||||
|
@ -86,7 +116,7 @@ Lint/UnusedBlockArgument:
|
|||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'app/services/activitypub/process_status_update_service.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
- 'config/initializers/omniauth.rb'
|
||||
- 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
|
||||
- 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
|
||||
- 'spec/controllers/api/v1/favourites_controller_spec.rb'
|
||||
|
@ -105,7 +135,7 @@ Lint/UselessAssignment:
|
|||
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 144
|
||||
Max: 146
|
||||
|
||||
# Configuration parameters: CountBlocks, Max.
|
||||
Metrics/BlockNesting:
|
||||
|
@ -134,19 +164,6 @@ Naming/VariableNumber:
|
|||
- 'spec/models/domain_block_spec.rb'
|
||||
- 'spec/models/user_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: SafeMultiline.
|
||||
Performance/DeletePrefix:
|
||||
Exclude:
|
||||
- 'app/models/featured_tag.rb'
|
||||
|
||||
Performance/MapMethodChain:
|
||||
Exclude:
|
||||
- 'app/models/feed.rb'
|
||||
- 'lib/mastodon/cli/maintenance.rb'
|
||||
- 'spec/services/bulk_import_service_spec.rb'
|
||||
- 'spec/services/import_service_spec.rb'
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Exclude:
|
||||
- 'spec/controllers/activitypub/inboxes_controller_spec.rb'
|
||||
|
@ -289,6 +306,10 @@ RSpec/MultipleMemoizedHelpers:
|
|||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
RSpec/PendingWithoutReason:
|
||||
Exclude:
|
||||
- 'spec/models/account_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/ApplicationController:
|
||||
Exclude:
|
||||
|
@ -569,11 +590,11 @@ Style/FetchEnvVar:
|
|||
- 'config/environments/development.rb'
|
||||
- 'config/environments/production.rb'
|
||||
- 'config/initializers/2_limited_federation_mode.rb'
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
- 'config/initializers/blacklists.rb'
|
||||
- 'config/initializers/cache_buster.rb'
|
||||
- 'config/initializers/content_security_policy.rb'
|
||||
- 'config/initializers/devise.rb'
|
||||
- 'config/initializers/omniauth.rb'
|
||||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/initializers/vapid.rb'
|
||||
- 'lib/mastodon/premailer_webpack_strategy.rb'
|
||||
|
@ -741,15 +762,6 @@ Style/RedundantFetchBlock:
|
|||
- 'config/initializers/paperclip.rb'
|
||||
- 'config/puma.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowMultipleReturnValues.
|
||||
Style/RedundantReturn:
|
||||
Exclude:
|
||||
- 'app/controllers/api/v1/directories_controller.rb'
|
||||
- 'app/controllers/auth/confirmations_controller.rb'
|
||||
- 'app/lib/ostatus/tag_manager.rb'
|
||||
- 'app/models/form/import.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength.
|
||||
# AllowedMethods: present?, blank?, presence, try, try!
|
||||
|
@ -807,7 +819,7 @@ Style/StringLiterals:
|
|||
# AllowedMethods: define_method, mail, respond_to
|
||||
Style/SymbolProc:
|
||||
Exclude:
|
||||
- 'config/initializers/3_omniauth.rb'
|
||||
- 'config/initializers/omniauth.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.2.3
|
||||
3.2.2
|
||||
|
|
1676
AUTHORS.md
1676
AUTHORS.md
File diff suppressed because it is too large
Load Diff
254
CHANGELOG.md
254
CHANGELOG.md
|
@ -2,207 +2,55 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.2.7] - 2024-02-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OmniAuth tests and edge cases in error handling ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/29201), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/29207))
|
||||
- Fix new installs by upgrading to the latest release of the `nsa` gem, instead of a no longer existing commit ([mjankowski](https://github.com/mastodon/mastodon/pull/29065))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix insufficient checking of remote posts ([GHSA-jhrq-qvrm-qr36](https://github.com/mastodon/mastodon/security/advisories/GHSA-jhrq-qvrm-qr36))
|
||||
|
||||
## [4.2.6] - 2024-02-14
|
||||
|
||||
### Security
|
||||
|
||||
- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
|
||||
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
|
||||
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
|
||||
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
|
||||
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
|
||||
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
|
||||
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
|
||||
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
|
||||
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
|
||||
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
|
||||
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
|
||||
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
|
||||
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
|
||||
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.
|
||||
|
||||
## [4.2.5] - 2024-02-01
|
||||
|
||||
### Security
|
||||
|
||||
- Fix insufficient origin validation (CVE-2024-23832, [GHSA-3fjr-858r-92rw](https://github.com/mastodon/mastodon/security/advisories/GHSA-3fjr-858r-92rw))
|
||||
|
||||
## [4.2.4] - 2024-01-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix error when processing remote files with unusually long names ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28823))
|
||||
- Fix processing of compacted single-item JSON-LD collections ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28816))
|
||||
- Retry 401 errors on replies fetching ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/28788))
|
||||
- Fix `RecordNotUnique` errors in LinkCrawlWorker ([tribela](https://github.com/mastodon/mastodon/pull/28748))
|
||||
- Fix Mastodon not correctly processing HTTP Signatures with query strings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28443), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28476))
|
||||
- Fix potential redirection loop of streaming endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28665))
|
||||
- Fix streaming API redirection ignoring the port of `streaming_api_base_url` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28558))
|
||||
- Fix error when processing link preview with an array as `inLanguage` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28252))
|
||||
- Fix unsupported time zone or locale preventing sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/28035))
|
||||
- Fix "Hide these posts from home" list setting not refreshing when switching lists ([brianholley](https://github.com/mastodon/mastodon/pull/27763))
|
||||
- Fix missing background behind dismissable banner in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/27479))
|
||||
- Fix line wrapping of language selection button with long locale codes ([gunchleoc](https://github.com/mastodon/mastodon/pull/27100), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27127))
|
||||
- Fix `Undo Announce` activity not being sent to non-follower authors ([MitarashiDango](https://github.com/mastodon/mastodon/pull/18482))
|
||||
- Fix N+1s because of association preloaders not actually getting called ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28339))
|
||||
- Fix empty column explainer getting cropped under certain conditions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28337))
|
||||
- Fix `LinkCrawlWorker` error when encountering empty OEmbed response ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28268))
|
||||
- Fix call to inefficient `delete_matched` cache method in domain blocks ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28367))
|
||||
|
||||
### Security
|
||||
|
||||
- Add rate-limit of TOTP authentication attempts at controller level ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/28801))
|
||||
|
||||
## [4.2.3] - 2023-12-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix dependency on `json-canonicalization` version that has been made unavailable since last release
|
||||
|
||||
## [4.2.2] - 2023-12-04
|
||||
|
||||
### Changed
|
||||
|
||||
- Change dismissed banners to be stored server-side ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27055))
|
||||
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
||||
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
||||
- Change single-column navigation notice to be displayed outside of the logo container ([renchap](https://github.com/mastodon/mastodon/pull/27462), [renchap](https://github.com/mastodon/mastodon/pull/27476))
|
||||
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
||||
- Change post language code to include country code when relevant ([gunchleoc](https://github.com/mastodon/mastodon/pull/27099), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27207))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix upper border radius of onboarding columns ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27890))
|
||||
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
||||
- Fix some posts from threads received out-of-order sometimes not being inserted into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27653))
|
||||
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
||||
- Fix error when trying to delete already-deleted file with OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27569))
|
||||
- Fix batch attachment deletion when using OpenStack Swift ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27554))
|
||||
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
||||
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
|
||||
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
|
||||
- Fix handling of `inLanguage` attribute in preview card processing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27423))
|
||||
- Fix own posts being removed from home timeline when unfollowing a used hashtag ([kmycode](https://github.com/mastodon/mastodon/pull/27391))
|
||||
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
|
||||
- Fix format-dependent redirects being cached regardless of requested format ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27634))
|
||||
|
||||
## [4.2.1] - 2023-10-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add redirection on `/deck` URLs for logged-out users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27128))
|
||||
- Add support for v4.2.0 migrations to `tootctl maintenance fix-duplicates` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27147))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change some worker lock TTLs to be shorter-lived ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27246))
|
||||
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix duplicate reports being sent when reporting some remote posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27355))
|
||||
- Fix clicking on already-opened thread post scrolling to the top of the thread ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27331), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27338), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27350))
|
||||
- Fix some remote posts getting truncated ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27307))
|
||||
- Fix some cases of infinite scroll code trying to fetch inaccessible posts in a loop ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27286))
|
||||
- Fix `Vary` headers not being set on some redirects ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27272))
|
||||
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
|
||||
- Fix unexpected linebreak in version string in the Web UI ([vmstan](https://github.com/mastodon/mastodon/pull/26986))
|
||||
- Fix double scroll bars in some columns in advanced interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27187))
|
||||
- Fix boosts of local users being filtered in account timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27204))
|
||||
- Fix multiple instances of the trend refresh scheduler sometimes running at once ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27253))
|
||||
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
|
||||
- Fix incorrectly keeping outdated update notices absent from the API endpoint ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27021))
|
||||
- Fix import progress not updating on certain failures ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27247))
|
||||
- Fix websocket connections being incorrectly decremented twice on errors ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/27238))
|
||||
- Fix explore prompt appearing because of posts being received out of order ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27211))
|
||||
- Fix explore prompt sometimes showing up when the home TL is loading ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27062))
|
||||
- Fix link handling of mentions in user profiles when logged out ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27185))
|
||||
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
|
||||
- Fix notification toasts not respecting reduce-motion ([c960657](https://github.com/mastodon/mastodon/pull/27178))
|
||||
- Fix retention dashboard not displaying correct month ([vmstan](https://github.com/mastodon/mastodon/pull/27180))
|
||||
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
|
||||
- Fix division by zero in video in bitrate computation code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27129))
|
||||
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
|
||||
- Fix ActiveRecord using two connection pools when no replica is defined ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27061))
|
||||
- Fix the search documentation URL in system checks ([renchap](https://github.com/mastodon/mastodon/pull/27036))
|
||||
|
||||
## [4.2.0] - 2023-09-21
|
||||
## [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 full-text search of opted-in public posts and rework search operators** ([Gargron](https://github.com/mastodon/mastodon/pull/26485), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26344), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26657), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26650), [jsgoldstein](https://github.com/mastodon/mastodon/pull/26659), [Gargron](https://github.com/mastodon/mastodon/pull/26660), [Gargron](https://github.com/mastodon/mastodon/pull/26663), [Gargron](https://github.com/mastodon/mastodon/pull/26688), [Gargron](https://github.com/mastodon/mastodon/pull/26689), [Gargron](https://github.com/mastodon/mastodon/pull/26686), [Gargron](https://github.com/mastodon/mastodon/pull/26687), [Gargron](https://github.com/mastodon/mastodon/pull/26692), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26697), [Gargron](https://github.com/mastodon/mastodon/pull/26699), [Gargron](https://github.com/mastodon/mastodon/pull/26701), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26710), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26739), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26754), [Gargron](https://github.com/mastodon/mastodon/pull/26662), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26755), [Gargron](https://github.com/mastodon/mastodon/pull/26781), [Gargron](https://github.com/mastodon/mastodon/pull/26782), [Gargron](https://github.com/mastodon/mastodon/pull/26760), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26756), [Gargron](https://github.com/mastodon/mastodon/pull/26784), [Gargron](https://github.com/mastodon/mastodon/pull/26807), [Gargron](https://github.com/mastodon/mastodon/pull/26835), [Gargron](https://github.com/mastodon/mastodon/pull/26847), [Gargron](https://github.com/mastodon/mastodon/pull/26834), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26893), [tribela](https://github.com/mastodon/mastodon/pull/26896), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26927), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26959), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27014))
|
||||
This introduces a new `public_statuses` Elasticsearch index for public posts by users who have opted in to their posts being searchable (`toot#indexable` flag).
|
||||
This also revisits the other indexes to provide more useful indexing, and adds new search operators such as `from:me`, `before:2022-11-01`, `after:2022-11-01`, `during:2022-11-01`, `language:fr`, `has:poll`, or `in:library` (for searching only in posts you have written or interacted with).
|
||||
Results are now ordered chronologically.
|
||||
- **Add admin notifications for new Mastodon versions** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26582))
|
||||
This is done by querying `https://api.joinmastodon.org/update-check` every 30 minutes in a background job.
|
||||
That URL can be changed using the `UPDATE_CHECK_URL` environment variable, and the feature outright disabled by setting that variable to an empty string (`UPDATE_CHECK_URL=`).
|
||||
- **Add “Privacy and reach” tab in profile settings** ([Gargron](https://github.com/mastodon/mastodon/pull/26484), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26508))
|
||||
This reorganized scattered privacy and reach settings to a single place, as well as improve their wording.
|
||||
- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26606), [Gargron](https://github.com/mastodon/mastodon/pull/26666), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26960))
|
||||
- **Add display of out-of-band hashtags in the web interface** ([Gargron](https://github.com/mastodon/mastodon/pull/26492), [arbolitoloco1](https://github.com/mastodon/mastodon/pull/26497), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26506), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26525))
|
||||
- **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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26636))
|
||||
- **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 `ONE_CLICK_SSO_LOGIN` environment variable to directly 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), [CSDUMMI](https://github.com/mastodon/mastodon/pull/26857), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26901))
|
||||
- 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, necropolina](https://github.com/mastodon/mastodon/pull/22048), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/25324))
|
||||
- **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 `S3_DISABLE_CHECKSUM_MODE` environment variable for compatibility with some S3-compatible providers** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
||||
- **Add auto-refresh of accounts we get new messages/edits of** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26510))
|
||||
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26605), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26658))
|
||||
- Add `hide_collections`, `discoverable` and `indexable` attributes to credentials API ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26998))
|
||||
- Add `S3_ENABLE_CHECKSUM_MODE` environment variable to enable checksum verification on compatible S3-providers ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26435))
|
||||
- Add admin API for managing tags ([rrgeorge](https://github.com/mastodon/mastodon/pull/26872))
|
||||
- Add a link to hashtag timelines from the Trending hashtags moderation interface ([gunchleoc](https://github.com/mastodon/mastodon/pull/26724))
|
||||
- Add timezone to datetimes in e-mails ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26822))
|
||||
- Add `authorized_fetch` server setting in addition to env var ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25798), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26958))
|
||||
- Add avatar image to webfinger responses ([tvler](https://github.com/mastodon/mastodon/pull/26558))
|
||||
- Add debug logging on signature verification failure ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26637), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26812))
|
||||
- Add explicit error messages when DeepL quota is exceeded ([lutoma](https://github.com/mastodon/mastodon/pull/26704))
|
||||
- Add Elasticsearch/OpenSearch version to “Software” in admin dashboard ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26652))
|
||||
- Add `data-nosnippet` attribute to remote posts and local posts with `noindex` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26648))
|
||||
- Add support for federating `memorial` attribute ([rrgeorge](https://github.com/mastodon/mastodon/pull/26583))
|
||||
- Add Cherokee and Kalmyk to languages dropdown ([gunchleoc](https://github.com/mastodon/mastodon/pull/26012), [gunchleoc](https://github.com/mastodon/mastodon/pull/26013))
|
||||
- **Add Elasticsearch cluster health check and indexes mismatch check to dashboard** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26448))
|
||||
- Add support for `indexable` attribute on remote actors ([Gargron](https://github.com/mastodon/mastodon/pull/26485))
|
||||
- Add `DELETE /api/v1/profile/avatar` and `DELETE /api/v1/profile/header` to the REST API ([danielmbrasil](https://github.com/mastodon/mastodon/pull/25124), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26573))
|
||||
- Add `ES_PRESET` option to customize numbers of shards and replicas ([Gargron](https://github.com/mastodon/mastodon/pull/26483), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26489))
|
||||
This can have a value of `single_node_cluster` (default), `small_cluster` (uses one replica) or `large_cluster` (uses one replica and a higher number of shards).
|
||||
- Add missing `instances` option to `tootctl search deploy` ([tribela](https://github.com/mastodon/mastodon/pull/26461))
|
||||
- Add `CACHE_BUSTER_HTTP_METHOD` environment variable ([renchap](https://github.com/mastodon/mastodon/pull/26528), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26542))
|
||||
- Add support for `DB_PASS` when using `DATABASE_URL` ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26295))
|
||||
- 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26737), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26979))
|
||||
- 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26447))
|
||||
- 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), [Gargron](https://github.com/mastodon/mastodon/pull/26664))
|
||||
- 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), [Gargron](https://github.com/mastodon/mastodon/pull/26829), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26935))
|
||||
- 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))
|
||||
|
@ -240,48 +88,41 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26934))
|
||||
- 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 hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499), [renchap](https://github.com/mastodon/mastodon/pull/26614), [renchap](https://github.com/mastodon/mastodon/pull/26615))
|
||||
- **Change hashtags to be displayed separately when they are the last line of a post** ([renchap](https://github.com/mastodon/mastodon/pull/26499))
|
||||
- **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), [mgmn](https://github.com/mastodon/mastodon/pull/26459), [tribela](https://github.com/mastodon/mastodon/pull/26461), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26593), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26795))
|
||||
- **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), [mgmn](https://github.com/mastodon/mastodon/pull/26459))
|
||||
- **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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26452))
|
||||
- **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 tabs of 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26633))
|
||||
- **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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26856))
|
||||
- **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 DCT method used for JPEG encoding to float ([electroCutie](https://github.com/mastodon/mastodon/pull/26675))
|
||||
- Change from `node-redis` to `ioredis` for streaming ([gmemstr](https://github.com/mastodon/mastodon/pull/26581))
|
||||
- Change private statuses index to index without crutches ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26713))
|
||||
- Change video compression parameters ([Gargron](https://github.com/mastodon/mastodon/pull/26631), [Gargron](https://github.com/mastodon/mastodon/pull/26745), [Gargron](https://github.com/mastodon/mastodon/pull/26766), [Gargron](https://github.com/mastodon/mastodon/pull/26970))
|
||||
- Change admin e-mail notification settings to be their own settings group ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26596))
|
||||
- Change opacity of the delete icon in the search field to be more visible ([AntoninDelFabbro](https://github.com/mastodon/mastodon/pull/26449))
|
||||
- Change Account Search to prioritize username over display name ([jsgoldstein](https://github.com/mastodon/mastodon/pull/26623))
|
||||
- Change follow recommendation materialized view to be faster in most cases ([renchap, ClearlyClaire](https://github.com/mastodon/mastodon/pull/26545))
|
||||
- Change `robots.txt` to block GPTBot ([Foritus](https://github.com/mastodon/mastodon/pull/26396))
|
||||
- Change header of hashtag timelines in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26362), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26416))
|
||||
- Change streaming `/metrics` to include additional metrics ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26299), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26945))
|
||||
- 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), [Gargron](https://github.com/mastodon/mastodon/pull/26767))
|
||||
- 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), [Signez](https://github.com/mastodon/mastodon/pull/26019), [Signez](https://github.com/mastodon/mastodon/pull/26759))
|
||||
- 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))
|
||||
|
@ -295,11 +136,12 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26801))
|
||||
- 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))
|
||||
|
@ -310,7 +152,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- 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), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27012))
|
||||
- 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))
|
||||
|
@ -333,8 +175,6 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- **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 obfuscation of reply count in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26768))
|
||||
- Remove `kmr` from language selection, as it was a duplicate for `ku` ([gunchleoc](https://github.com/mastodon/mastodon/pull/26014), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26787))
|
||||
- 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))
|
||||
|
@ -348,23 +188,9 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- **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 migration handler not updating lists** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24808))
|
||||
- Fix crash when viewing a moderation appeal and the moderator account has been deleted ([xrobau](https://github.com/mastodon/mastodon/pull/25900))
|
||||
- Fix error in Web UI when server rules cannot be fetched ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26957))
|
||||
- Fix paragraph margins resulting in irregular read-more cut-off in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26828))
|
||||
- Fix notification permissions being requested immediately after login ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26472))
|
||||
- Fix performances of profile directory ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26840), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26842))
|
||||
- Fix mute button and volume slider feeling disconnected in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/26827), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/26860))
|
||||
- Fix “Scoped order is ignored, it's forced to be batch order.” warnings ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26793))
|
||||
- Fix blocked domain appearing in account feeds ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26823))
|
||||
- Fix invalid `Content-Type` header for WebP images ([c960657](https://github.com/mastodon/mastodon/pull/26773))
|
||||
- Fix minor inefficiencies in `tootctl search deploy` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26721))
|
||||
- Fix filter form in profiles directory overflowing instead of wrapping ([arbolitoloco1](https://github.com/mastodon/mastodon/pull/26682))
|
||||
- Fix sign up steps progress layout in right-to-left locales ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26728))
|
||||
- Fix bug with “favorited by” and “reblogged by“ view on posts only showing up to 40 items ([timothyjrogers](https://github.com/mastodon/mastodon/pull/26577), [timothyjrogers](https://github.com/mastodon/mastodon/pull/26574))
|
||||
- Fix bad search type heuristic ([Gargron](https://github.com/mastodon/mastodon/pull/26673))
|
||||
- Fix not being able to negate prefix clauses in search ([Gargron](https://github.com/mastodon/mastodon/pull/26672))
|
||||
- Fix timeout on invalid set of exclusionary parameters in `/api/v1/timelines/public` ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26239))
|
||||
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
||||
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
||||
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
||||
- 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))
|
||||
|
@ -429,34 +255,6 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
- 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.8] - 2023-09-19
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix post edits not being forwarded as expected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26936))
|
||||
- Fix moderator rights inconsistencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26729))
|
||||
- Fix crash when encountering invalid URL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26814))
|
||||
- Fix cached posts including stale stats ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26409))
|
||||
- Fix uploading of video files for which `ffprobe` reports `0/0` average framerate ([NicolaiSoeborg](https://github.com/mastodon/mastodon/pull/26500))
|
||||
- Fix unexpected audio stream transcoding when uploaded video is eligible to passthrough ([yufushiro](https://github.com/mastodon/mastodon/pull/26608))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix missing HTML sanitization in translation API (CVE-2023-42452, [GHSA-2693-xr3m-jhqr](https://github.com/mastodon/mastodon/security/advisories/GHSA-2693-xr3m-jhqr))
|
||||
- Fix incorrect domain name normalization (CVE-2023-42451, [GHSA-v3xf-c9qf-j667](https://github.com/mastodon/mastodon/security/advisories/GHSA-v3xf-c9qf-j667))
|
||||
|
||||
## [4.1.7] - 2023-09-05
|
||||
|
||||
### Changed
|
||||
|
||||
- Change remote report processing to accept reports with long comments, but truncate them ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/25028))
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Fix blocking subdomains of an already-blocked domain** ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26392))
|
||||
- Fix `/api/v1/timelines/tag/:hashtag` allowing for unauthenticated access when public preview is disabled ([danielmbrasil](https://github.com/mastodon/mastodon/pull/26237))
|
||||
- Fix inefficiencies in `PlainTextFormatter` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26727))
|
||||
|
||||
## [4.1.6] - 2023-07-31
|
||||
|
||||
### Fixed
|
||||
|
|
23
Dockerfile
23
Dockerfile
|
@ -1,8 +1,8 @@
|
|||
# syntax=docker/dockerfile:1.4
|
||||
# This needs to be bookworm-slim because the Ruby image is built on bookworm-slim
|
||||
ARG NODE_VERSION="20.6-bookworm-slim"
|
||||
# This needs to be bullseye-slim because the Ruby image is built on bullseye-slim
|
||||
ARG NODE_VERSION="16.20-bullseye-slim"
|
||||
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.3-slim as ruby
|
||||
FROM ghcr.io/moritzheiber/ruby-jemalloc:3.2.2-slim as ruby
|
||||
FROM node:${NODE_VERSION} as build
|
||||
|
||||
COPY --link --from=ruby /opt/ruby /opt/ruby
|
||||
|
@ -17,11 +17,10 @@ COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
|||
|
||||
# hadolint ignore=DL3008
|
||||
RUN apt-get update && \
|
||||
apt-get -yq dist-upgrade && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
git \
|
||||
libicu-dev \
|
||||
libidn-dev \
|
||||
libidn11-dev \
|
||||
libpq-dev \
|
||||
libjemalloc-dev \
|
||||
zlib1g-dev \
|
||||
|
@ -43,8 +42,8 @@ RUN apt-get update && \
|
|||
FROM node:${NODE_VERSION}
|
||||
|
||||
# Use those args to specify your own version flags & suffixes
|
||||
ARG MASTODON_VERSION_PRERELEASE=""
|
||||
ARG MASTODON_VERSION_METADATA=""
|
||||
ARG MASTODON_VERSION_FLAGS=""
|
||||
ARG MASTODON_VERSION_SUFFIX=""
|
||||
|
||||
ARG UID="991"
|
||||
ARG GID="991"
|
||||
|
@ -65,13 +64,13 @@ RUN apt-get update && \
|
|||
apt-get -y --no-install-recommends install whois \
|
||||
wget \
|
||||
procps \
|
||||
libssl3 \
|
||||
libssl1.1 \
|
||||
libpq5 \
|
||||
imagemagick \
|
||||
ffmpeg \
|
||||
libjemalloc2 \
|
||||
libicu72 \
|
||||
libidn12 \
|
||||
libicu67 \
|
||||
libidn11 \
|
||||
libyaml-0-2 \
|
||||
file \
|
||||
ca-certificates \
|
||||
|
@ -90,8 +89,8 @@ ENV RAILS_ENV="production" \
|
|||
NODE_ENV="production" \
|
||||
RAILS_SERVE_STATIC_FILES="true" \
|
||||
BIND="0.0.0.0" \
|
||||
MASTODON_VERSION_PRERELEASE="${MASTODON_VERSION_PRERELEASE}" \
|
||||
MASTODON_VERSION_METADATA="${MASTODON_VERSION_METADATA}"
|
||||
MASTODON_VERSION_FLAGS="${MASTODON_VERSION_FLAGS}" \
|
||||
MASTODON_VERSION_SUFFIX="${MASTODON_VERSION_SUFFIX}"
|
||||
|
||||
# Set the run user
|
||||
USER mastodon
|
||||
|
|
|
@ -27,5 +27,4 @@ More information on HTTP Signatures, as well as examples, can be found here: htt
|
|||
|
||||
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
|
||||
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
|
||||
- Followers collection synchronization: https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md
|
||||
- Search indexing consent for actors: https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md
|
||||
- Followers collection synchronization: https://git.activitypub.dev/ActivityPubDev/Fediverse-Enhancement-Proposals/src/branch/main/feps/fep-8fcf.md
|
||||
|
|
4
Gemfile
4
Gemfile
|
@ -61,7 +61,7 @@ gem 'kaminari', '~> 1.2'
|
|||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'nsa'
|
||||
gem 'nsa', github: 'jhawthorn/nsa', ref: 'e020fcc3a54d993ab45b7194d89ab720296c111b'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
gem 'parslet'
|
||||
|
@ -110,7 +110,7 @@ group :test do
|
|||
gem 'fuubar', '~> 2.5'
|
||||
|
||||
# Extra RSpec extenion methods and helpers for sidekiq
|
||||
gem 'rspec-sidekiq', '~> 4.0'
|
||||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
|
||||
# Browser integration testing
|
||||
gem 'capybara', '~> 3.39'
|
||||
|
|
208
Gemfile.lock
208
Gemfile.lock
|
@ -7,6 +7,17 @@ GIT
|
|||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/jhawthorn/nsa.git
|
||||
revision: e020fcc3a54d993ab45b7194d89ab720296c111b
|
||||
ref: e020fcc3a54d993ab45b7194d89ab720296c111b
|
||||
specs:
|
||||
nsa (0.2.8)
|
||||
activesupport (>= 4.2, < 7.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/mastodon/rails-settings-cached.git
|
||||
revision: 86328ef0bd04ce21cc0504ff5e334591e8c2ccab
|
||||
|
@ -28,47 +39,47 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actioncable (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailbox (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionmailer (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionpack (7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actiontext (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
actionview (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -78,27 +89,27 @@ GEM
|
|||
activemodel (>= 4.1, < 7.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activejob (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activerecord (7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activestorage (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
activemodel (7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activerecord (7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
activestorage (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.8)
|
||||
activesupport (7.0.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.5)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
aes_key_wrap (1.1.0)
|
||||
airbrussh (1.4.1)
|
||||
|
@ -113,8 +124,8 @@ GEM
|
|||
attr_required (1.0.1)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.809.0)
|
||||
aws-sdk-core (3.181.0)
|
||||
aws-partitions (1.793.0)
|
||||
aws-sdk-core (3.180.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
|
@ -122,8 +133,8 @@ GEM
|
|||
aws-sdk-kms (1.71.0)
|
||||
aws-sdk-core (~> 3, >= 3.177.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.133.0)
|
||||
aws-sdk-core (~> 3, >= 3.181.0)
|
||||
aws-sdk-s3 (1.132.1)
|
||||
aws-sdk-core (~> 3, >= 3.179.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.6)
|
||||
aws-sigv4 (1.6.0)
|
||||
|
@ -136,8 +147,6 @@ GEM
|
|||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||
net-http-persistent (~> 4.0)
|
||||
nokogiri (~> 1, >= 1.10.8)
|
||||
base64 (0.1.1)
|
||||
bcp47_spec (0.2.1)
|
||||
bcrypt (3.1.18)
|
||||
better_errors (2.10.1)
|
||||
erubi (>= 1.0.0)
|
||||
|
@ -193,7 +202,7 @@ GEM
|
|||
activesupport
|
||||
cbor (0.5.9.6)
|
||||
charlock_holmes (0.7.7)
|
||||
chewy (7.3.4)
|
||||
chewy (7.3.3)
|
||||
activesupport (>= 5.2)
|
||||
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||
elasticsearch-dsl
|
||||
|
@ -201,7 +210,7 @@ GEM
|
|||
climate_control (0.2.0)
|
||||
cocoon (1.2.15)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.2.3)
|
||||
concurrent-ruby (1.2.2)
|
||||
connection_pool (2.4.1)
|
||||
cose (1.3.0)
|
||||
cbor (~> 0.5.9)
|
||||
|
@ -314,7 +323,7 @@ GEM
|
|||
ruby-progressbar (~> 1.4)
|
||||
globalid (1.1.0)
|
||||
activesupport (>= 5.0)
|
||||
haml (6.1.2)
|
||||
haml (6.1.1)
|
||||
temple (>= 0.8.2)
|
||||
thor
|
||||
tilt
|
||||
|
@ -323,7 +332,7 @@ GEM
|
|||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.50.0)
|
||||
haml_lint (0.49.3)
|
||||
haml (>= 4.0, < 6.2)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
|
@ -367,19 +376,19 @@ GEM
|
|||
ipaddress (0.8.3)
|
||||
jmespath (1.6.2)
|
||||
json (2.6.3)
|
||||
json-canonicalization (1.0.0)
|
||||
json-canonicalization (0.3.2)
|
||||
json-jwt (1.15.3)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
bindata
|
||||
httpclient
|
||||
json-ld (3.3.1)
|
||||
json-ld (3.2.5)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 1.0)
|
||||
json-canonicalization (~> 0.3, >= 0.3.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.15)
|
||||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.3)
|
||||
rdf (~> 3.2, >= 3.2.10)
|
||||
json-ld-preloaded (3.2.2)
|
||||
json-ld (~> 3.2)
|
||||
rdf (~> 3.2)
|
||||
|
@ -399,7 +408,7 @@ GEM
|
|||
activerecord
|
||||
kaminari-core (= 1.2.2)
|
||||
kaminari-core (1.2.2)
|
||||
kt-paperclip (7.2.1)
|
||||
kt-paperclip (7.2.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
marcel (~> 1.0.1)
|
||||
|
@ -442,11 +451,11 @@ GEM
|
|||
hashie (~> 5.0)
|
||||
memory_profiler (1.0.1)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.5.1)
|
||||
mime-types (3.5.0)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0808)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
mini_portile2 (2.8.4)
|
||||
minitest (5.19.0)
|
||||
msgpack (1.7.1)
|
||||
multi_json (1.15.0)
|
||||
|
@ -468,16 +477,11 @@ GEM
|
|||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
net-ssh (7.1.0)
|
||||
nio4r (2.7.0)
|
||||
nokogiri (1.16.2)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.4)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.3.0)
|
||||
activesupport (>= 4.2, < 7.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.16.1)
|
||||
oj (3.15.0)
|
||||
omniauth (2.1.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
|
@ -514,8 +518,8 @@ GEM
|
|||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.5)
|
||||
pghero (3.3.4)
|
||||
pg (1.5.3)
|
||||
pghero (3.3.3)
|
||||
activerecord (>= 6)
|
||||
posix-spawn (0.3.15)
|
||||
premailer (1.21.0)
|
||||
|
@ -528,12 +532,12 @@ GEM
|
|||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
public_suffix (5.0.3)
|
||||
puma (6.4.2)
|
||||
puma (6.3.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.3)
|
||||
racc (1.7.1)
|
||||
rack (2.2.8)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
|
@ -551,20 +555,20 @@ GEM
|
|||
rack
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rails (7.0.8)
|
||||
actioncable (= 7.0.8)
|
||||
actionmailbox (= 7.0.8)
|
||||
actionmailer (= 7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
actiontext (= 7.0.8)
|
||||
actionview (= 7.0.8)
|
||||
activejob (= 7.0.8)
|
||||
activemodel (= 7.0.8)
|
||||
activerecord (= 7.0.8)
|
||||
activestorage (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
rails (7.0.7)
|
||||
actioncable (= 7.0.7)
|
||||
actionmailbox (= 7.0.7)
|
||||
actionmailer (= 7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
actiontext (= 7.0.7)
|
||||
actionview (= 7.0.7)
|
||||
activejob (= 7.0.7)
|
||||
activemodel (= 7.0.7)
|
||||
activerecord (= 7.0.7)
|
||||
activestorage (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.8)
|
||||
railties (= 7.0.7)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
|
@ -579,17 +583,16 @@ GEM
|
|||
rails-i18n (7.0.7)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.0.8)
|
||||
actionpack (= 7.0.8)
|
||||
activesupport (= 7.0.8)
|
||||
railties (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
rdf (3.3.1)
|
||||
bcp47_spec (~> 0.2)
|
||||
rdf (3.2.11)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.6.1)
|
||||
rdf (~> 3.2)
|
||||
|
@ -629,15 +632,12 @@ GEM
|
|||
rspec-expectations (~> 3.12)
|
||||
rspec-mocks (~> 3.12)
|
||||
rspec-support (~> 3.12)
|
||||
rspec-sidekiq (4.0.1)
|
||||
rspec-core (~> 3.0)
|
||||
rspec-expectations (~> 3.0)
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.12.1)
|
||||
rspec-sidekiq (3.1.0)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.12.0)
|
||||
rspec_chunked (0.6)
|
||||
rubocop (1.56.3)
|
||||
base64 (~> 0.1.1)
|
||||
rubocop (1.54.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
|
@ -645,7 +645,7 @@ GEM
|
|||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.28.1, < 2.0)
|
||||
rubocop-ast (>= 1.28.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.29.0)
|
||||
|
@ -654,14 +654,14 @@ GEM
|
|||
rubocop (~> 1.41)
|
||||
rubocop-factory_bot (2.23.1)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-performance (1.19.0)
|
||||
rubocop-performance (1.18.0)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.20.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop-rspec (2.23.2)
|
||||
rubocop-rspec (2.22.0)
|
||||
rubocop (~> 1.33)
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
|
@ -687,7 +687,7 @@ GEM
|
|||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
semantic_range (3.0.0)
|
||||
sidekiq (6.5.12)
|
||||
sidekiq (6.5.9)
|
||||
connection_pool (>= 2.2.5, < 3)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.5.0, < 5)
|
||||
|
@ -697,7 +697,7 @@ GEM
|
|||
rufus-scheduler (~> 3.2)
|
||||
sidekiq (>= 6, < 8)
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (7.1.33)
|
||||
sidekiq-unique-jobs (7.1.29)
|
||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
redis (< 5.0)
|
||||
|
@ -727,7 +727,7 @@ GEM
|
|||
net-ssh (>= 2.8.0)
|
||||
stackprof (0.2.25)
|
||||
statsd-ruby (1.5.0)
|
||||
stoplight (3.0.2)
|
||||
stoplight (3.0.1)
|
||||
redlock (~> 1.0)
|
||||
strong_migrations (0.8.0)
|
||||
activerecord (>= 5.2)
|
||||
|
@ -741,8 +741,8 @@ GEM
|
|||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
test-prof (1.2.3)
|
||||
thor (1.3.0)
|
||||
test-prof (1.2.2)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
timeout (0.4.0)
|
||||
tpm-key_attestation (0.12.0)
|
||||
|
@ -791,7 +791,7 @@ GEM
|
|||
webfinger (1.2.0)
|
||||
activesupport
|
||||
httpclient (>= 2.4)
|
||||
webmock (3.19.1)
|
||||
webmock (3.18.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
|
@ -878,7 +878,7 @@ DEPENDENCIES
|
|||
net-http (~> 0.3.2)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
nsa
|
||||
nsa!
|
||||
oj (~> 3.14)
|
||||
omniauth (~> 2.0)
|
||||
omniauth-cas!
|
||||
|
@ -909,7 +909,7 @@ DEPENDENCIES
|
|||
redis-namespace (~> 1.10)
|
||||
rqrcode (~> 2.2)
|
||||
rspec-rails (~> 6.0)
|
||||
rspec-sidekiq (~> 4.0)
|
||||
rspec-sidekiq (~> 3.1)
|
||||
rspec_chunked (~> 0.6)
|
||||
rubocop
|
||||
rubocop-capybara
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
||||
stream: env PORT=4000 yarn run start
|
||||
webpack: bin/webpack-dev-server
|
||||
webpack: ./bin/webpack-dev-server --listen-host 0.0.0.0
|
||||
|
|
36
README.md
36
README.md
|
@ -59,13 +59,13 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre
|
|||
|
||||
## Deployment
|
||||
|
||||
### Tech stack
|
||||
### Tech stack:
|
||||
|
||||
- **Ruby on Rails** powers the REST API and other web pages
|
||||
- **React.js** and Redux are used for the dynamic parts of the interface
|
||||
- **Node.js** powers the streaming API
|
||||
|
||||
### Requirements
|
||||
### Requirements:
|
||||
|
||||
- **PostgreSQL** 9.5+
|
||||
- **Redis** 4+
|
||||
|
@ -74,10 +74,6 @@ 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.
|
||||
|
||||
## Development
|
||||
|
||||
### Vagrant
|
||||
|
||||
A **Vagrant** configuration is included for development purposes. To use it, complete the following steps:
|
||||
|
||||
- Install Vagrant and Virtualbox
|
||||
|
@ -86,11 +82,9 @@ A **Vagrant** configuration is included for development purposes. To use it, com
|
|||
- Run `vagrant ssh -c "cd /vagrant && foreman start"`
|
||||
- Open `http://mastodon.local` in your browser
|
||||
|
||||
### MacOS
|
||||
|
||||
To set up **MacOS** for native development, complete the following steps:
|
||||
|
||||
- Install the latest stable Ruby version (use a Ruby version manager for easy installation and management of Ruby versions)
|
||||
- Install the latest stable Ruby version (use a ruby version manager for easy installation and management of ruby versions)
|
||||
- Run `brew install postgresql@14`
|
||||
- Run `brew install redis`
|
||||
- Run `brew install imagemagick`
|
||||
|
@ -100,27 +94,15 @@ To set up **MacOS** for native development, complete the following steps:
|
|||
- Run `bundle exec rails db:setup` (optionally prepend `RAILS_ENV=development` to target the dev environment)
|
||||
- Finally, run `overmind start -f Procfile.dev`
|
||||
|
||||
### Docker
|
||||
### Getting Started with GitHub Codespaces
|
||||
|
||||
For development with **Docker**, complete the following steps:
|
||||
To get started, create a codespace for this repository by clicking this 👇
|
||||
|
||||
- Install Docker Desktop
|
||||
- Run `docker compose -f .devcontainer/docker-compose.yml up -d`
|
||||
- Run `docker compose -f .devcontainer/docker-compose.yml exec app .devcontainer/post-create.sh`
|
||||
- Finally, run `docker compose -f .devcontainer/docker-compose.yml exec app foreman start -f Procfile.dev`
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283)
|
||||
|
||||
If you are using an IDE with [support for the Development Container specification](https://containers.dev/supporting), it will run the above `docker compose` commands automatically. For **Visual Studio Code** this requires the [Dev Container extension](https://containers.dev/supporting#dev-containers).
|
||||
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.
|
||||
|
||||
### GitHub Codespaces
|
||||
|
||||
To get you coding in just a few minutes, GitHub Codespaces provides a web-based version of Visual Studio Code and a cloud-hosted development environment fully configured with the software needed for this project..
|
||||
|
||||
- Click this button to create a new codespace:<br>
|
||||
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283&devcontainer_path=.devcontainer%2Fcodespaces%2Fdevcontainer.json)
|
||||
- Wait for the environment to build. This will take a few minutes.
|
||||
- When the editor is ready, run `foreman start -f Procfile.dev` in the terminal.
|
||||
- After a few seconds, a popup will appear with a button labeled _Open in Browser_. This will open Mastodon.
|
||||
- On the _Ports_ tab, right click on the “stream” row and select _Port visibility_ → _Public_.
|
||||
**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
|
||||
|
||||
|
@ -132,7 +114,7 @@ You can open issues for bugs you've found or features you think are missing. You
|
|||
|
||||
## License
|
||||
|
||||
Copyright (C) 2016-2023 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
|
||||
Copyright (C) 2016-2022 Eugen Rochko & other Mastodon contributors (see [AUTHORS.md](AUTHORS.md))
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
|
|
14
SECURITY.md
14
SECURITY.md
|
@ -1,11 +1,8 @@
|
|||
# Security Policy
|
||||
|
||||
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either:
|
||||
If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can reach us at <security@joinmastodon.org>.
|
||||
|
||||
- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new)
|
||||
- reach us at <security@joinmastodon.org>
|
||||
|
||||
You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
|
||||
You should _not_ report such issues on GitHub or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk.
|
||||
|
||||
## Scope
|
||||
|
||||
|
@ -14,7 +11,8 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ---------------- |
|
||||
| 4.2.x | Yes |
|
||||
| ------- | --------- |
|
||||
| 4.1.x | Yes |
|
||||
| < 4.1 | No |
|
||||
| 4.0.x | Yes |
|
||||
| 3.5.x | Yes |
|
||||
| < 3.5 | No |
|
||||
|
|
|
@ -60,38 +60,6 @@ sudo usermod -a -G rvm $USER
|
|||
|
||||
SCRIPT
|
||||
|
||||
$provisionElasticsearch = <<SCRIPT
|
||||
# Install Elastic Search
|
||||
sudo apt install openjdk-17-jre-headless -y
|
||||
sudo wget -O /usr/share/keyrings/elasticsearch.asc https://artifacts.elastic.co/GPG-KEY-elasticsearch
|
||||
sudo sh -c 'echo "deb [signed-by=/usr/share/keyrings/elasticsearch.asc] https://artifacts.elastic.co/packages/7.x/apt stable main" > /etc/apt/sources.list.d/elastic-7.x.list'
|
||||
sudo apt update
|
||||
sudo apt install elasticsearch -y
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now elasticsearch
|
||||
|
||||
echo 'path.data: /var/lib/elasticsearch
|
||||
path.logs: /var/log/elasticsearch
|
||||
network.host: 0.0.0.0
|
||||
http.port: 9200
|
||||
discovery.seed_hosts: ["localhost"]
|
||||
cluster.initial_master_nodes: ["node-1"]
|
||||
xpack.security.enabled: false' > /etc/elasticsearch/elasticsearch.yml
|
||||
|
||||
sudo systemctl restart elasticsearch
|
||||
|
||||
# Install Kibana
|
||||
sudo apt install kibana -y
|
||||
sudo systemctl enable --now kibana
|
||||
|
||||
echo 'server.host: "0.0.0.0"
|
||||
elasticsearch.hosts: ["http://localhost:9200"]' > /etc/kibana/kibana.yml
|
||||
|
||||
sudo systemctl restart kibana
|
||||
|
||||
SCRIPT
|
||||
|
||||
$provisionB = <<SCRIPT
|
||||
|
||||
source "/etc/profile.d/rvm.sh"
|
||||
|
@ -134,8 +102,10 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
vb.name = "mastodon"
|
||||
vb.customize ["modifyvm", :id, "--memory", "8192"]
|
||||
vb.customize ["modifyvm", :id, "--cpus", "3"]
|
||||
vb.customize ["modifyvm", :id, "--memory", "2048"]
|
||||
# Increase the number of CPUs. Uncomment and adjust to
|
||||
# increase performance
|
||||
# vb.customize ["modifyvm", :id, "--cpus", "3"]
|
||||
|
||||
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
|
||||
# https://github.com/mitchellh/vagrant/issues/1172
|
||||
|
@ -171,15 +141,9 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
config.vm.network :forwarded_port, guest: 3000, host: 3000
|
||||
config.vm.network :forwarded_port, guest: 4000, host: 4000
|
||||
config.vm.network :forwarded_port, guest: 8080, host: 8080
|
||||
config.vm.network :forwarded_port, guest: 9200, host: 9200
|
||||
config.vm.network :forwarded_port, guest: 9300, host: 9300
|
||||
config.vm.network :forwarded_port, guest: 9243, host: 9243
|
||||
config.vm.network :forwarded_port, guest: 5601, host: 5601
|
||||
|
||||
# Full provisioning script, only runs on first 'vagrant up' or with 'vagrant provision'
|
||||
config.vm.provision :shell, inline: $provisionA, privileged: false, reset: true
|
||||
# Run with elevated privileges for Elasticsearch installation
|
||||
config.vm.provision :shell, inline: $provisionElasticsearch, privileged: true
|
||||
config.vm.provision :shell, inline: $provisionB, privileged: false
|
||||
|
||||
config.vm.post_up_message = <<MESSAGE
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
|
@ -23,13 +21,12 @@ class AccountsIndex < Chewy::Index
|
|||
|
||||
analyzer: {
|
||||
natural: {
|
||||
tokenizer: 'standard',
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(
|
||||
english_possessive_stemmer
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
|
@ -62,9 +59,9 @@ class AccountsIndex < Chewy::Index
|
|||
field(:following_count, type: 'long')
|
||||
field(:followers_count, type: 'long')
|
||||
field(:properties, type: 'keyword', value: ->(account) { account.searchable_properties })
|
||||
field(:last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) })
|
||||
field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
|
||||
field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||
field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||
field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DatetimeClampingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
||||
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
||||
|
||||
class_methods do
|
||||
def clamp_date(datetime)
|
||||
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,69 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PublicStatusesIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
english_stop: {
|
||||
type: 'stop',
|
||||
stopwords: '_english_',
|
||||
},
|
||||
|
||||
english_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'english',
|
||||
},
|
||||
|
||||
english_possessive_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'possessive_english',
|
||||
},
|
||||
},
|
||||
|
||||
analyzer: {
|
||||
verbatim: {
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(lowercase),
|
||||
},
|
||||
|
||||
content: {
|
||||
tokenizer: 'standard',
|
||||
filter: %w(
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
},
|
||||
|
||||
hashtag: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
index_scope ::Status.unscoped
|
||||
.kept
|
||||
.indexable
|
||||
.includes(:media_attachments, :preloadable_poll, :preview_cards, :tags)
|
||||
|
||||
root date_detection: false do
|
||||
field(:id, type: 'long')
|
||||
field(:account_id, type: 'long')
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||
field(:language, type: 'keyword')
|
||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatusesIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
include FormattingHelper
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
|
||||
filter: {
|
||||
|
@ -9,59 +9,67 @@ class StatusesIndex < Chewy::Index
|
|||
type: 'stop',
|
||||
stopwords: '_english_',
|
||||
},
|
||||
|
||||
english_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'english',
|
||||
},
|
||||
|
||||
english_possessive_stemmer: {
|
||||
type: 'stemmer',
|
||||
language: 'possessive_english',
|
||||
},
|
||||
},
|
||||
|
||||
analyzer: {
|
||||
verbatim: {
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(lowercase),
|
||||
},
|
||||
|
||||
content: {
|
||||
tokenizer: 'standard',
|
||||
tokenizer: 'uax_url_email',
|
||||
filter: %w(
|
||||
english_possessive_stemmer
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
elision
|
||||
english_possessive_stemmer
|
||||
english_stop
|
||||
english_stemmer
|
||||
),
|
||||
},
|
||||
|
||||
hashtag: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preview_cards, :local_mentioned, :local_favorited, :local_reblogged, :local_bookmarked, :tags, preloadable_poll: :local_voters), delete_if: ->(status) { status.searchable_by.empty? }
|
||||
# We do not use delete_if option here because it would call a method that we
|
||||
# expect to be called with crutches without crutches, causing n+1 queries
|
||||
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
||||
|
||||
crutch :mentions do |collection|
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :favourites do |collection|
|
||||
data = ::Favourite.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :reblogs do |collection|
|
||||
data = ::Status.where(reblog_of_id: collection.map(&:id)).where(account: Account.local).pluck(:reblog_of_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :bookmarks do |collection|
|
||||
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
crutch :votes do |collection|
|
||||
data = ::PollVote.joins(:poll).where(poll: { status_id: collection.map(&:id) }).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
root date_detection: false do
|
||||
field(:id, type: 'long')
|
||||
field(:account_id, type: 'long')
|
||||
field(:text, type: 'text', analyzer: 'verbatim', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
|
||||
field(:tags, type: 'text', analyzer: 'hashtag', value: ->(status) { status.tags.map(&:display_name) })
|
||||
field(:searchable_by, type: 'long', value: ->(status) { status.searchable_by })
|
||||
field(:language, type: 'keyword')
|
||||
field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
|
||||
field(:created_at, type: 'date', value: ->(status) { clamp_date(status.created_at) })
|
||||
field :id, type: 'long'
|
||||
field :account_id, type: 'long'
|
||||
|
||||
field :text, type: 'text', value: ->(status) { status.searchable_text } do
|
||||
field :stemmed, type: 'text', analyzer: 'content'
|
||||
end
|
||||
|
||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,27 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TagsIndex < Chewy::Index
|
||||
include DatetimeClampingConcern
|
||||
|
||||
settings index: index_preset(refresh_interval: '30s'), analysis: {
|
||||
analyzer: {
|
||||
content: {
|
||||
tokenizer: 'keyword',
|
||||
filter: %w(
|
||||
word_delimiter_graph
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
filter: %w(lowercase asciifolding cjk_width),
|
||||
},
|
||||
|
||||
edge_ngram: {
|
||||
tokenizer: 'edge_ngram',
|
||||
filter: %w(
|
||||
lowercase
|
||||
asciifolding
|
||||
cjk_width
|
||||
),
|
||||
filter: %w(lowercase asciifolding cjk_width),
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -41,9 +30,12 @@ class TagsIndex < Chewy::Index
|
|||
end
|
||||
|
||||
root date_detection: false do
|
||||
field(:name, type: 'text', analyzer: 'content', value: :display_name) { field(:edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content') }
|
||||
field(:reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? })
|
||||
field(:usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts })
|
||||
field(:last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) })
|
||||
field :name, type: 'text', analyzer: 'content' do
|
||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||
end
|
||||
|
||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Admin
|
|||
account_action.save!
|
||||
|
||||
if account_action.with_report?
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
|
||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id])
|
||||
else
|
||||
redirect_to admin_account_path(@account.id)
|
||||
end
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Admin
|
||||
class SoftwareUpdatesController < BaseController
|
||||
before_action :check_enabled!
|
||||
|
||||
def index
|
||||
authorize :software_update, :index?
|
||||
@software_updates = SoftwareUpdate.all.sort_by(&:gem_version)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_enabled!
|
||||
not_found unless SoftwareUpdate.check_enabled?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,7 +30,6 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
|
|||
:bot,
|
||||
:discoverable,
|
||||
:hide_collections,
|
||||
:indexable,
|
||||
fields_attributes: [:name, :value]
|
||||
)
|
||||
end
|
||||
|
|
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,6 +25,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,10 +5,11 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select(:id, :domain).to_a
|
||||
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||
# .where doesn't guarantee that our results are in the same order
|
||||
# we requested them, so return the "right" order to the requestor.
|
||||
render json: @accounts.index_by(&:id).values_at(*account_ids).compact, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -86,7 +86,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def account_params
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Admin::TagsController < Api::BaseController
|
||||
include Authorization
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
||||
|
||||
before_action :set_tags, only: :index
|
||||
before_action :set_tag, except: :index
|
||||
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
after_action :verify_authorized
|
||||
|
||||
LIMIT = 100
|
||||
PAGINATION_PARAMS = %i(limit).freeze
|
||||
|
||||
def index
|
||||
authorize :tag, :index?
|
||||
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @tag, :show?
|
||||
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @tag, :update?
|
||||
@tag.update!(tag_params.merge(reviewed_at: Time.now.utc))
|
||||
render json: @tag, serializer: REST::Admin::TagSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_tag
|
||||
@tag = Tag.find(params[:id])
|
||||
end
|
||||
|
||||
def set_tags
|
||||
@tags = Tag.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def tag_params
|
||||
params.permit(:display_name, :trendable, :usable, :listable)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@tags.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@tags.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@tags.size == limit_param(LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
|
||||
end
|
||||
end
|
|
@ -16,10 +16,8 @@ class Api::V1::DirectoriesController < Api::BaseController
|
|||
end
|
||||
|
||||
def set_accounts
|
||||
with_read_replica do
|
||||
@accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
|
||||
end
|
||||
end
|
||||
|
||||
def accounts_scope
|
||||
Account.discoverable.tap do |scope|
|
||||
|
|
|
@ -25,11 +25,11 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||
private
|
||||
|
||||
def account
|
||||
@account ||= Account.find(params[:id])
|
||||
Account.find(params[:id])
|
||||
end
|
||||
|
||||
def relationships(**options)
|
||||
AccountRelationshipsPresenter.new([account], current_user.account_id, **options)
|
||||
AccountRelationshipsPresenter.new([params[:id]], current_user.account_id, **options)
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
|
|
|
@ -41,7 +41,5 @@ class Api::V1::Peers::SearchController < Api::BaseController
|
|||
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
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
@domains = []
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,15 +8,7 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
|
|||
before_action :set_translation
|
||||
|
||||
rescue_from TranslationService::NotConfiguredError, with: :not_found
|
||||
rescue_from TranslationService::UnexpectedResponseError, with: :service_unavailable
|
||||
|
||||
rescue_from TranslationService::QuotaExceededError do
|
||||
render json: { error: I18n.t('translation.errors.quota_exceeded') }, status: 503
|
||||
end
|
||||
|
||||
rescue_from TranslationService::TooManyRequestsError do
|
||||
render json: { error: I18n.t('translation.errors.too_many_requests') }, status: 503
|
||||
end
|
||||
rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable
|
||||
|
||||
def create
|
||||
render json: @translation, serializer: REST::TranslationSerializer
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class Api::V1::StreamingController < Api::BaseController
|
||||
def index
|
||||
if same_host?
|
||||
if Rails.configuration.x.streaming_api_base_url == request.host
|
||||
not_found
|
||||
else
|
||||
redirect_to streaming_api_url, status: 301, allow_other_host: true
|
||||
|
@ -11,16 +11,9 @@ class Api::V1::StreamingController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def same_host?
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
request.host == base_url.host && request.port == (base_url.port || 80)
|
||||
end
|
||||
|
||||
def streaming_api_url
|
||||
Addressable::URI.parse(request.url).tap do |uri|
|
||||
base_url = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url)
|
||||
uri.host = base_url.host
|
||||
uri.port = base_url.port
|
||||
uri.host = Addressable::URI.parse(Rails.configuration.x.streaming_api_base_url).host
|
||||
end.to_s
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::TagController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :show, if: :require_auth?
|
||||
before_action :load_tag
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
|
@ -13,10 +12,6 @@ class Api::V1::Timelines::TagController < Api::BaseController
|
|||
|
||||
private
|
||||
|
||||
def require_auth?
|
||||
!Setting.timeline_preview
|
||||
end
|
||||
|
||||
def load_tag
|
||||
@tag = Tag.find_normalized(params[:id])
|
||||
end
|
||||
|
|
|
@ -11,7 +11,6 @@ class ApplicationController < ActionController::Base
|
|||
include CacheConcern
|
||||
include DomainControlHelper
|
||||
include DatabaseHelper
|
||||
include AuthorizedFetchHelper
|
||||
|
||||
helper_method :current_account
|
||||
helper_method :current_session
|
||||
|
@ -52,6 +51,10 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def authorized_fetch_mode?
|
||||
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
|
||||
def public_fetch_mode?
|
||||
!authorized_fetch_mode?
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
def self.provides_callback_for(provider)
|
||||
define_method provider do
|
||||
@provider = provider
|
||||
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
|
||||
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
|
||||
|
||||
if @user.persisted?
|
||||
record_login_activity
|
||||
|
@ -16,9 +16,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|||
session["devise.#{provider}_data"] = request.env['omniauth.auth']
|
||||
redirect_to new_user_registration_url
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
flash[:alert] = I18n.t('devise.failure.omniauth_user_creation_failure') if is_navigational_format?
|
||||
redirect_to new_user_session_url
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::SessionsController < Devise::SessionsController
|
||||
include Redisable
|
||||
|
||||
MAX_2FA_ATTEMPTS_PER_HOUR = 10
|
||||
|
||||
layout 'auth'
|
||||
|
||||
skip_before_action :require_no_authentication, only: [:create]
|
||||
|
@ -138,23 +134,9 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
session.delete(:attempt_user_updated_at)
|
||||
end
|
||||
|
||||
def clear_2fa_attempt_from_user(user)
|
||||
redis.del(second_factor_attempts_key(user))
|
||||
end
|
||||
|
||||
def check_second_factor_rate_limits(user)
|
||||
attempts, = redis.multi do |multi|
|
||||
multi.incr(second_factor_attempts_key(user))
|
||||
multi.expire(second_factor_attempts_key(user), 1.hour)
|
||||
end
|
||||
|
||||
attempts >= MAX_2FA_ATTEMPTS_PER_HOUR
|
||||
end
|
||||
|
||||
def on_authentication_success(user, security_measure)
|
||||
@on_authentication_success_called = true
|
||||
|
||||
clear_2fa_attempt_from_user(user)
|
||||
clear_attempt_from_session
|
||||
|
||||
user.update_sign_in!(new_sign_in: true)
|
||||
|
@ -186,8 +168,4 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
user_agent: request.user_agent
|
||||
)
|
||||
end
|
||||
|
||||
def second_factor_attempts_key(user)
|
||||
"2fa_auth_attempts:#{user.id}:#{Time.now.utc.hour}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -91,23 +91,14 @@ module SignatureVerification
|
|||
raise SignatureVerificationError, "Public key not found for key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
compare_signed_string = build_signed_string
|
||||
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
actor = stoplight_wrap_request { actor_refresh_key!(actor) }
|
||||
|
||||
raise SignatureVerificationError, "Could not refresh public key #{signature_params['keyId']}" if actor.nil?
|
||||
|
||||
compare_signed_string = build_signed_string(include_query_string: true)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
# Compatibility quirk with older Mastodon versions
|
||||
compare_signed_string = build_signed_string(include_query_string: false)
|
||||
return actor unless verify_signature(actor, signature, compare_signed_string).nil?
|
||||
|
||||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||
|
@ -128,8 +119,6 @@ module SignatureVerification
|
|||
private
|
||||
|
||||
def fail_with!(message, **options)
|
||||
Rails.logger.debug { "Signature verification failed: #{message}" }
|
||||
|
||||
@signature_verification_failure_reason = { error: message }.merge(options)
|
||||
@signed_request_actor = nil
|
||||
end
|
||||
|
@ -189,18 +178,11 @@ module SignatureVerification
|
|||
nil
|
||||
end
|
||||
|
||||
def build_signed_string(include_query_string: true)
|
||||
def build_signed_string
|
||||
signed_headers.map do |signed_header|
|
||||
case signed_header
|
||||
when Request::REQUEST_TARGET
|
||||
if include_query_string
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||
else
|
||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||
# TODO: remove eventually some time after release of the fixed version
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
end
|
||||
when '(created)'
|
||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||
raise SignatureVerificationError, 'Pseudo-header (created) used but corresponding argument missing' if signature_params['created'].blank?
|
||||
|
@ -266,7 +248,7 @@ module SignatureVerification
|
|||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.delete_prefix('acct:'), suppress_errors: false) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_actor(key_id)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
|
||||
account
|
||||
end
|
||||
rescue Mastodon::PrivateNetworkAddressError => e
|
||||
|
|
|
@ -65,11 +65,6 @@ module TwoFactorAuthenticationConcern
|
|||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if check_second_factor_rate_limits(user)
|
||||
flash.now[:alert] = I18n.t('users.rate_limited')
|
||||
return prompt_for_two_factor(user)
|
||||
end
|
||||
|
||||
if valid_otp_attempt?(user)
|
||||
on_authentication_success(user, :otp)
|
||||
else
|
||||
|
|
|
@ -4,14 +4,14 @@ module WebAppControllerConcern
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
prepend_before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :set_app_body_class
|
||||
|
||||
vary_by 'Accept, Accept-Language, Cookie'
|
||||
end
|
||||
|
||||
def skip_csrf_meta_tags?
|
||||
!(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||
!(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil?
|
||||
end
|
||||
|
||||
def set_app_body_class
|
||||
|
@ -22,9 +22,7 @@ module WebAppControllerConcern
|
|||
return if user_signed_in? && current_account.moved_to_account_id.nil?
|
||||
|
||||
redirect_path = PermalinkRedirector.new(request.path).redirect_path
|
||||
return if redirect_path.blank?
|
||||
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
|
||||
redirect_to(redirect_path)
|
||||
redirect_to(redirect_path) if redirect_path.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class FollowerAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class FollowingAccountsController < ApplicationController
|
||||
include AccountControllerConcern
|
||||
include SignatureVerification
|
||||
include WebAppControllerConcern
|
||||
|
||||
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ class RelationshipsController < ApplicationController
|
|||
end
|
||||
|
||||
def set_relationships
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts, current_user.account_id)
|
||||
@relationships = AccountRelationshipsPresenter.new(@accounts.pluck(:id), current_user.account_id)
|
||||
end
|
||||
|
||||
def form_account_batch_params
|
||||
|
|
|
@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
|
|||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
|
||||
params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AuthorizedFetchHelper
|
||||
def authorized_fetch_mode?
|
||||
ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch && 'true' } == 'true' || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
|
||||
def authorized_fetch_overridden?
|
||||
ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode
|
||||
end
|
||||
end
|
|
@ -21,7 +21,6 @@ module ContextHelper
|
|||
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
|
||||
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
|
||||
indexable: { 'toot' => 'http://joinmastodon.org/ns#', 'indexable' => 'toot:indexable' },
|
||||
memorial: { 'toot' => 'http://joinmastodon.org/ns#', 'memorial' => 'toot:memorial' },
|
||||
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',
|
||||
|
|
|
@ -1,24 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DatabaseHelper
|
||||
def replica_enabled?
|
||||
ENV['REPLICA_DB_NAME'] || ENV.fetch('REPLICA_DATABASE_URL', nil)
|
||||
end
|
||||
module_function :replica_enabled?
|
||||
|
||||
def with_read_replica(&block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :reading, prevent_writes: true, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def with_primary(&block)
|
||||
if replica_enabled?
|
||||
ApplicationRecord.connected_to(role: :writing, &block)
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -155,8 +155,8 @@ module JsonLdHelper
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
|
||||
unless id_is_known
|
||||
def fetch_resource(uri, id, on_behalf_of = nil)
|
||||
unless id
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
|
||||
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
|
||||
|
@ -164,29 +164,17 @@ module JsonLdHelper
|
|||
uri = json['id']
|
||||
end
|
||||
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
|
||||
json = fetch_resource_without_id_validation(uri, on_behalf_of)
|
||||
json.present? && json['id'] == uri ? json : nil
|
||||
end
|
||||
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
|
||||
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false)
|
||||
on_behalf_of ||= Account.representative
|
||||
|
||||
build_request(uri, on_behalf_of, options: request_options).perform do |response|
|
||||
build_request(uri, on_behalf_of).perform do |response|
|
||||
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
|
||||
|
||||
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
|
||||
end
|
||||
end
|
||||
|
||||
def valid_activitypub_content_type?(response)
|
||||
return true if response.mime_type == 'application/activity+json'
|
||||
|
||||
# When the mime type is `application/ld+json`, we need to check the profile,
|
||||
# but `http.rb` does not parse it for us.
|
||||
return false unless response.mime_type == 'application/ld+json'
|
||||
|
||||
response.headers[HTTP::Headers::CONTENT_TYPE]&.split(';')&.map(&:strip)&.any? do |str|
|
||||
str.start_with?('profile="') && str[9...-1].split.include?('https://www.w3.org/ns/activitystreams')
|
||||
body_to_json(response.body_with_limit) if response.code == 200
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -216,8 +204,8 @@ module JsonLdHelper
|
|||
response.code == 501 || ((400...500).cover?(response.code) && ![401, 408, 429].include?(response.code))
|
||||
end
|
||||
|
||||
def build_request(uri, on_behalf_of = nil, options: {})
|
||||
Request.new(:get, uri, **options).tap do |request|
|
||||
def build_request(uri, on_behalf_of = nil)
|
||||
Request.new(:get, uri).tap do |request|
|
||||
request.on_behalf_of(on_behalf_of) if on_behalf_of
|
||||
request.add_headers('Accept' => 'application/activity+json, application/ld+json')
|
||||
end
|
||||
|
|
|
@ -188,11 +188,11 @@ module LanguagesHelper
|
|||
|
||||
ISO_639_3 = {
|
||||
ast: ['Asturian', 'Asturianu'].freeze,
|
||||
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
|
||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||
kmr: ['Kurmanji (Kurdish)', 'Kurmancî'].freeze,
|
||||
ldn: ['Láadan', 'Láadan'].freeze,
|
||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||
sco: ['Scots', 'Scots'].freeze,
|
||||
|
@ -200,7 +200,6 @@ module LanguagesHelper
|
|||
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
|
||||
szl: ['Silesian', 'ślůnsko godka'].freeze,
|
||||
tok: ['Toki Pona', 'toki pona'].freeze,
|
||||
xal: ['Kalmyk', 'Хальмг келн'].freeze,
|
||||
zba: ['Balaibalan', 'باليبلن'].freeze,
|
||||
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||
}.freeze
|
||||
|
@ -254,7 +253,6 @@ module LanguagesHelper
|
|||
|
||||
def valid_locale_or_nil(str)
|
||||
return if str.blank?
|
||||
return str if valid_locale?(str)
|
||||
|
||||
code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ module MediaComponentHelper
|
|||
blurhash: video.blurhash,
|
||||
frameRate: meta.dig('original', 'frame_rate'),
|
||||
inline: true,
|
||||
aspectRatio: "#{meta.dig('original', 'width')} / #{meta.dig('original', 'height')}",
|
||||
media: [
|
||||
serialize_media_attachment(video),
|
||||
].as_json,
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import api from '../api';
|
||||
|
||||
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
||||
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
|
||||
|
||||
export function submitAccountNote(id, value) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(submitAccountNoteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
}).then(response => {
|
||||
dispatch(submitAccountNoteSuccess(response.data));
|
||||
}).catch(error => dispatch(submitAccountNoteFail(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteRequest() {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
}
|
||||
|
||||
export function submitAccountNoteFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_FAIL,
|
||||
error,
|
||||
};
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
export const submitAccountNote = createAppAsyncThunk(
|
||||
'account_note/submit',
|
||||
async (args: { id: string; value: string }, { getState }) => {
|
||||
// TODO: replace `unknown` with `ApiRelationshipJSON` when it is merged
|
||||
const response = await api(getState).post<unknown>(
|
||||
`/api/v1/accounts/${args.id}/note`,
|
||||
{
|
||||
comment: args.value,
|
||||
},
|
||||
);
|
||||
|
||||
return { relationship: response.data };
|
||||
},
|
||||
);
|
|
@ -84,7 +84,6 @@ const messages = defineMessages({
|
|||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
open: { id: 'compose.published.open', defaultMessage: 'Open' },
|
||||
published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
|
||||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||
});
|
||||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
|
@ -245,7 +244,7 @@ export function submitCompose(routerHistory) {
|
|||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
message: messages.published,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
import api, { getLinks } from '../api';
|
||||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
export const REBLOG_FAIL = 'REBLOG_FAIL';
|
||||
|
||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||
export const REBLOGS_EXPAND_FAIL = 'REBLOGS_EXPAND_FAIL';
|
||||
|
||||
export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST';
|
||||
export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS';
|
||||
export const FAVOURITE_FAIL = 'FAVOURITE_FAIL';
|
||||
|
@ -31,10 +26,6 @@ export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST';
|
|||
export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS';
|
||||
export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL';
|
||||
|
||||
export const FAVOURITES_EXPAND_REQUEST = 'FAVOURITES_EXPAND_REQUEST';
|
||||
export const FAVOURITES_EXPAND_SUCCESS = 'FAVOURITES_EXPAND_SUCCESS';
|
||||
export const FAVOURITES_EXPAND_FAIL = 'FAVOURITES_EXPAND_FAIL';
|
||||
|
||||
export const PIN_REQUEST = 'PIN_REQUEST';
|
||||
export const PIN_SUCCESS = 'PIN_SUCCESS';
|
||||
export const PIN_FAIL = 'PIN_FAIL';
|
||||
|
@ -282,10 +273,8 @@ export function fetchReblogs(id) {
|
|||
dispatch(fetchReblogsRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
dispatch(fetchReblogsSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchReblogsFail(id, error));
|
||||
});
|
||||
|
@ -299,62 +288,17 @@ export function fetchReblogsRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchReblogsSuccess(id, accounts, next) {
|
||||
export function fetchReblogsSuccess(id, accounts) {
|
||||
return {
|
||||
type: REBLOGS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchReblogsFail(id, error) {
|
||||
return {
|
||||
type: REBLOGS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogs(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'reblogged_by', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandReblogsRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandReblogsSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandReblogsFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsRequest(id) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandReblogsFail(id, error) {
|
||||
return {
|
||||
type: REBLOGS_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
@ -364,10 +308,8 @@ export function fetchFavourites(id) {
|
|||
dispatch(fetchFavouritesRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
dispatch(fetchFavouritesSuccess(id, response.data));
|
||||
}).catch(error => {
|
||||
dispatch(fetchFavouritesFail(id, error));
|
||||
});
|
||||
|
@ -381,62 +323,17 @@ export function fetchFavouritesRequest(id) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchFavouritesSuccess(id, accounts, next) {
|
||||
export function fetchFavouritesSuccess(id, accounts) {
|
||||
return {
|
||||
type: FAVOURITES_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFavouritesFail(id, error) {
|
||||
return {
|
||||
type: FAVOURITES_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavourites(id) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['user_lists', 'favourited_by', id, 'next']);
|
||||
if (url === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandFavouritesRequest(id));
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch(expandFavouritesSuccess(id, response.data, next ? next.uri : null));
|
||||
dispatch(fetchRelationships(response.data.map(item => item.id)));
|
||||
}).catch(error => dispatch(expandFavouritesFail(id, error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesRequest(id) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_REQUEST,
|
||||
id,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesSuccess(id, accounts, next) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
};
|
||||
}
|
||||
|
||||
export function expandFavouritesFail(id, error) {
|
||||
return {
|
||||
type: FAVOURITES_EXPAND_FAIL,
|
||||
id,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { register as registerPushNotifications } from './push_notifications';
|
||||
import { saveSettings } from './settings';
|
||||
|
||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||
|
@ -294,10 +293,6 @@ export function requestBrowserPermission(callback = noOp) {
|
|||
requestNotificationPermission((permission) => {
|
||||
dispatch(setBrowserPermission(permission));
|
||||
callback(permission);
|
||||
|
||||
if (permission === 'granted') {
|
||||
dispatch(registerPushNotifications());
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
import { fromJS } from 'immutable';
|
||||
|
||||
import { searchHistory } from 'mastodon/settings';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
|
@ -19,7 +15,8 @@ export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
|
|||
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
|
||||
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
|
||||
|
||||
export const SEARCH_HISTORY_UPDATE = 'SEARCH_HISTORY_UPDATE';
|
||||
export const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
|
||||
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
|
||||
|
||||
export function changeSearch(value) {
|
||||
return {
|
||||
|
@ -40,17 +37,17 @@ export function submitSearch(type) {
|
|||
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||
|
||||
if (value.length === 0) {
|
||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, '', type));
|
||||
dispatch(fetchSearchSuccess({ accounts: [], statuses: [], hashtags: [] }, ''));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchSearchRequest(type));
|
||||
dispatch(fetchSearchRequest());
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
resolve: signedIn,
|
||||
limit: 11,
|
||||
limit: 5,
|
||||
type,
|
||||
},
|
||||
}).then(response => {
|
||||
|
@ -62,7 +59,7 @@ export function submitSearch(type) {
|
|||
dispatch(importFetchedStatuses(response.data.statuses));
|
||||
}
|
||||
|
||||
dispatch(fetchSearchSuccess(response.data, value, type));
|
||||
dispatch(fetchSearchSuccess(response.data, value));
|
||||
dispatch(fetchRelationships(response.data.accounts.map(item => item.id)));
|
||||
}).catch(error => {
|
||||
dispatch(fetchSearchFail(error));
|
||||
|
@ -70,18 +67,16 @@ export function submitSearch(type) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchSearchRequest(searchType) {
|
||||
export function fetchSearchRequest() {
|
||||
return {
|
||||
type: SEARCH_FETCH_REQUEST,
|
||||
searchType,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSearchSuccess(results, searchTerm, searchType) {
|
||||
export function fetchSearchSuccess(results, searchTerm) {
|
||||
return {
|
||||
type: SEARCH_FETCH_SUCCESS,
|
||||
results,
|
||||
searchType,
|
||||
searchTerm,
|
||||
};
|
||||
}
|
||||
|
@ -95,16 +90,15 @@ export function fetchSearchFail(error) {
|
|||
|
||||
export const expandSearch = type => (dispatch, getState) => {
|
||||
const value = getState().getIn(['search', 'value']);
|
||||
const offset = getState().getIn(['search', 'results', type]).size - 1;
|
||||
const offset = getState().getIn(['search', 'results', type]).size;
|
||||
|
||||
dispatch(expandSearchRequest(type));
|
||||
dispatch(expandSearchRequest());
|
||||
|
||||
api(getState).get('/api/v2/search', {
|
||||
params: {
|
||||
q: value,
|
||||
type,
|
||||
offset,
|
||||
limit: 11,
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
if (data.accounts) {
|
||||
|
@ -122,9 +116,8 @@ export const expandSearch = type => (dispatch, getState) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const expandSearchRequest = (searchType) => ({
|
||||
export const expandSearchRequest = () => ({
|
||||
type: SEARCH_EXPAND_REQUEST,
|
||||
searchType,
|
||||
});
|
||||
|
||||
export const expandSearchSuccess = (results, searchTerm, searchType) => ({
|
||||
|
@ -147,10 +140,6 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
const signedIn = !!getState().getIn(['meta', 'me']);
|
||||
|
||||
if (!signedIn) {
|
||||
if (onFailure) {
|
||||
onFailure();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -177,34 +166,16 @@ export const openURL = (value, history, onFailure) => (dispatch, getState) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const clickSearchResult = (q, type) => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.add(fromJS({ type, q })).takeLast(4);
|
||||
export const clickSearchResult = (q, type) => ({
|
||||
type: SEARCH_RESULT_CLICK,
|
||||
|
||||
searchHistory.set(me, current.toJS());
|
||||
dispatch(updateSearchHistory(current));
|
||||
};
|
||||
|
||||
export const forgetSearchResult = q => (dispatch, getState) => {
|
||||
const previous = getState().getIn(['search', 'recent']);
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const current = previous.filterNot(result => result.get('q') === q);
|
||||
|
||||
searchHistory.set(me, current.toJS());
|
||||
dispatch(updateSearchHistory(current));
|
||||
};
|
||||
|
||||
export const updateSearchHistory = recent => ({
|
||||
type: SEARCH_HISTORY_UPDATE,
|
||||
recent,
|
||||
result: {
|
||||
type,
|
||||
q,
|
||||
},
|
||||
});
|
||||
|
||||
export const hydrateSearch = () => (dispatch, getState) => {
|
||||
const me = getState().getIn(['meta', 'me']);
|
||||
const history = searchHistory.get(me);
|
||||
|
||||
if (history !== null) {
|
||||
dispatch(updateSearchHistory(history));
|
||||
}
|
||||
};
|
||||
export const forgetSearchResult = q => ({
|
||||
type: SEARCH_RESULT_FORGET,
|
||||
q,
|
||||
});
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Iterable, fromJS } from 'immutable';
|
|||
|
||||
import { hydrateCompose } from './compose';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
import { hydrateSearch } from './search';
|
||||
|
||||
export const STORE_HYDRATE = 'STORE_HYDRATE';
|
||||
export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY';
|
||||
|
@ -21,7 +20,6 @@ export function hydrateStore(rawState) {
|
|||
});
|
||||
|
||||
dispatch(hydrateCompose());
|
||||
dispatch(hydrateSearch());
|
||||
dispatch(importFetchedAccounts(Object.values(rawState.accounts)));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// @ts-check
|
||||
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
|
||||
/**
|
||||
* @param {import('axios').AxiosResponse} response
|
||||
* @returns {LinkHeader}
|
||||
*/
|
||||
export const getLinks = response => {
|
||||
const value = response.headers.link;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
/** @type {import('axios').RawAxiosRequestHeaders} */
|
||||
const csrfHeader = {};
|
||||
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
const setCSRFHeader = () => {
|
||||
/** @type {HTMLMetaElement | null} */
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]');
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
ready(setCSRFHeader);
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').RawAxiosRequestHeaders}
|
||||
*/
|
||||
const authorizationHeaderFromState = getState => {
|
||||
const accessToken = getState && getState().getIn(['meta', 'access_token'], '');
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {() => import('immutable').Map<string,any>} getState
|
||||
* @returns {import('axios').AxiosInstance}
|
||||
*/
|
||||
export default function api(getState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import type { AxiosResponse, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import ready from './ready';
|
||||
import type { GetState } from './store';
|
||||
|
||||
export const getLinks = (response: AxiosResponse) => {
|
||||
const value = response.headers.link as string | undefined;
|
||||
|
||||
if (!value) {
|
||||
return new LinkHeader();
|
||||
}
|
||||
|
||||
return LinkHeader.parse(value);
|
||||
};
|
||||
|
||||
const csrfHeader: RawAxiosRequestHeaders = {};
|
||||
|
||||
const setCSRFHeader = () => {
|
||||
const csrfToken = document.querySelector<HTMLMetaElement>(
|
||||
'meta[name=csrf-token]',
|
||||
);
|
||||
|
||||
if (csrfToken) {
|
||||
csrfHeader['X-CSRF-Token'] = csrfToken.content;
|
||||
}
|
||||
};
|
||||
|
||||
void ready(setCSRFHeader);
|
||||
|
||||
const authorizationHeaderFromState = (getState?: GetState) => {
|
||||
const accessToken =
|
||||
getState && (getState().meta.get('access_token', '') as string);
|
||||
|
||||
if (!accessToken) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
} as RawAxiosRequestHeaders;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(getState: GetState) {
|
||||
return axios.create({
|
||||
headers: {
|
||||
...csrfHeader,
|
||||
...authorizationHeaderFromState(getState),
|
||||
},
|
||||
|
||||
transformResponse: [
|
||||
function (data: unknown) {
|
||||
try {
|
||||
return JSON.parse(data as string) as unknown;
|
||||
} catch {
|
||||
return data;
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
|
@ -45,21 +45,6 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('does not truncate the contents when the last child is a text node', () => {
|
||||
const status = createStatus(
|
||||
'this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text',
|
||||
['test'],
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual([]);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"this is a #<a class="zrl" href="https://example.com/search?tag=test">test</a>. Some more text"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('extract tags from the last line', () => {
|
||||
const status = createStatus(
|
||||
'<p>Simple text</p><p><a href="test">#hashtag</a></p>',
|
||||
|
@ -120,21 +105,6 @@ describe('computeHashtagBarForStatus', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('handles server-side normalized tags with accentuated characters', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text</p><p><a href="test">#éaa</a> <a href="test">#Éaa</a></p>',
|
||||
['eaa'], // The server may normalize the hashtags in the `tags` attribute
|
||||
);
|
||||
|
||||
const { hashtagsInBar, statusContentProps } =
|
||||
computeHashtagBarForStatus(status);
|
||||
|
||||
expect(hashtagsInBar).toEqual(['Éaa']);
|
||||
expect(statusContentProps.statusContent).toMatchInlineSnapshot(
|
||||
`"<p>Text</p>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display in bar a hashtag in content with a case difference', () => {
|
||||
const status = createStatus(
|
||||
'<p>Text <a href="test">#Éaa</a></p><p><a href="test">#éaa</a></p>',
|
||||
|
|
|
@ -9,12 +9,11 @@ import api from 'mastodon/api';
|
|||
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||
|
||||
const dateForCohort = cohort => {
|
||||
const timeZone = 'UTC';
|
||||
switch(cohort.frequency) {
|
||||
case 'day':
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||
default:
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' timeZone={timeZone} />;
|
||||
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -6,10 +6,21 @@ import { reduceMotion } from '../initial_state';
|
|||
|
||||
import { ShortNumber } from './short_number';
|
||||
|
||||
const obfuscatedCount = (count: number) => {
|
||||
if (count < 0) {
|
||||
return 0;
|
||||
} else if (count <= 1) {
|
||||
return count;
|
||||
} else {
|
||||
return '1+';
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
value: number;
|
||||
obfuscate?: boolean;
|
||||
}
|
||||
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
||||
export const AnimatedNumber: React.FC<Props> = ({ value, obfuscate }) => {
|
||||
const [previousValue, setPreviousValue] = useState(value);
|
||||
const [direction, setDirection] = useState<1 | -1>(1);
|
||||
|
||||
|
@ -25,7 +36,11 @@ export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
|||
);
|
||||
|
||||
if (reduceMotion) {
|
||||
return <ShortNumber value={value} />;
|
||||
return obfuscate ? (
|
||||
<>{obfuscatedCount(value)}</>
|
||||
) : (
|
||||
<ShortNumber value={value} />
|
||||
);
|
||||
}
|
||||
|
||||
const styles = [
|
||||
|
@ -52,7 +67,11 @@ export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
|||
transform: `translateY(${style.y * 100}%)`,
|
||||
}}
|
||||
>
|
||||
{obfuscate ? (
|
||||
obfuscatedCount(data as number)
|
||||
) : (
|
||||
<ShortNumber value={data as number} />
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
|
|
|
@ -16,13 +16,7 @@ export default class Column extends PureComponent {
|
|||
};
|
||||
|
||||
scrollTop () {
|
||||
let scrollable = null;
|
||||
|
||||
if (this.props.bindToDocument) {
|
||||
scrollable = document.scrollingElement;
|
||||
} else {
|
||||
scrollable = this.node.querySelector('.scrollable');
|
||||
}
|
||||
const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
|
||||
|
||||
if (!scrollable) {
|
||||
return;
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call,
|
||||
@typescript-eslint/no-unsafe-return,
|
||||
@typescript-eslint/no-unsafe-assignment,
|
||||
@typescript-eslint/no-unsafe-member-access
|
||||
-- the settings store is not yet typed */
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeSetting } from 'mastodon/actions/settings';
|
||||
import { bannerSettings } from 'mastodon/settings';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { IconButton } from './icon_button';
|
||||
|
||||
|
@ -26,25 +19,13 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
id,
|
||||
children,
|
||||
}) => {
|
||||
const dismissed = useAppSelector((state) =>
|
||||
state.settings.getIn(['dismissed_banners', id], false),
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [visible, setVisible] = useState(!bannerSettings.get(id) && !dismissed);
|
||||
const [visible, setVisible] = useState(!bannerSettings.get(id));
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setVisible(false);
|
||||
bannerSettings.set(id, true);
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !dismissed) {
|
||||
dispatch(changeSetting(['dismissed_banners', id], true));
|
||||
}
|
||||
}, [id, dispatch, visible, dismissed]);
|
||||
}, [id]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
|
@ -52,6 +33,8 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
|
||||
return (
|
||||
<div className='dismissable-banner'>
|
||||
<div className='dismissable-banner__message'>{children}</div>
|
||||
|
||||
<div className='dismissable-banner__action'>
|
||||
<IconButton
|
||||
icon='times'
|
||||
|
@ -59,8 +42,6 @@ export const DismissableBanner: React.FC<PropsWithChildren<Props>> = ({
|
|||
onClick={handleDismiss}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='dismissable-banner__message'>{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,8 +10,8 @@ import { groupBy, minBy } from 'lodash';
|
|||
|
||||
import { getStatusContent } from './status_content';
|
||||
|
||||
// Fit on a single line on desktop
|
||||
const VISIBLE_HASHTAGS = 3;
|
||||
// About two lines on desktop
|
||||
const VISIBLE_HASHTAGS = 7;
|
||||
|
||||
// Those types are not correct, they need to be replaced once this part of the state is typed
|
||||
export type TagLike = Record<{ name: string }>;
|
||||
|
@ -23,9 +23,8 @@ export type StatusLike = Record<{
|
|||
}>;
|
||||
|
||||
function normalizeHashtag(hashtag: string) {
|
||||
return (
|
||||
hashtag && hashtag.startsWith('#') ? hashtag.slice(1) : hashtag
|
||||
).normalize('NFKC');
|
||||
if (hashtag && hashtag.startsWith('#')) return hashtag.slice(1);
|
||||
else return hashtag;
|
||||
}
|
||||
|
||||
function isNodeLinkHashtag(element: Node): element is HTMLLinkElement {
|
||||
|
@ -71,16 +70,9 @@ function uniqueHashtagsWithCaseHandling(hashtags: string[]) {
|
|||
}
|
||||
|
||||
// Create the collator once, this is much more efficient
|
||||
const collator = new Intl.Collator(undefined, {
|
||||
sensitivity: 'base', // we use this to emulate the ASCII folding done on the server-side, hopefuly more efficiently
|
||||
});
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'accent' });
|
||||
function localeAwareInclude(collection: string[], value: string) {
|
||||
const normalizedValue = value.normalize('NFKC');
|
||||
|
||||
return !!collection.find(
|
||||
(item) => collator.compare(item.normalize('NFKC'), normalizedValue) === 0,
|
||||
);
|
||||
return collection.find((item) => collator.compare(item, value) === 0);
|
||||
}
|
||||
|
||||
// We use an intermediate function here to make it easier to test
|
||||
|
@ -109,7 +101,7 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
|
||||
const lastChild = template.content.lastChild;
|
||||
|
||||
if (!lastChild || lastChild.nodeType === Node.TEXT_NODE) return defaultResult;
|
||||
if (!lastChild) return defaultResult;
|
||||
|
||||
template.content.removeChild(lastChild);
|
||||
const contentWithoutLastLine = template;
|
||||
|
@ -129,13 +121,11 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
// try to see if the last line is only hashtags
|
||||
let onlyHashtags = true;
|
||||
|
||||
const normalizedTagNames = tagNames.map((tag) => tag.normalize('NFKC'));
|
||||
|
||||
Array.from(lastChild.childNodes).forEach((node) => {
|
||||
if (isNodeLinkHashtag(node) && node.textContent) {
|
||||
const normalized = normalizeHashtag(node.textContent);
|
||||
|
||||
if (!localeAwareInclude(normalizedTagNames, normalized)) {
|
||||
if (!localeAwareInclude(tagNames, normalized)) {
|
||||
// stop here, this is not a real hashtag, so consider it as text
|
||||
onlyHashtags = false;
|
||||
return;
|
||||
|
@ -150,14 +140,12 @@ export function computeHashtagBarForStatus(status: StatusLike): {
|
|||
}
|
||||
});
|
||||
|
||||
const hashtagsInBar = tagNames.filter((tag) => {
|
||||
const normalizedTag = tag.normalize('NFKC');
|
||||
const hashtagsInBar = tagNames.filter(
|
||||
(tag) =>
|
||||
// the tag does not appear at all in the status content, it is an out-of-band tag
|
||||
return (
|
||||
!localeAwareInclude(contentHashtags, normalizedTag) &&
|
||||
!localeAwareInclude(lastLineHashtags, normalizedTag)
|
||||
!localeAwareInclude(contentHashtags, tag) &&
|
||||
!localeAwareInclude(lastLineHashtags, tag),
|
||||
);
|
||||
});
|
||||
|
||||
const isOnlyOneLine = contentWithoutLastLine.content.childElementCount === 0;
|
||||
const hasMedia = status.get('media_attachments').size > 0;
|
||||
|
@ -210,13 +198,13 @@ const HashtagBar: React.FC<{
|
|||
|
||||
const revealedHashtags = expanded
|
||||
? hashtags
|
||||
: hashtags.slice(0, VISIBLE_HASHTAGS);
|
||||
: hashtags.slice(0, VISIBLE_HASHTAGS - 1);
|
||||
|
||||
return (
|
||||
<div className='hashtag-bar'>
|
||||
{revealedHashtags.map((hashtag) => (
|
||||
<Link key={hashtag} to={`/tags/${hashtag}`}>
|
||||
#<span>{hashtag}</span>
|
||||
#{hashtag}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ interface Props {
|
|||
overlay: boolean;
|
||||
tabIndex: number;
|
||||
counter?: number;
|
||||
obfuscateCount?: boolean;
|
||||
href?: string;
|
||||
ariaHidden: boolean;
|
||||
}
|
||||
|
@ -104,6 +105,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
tabIndex,
|
||||
title,
|
||||
counter,
|
||||
obfuscateCount,
|
||||
href,
|
||||
ariaHidden,
|
||||
} = this.props;
|
||||
|
@ -129,7 +131,7 @@ export class IconButton extends PureComponent<Props, States> {
|
|||
<Icon id={icon} fixedWidth aria-hidden='true' />{' '}
|
||||
{typeof counter !== 'undefined' && (
|
||||
<span className='icon-button__counter'>
|
||||
<AnimatedNumber value={counter} />
|
||||
<AnimatedNumber value={counter} obfuscate={obfuscateCount} />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -73,7 +73,7 @@ class ScrollableList extends PureComponent {
|
|||
const clientHeight = this.getClientHeight();
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (scrollTop > 0 && offset < 400 && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
if (400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
|
||||
this.props.onLoadMore();
|
||||
}
|
||||
|
||||
|
|
|
@ -546,11 +546,10 @@ class Status extends ImmutablePureComponent {
|
|||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||
|
||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef} data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
{prepend}
|
||||
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||
|
@ -575,7 +574,7 @@ class Status extends ImmutablePureComponent {
|
|||
<StatusContent
|
||||
status={status}
|
||||
onClick={this.handleClick}
|
||||
expanded={expanded}
|
||||
expanded={!status.get('hidden')}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
onTranslate={this.handleTranslate}
|
||||
collapsible
|
||||
|
@ -585,7 +584,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
{media}
|
||||
|
||||
{expanded && hashtagBar}
|
||||
{hashtagBar}
|
||||
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} onFilter={matchedFilters ? this.handleFilterClick : null} {...other} />
|
||||
</div>
|
||||
|
|
|
@ -362,7 +362,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className='status__action-bar__button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className={classNames('status__action-bar__button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||
<IconButton className='status__action-bar__button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||
|
|
|
@ -6,7 +6,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
@ -162,7 +161,7 @@ class About extends PureComponent {
|
|||
</Section>
|
||||
|
||||
<Section title={intl.formatMessage(messages.rules)}>
|
||||
{!isLoading && (server.get('rules', ImmutableList()).isEmpty() ? (
|
||||
{!isLoading && (server.get('rules', []).isEmpty() ? (
|
||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||
) : (
|
||||
<ol className='rules-list'>
|
||||
|
|
|
@ -11,7 +11,7 @@ const mapStateToProps = (state, { account }) => ({
|
|||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote({ id: account.get('id'), value}));
|
||||
dispatch(submitAccountNote(account.get('id'), value));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -205,11 +205,11 @@ class Audio extends PureComponent {
|
|||
};
|
||||
|
||||
toggleMute = () => {
|
||||
const muted = !(this.state.muted || this.state.volume === 0);
|
||||
const muted = !this.state.muted;
|
||||
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => {
|
||||
this.setState({ muted }, () => {
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
|
||||
this.gainNode.gain.value = muted ? 0 : this.state.volume;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -287,7 +287,7 @@ class Audio extends PureComponent {
|
|||
const { x } = getPointerPosition(this.volume, e);
|
||||
|
||||
if(!isNaN(x)) {
|
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => {
|
||||
this.setState({ volume: x }, () => {
|
||||
if (this.gainNode) {
|
||||
this.gainNode.gain.value = this.state.muted ? 0 : x;
|
||||
}
|
||||
|
@ -466,9 +466,8 @@ class Audio extends PureComponent {
|
|||
|
||||
render () {
|
||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||
const { paused, muted, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const muted = this.state.muted || volume === 0;
|
||||
|
||||
let warning;
|
||||
|
||||
|
@ -558,12 +557,12 @@ class Audio extends PureComponent {
|
|||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
|
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}>
|
||||
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||
<div className='video-player__volume__current' style={{ width: `${volume * 100}%`, backgroundColor: this._getAccentColor() }} />
|
||||
|
||||
<span
|
||||
className='video-player__volume__handle'
|
||||
tabIndex={0}
|
||||
style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
||||
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage, FormattedList } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { domain, searchEnabled } from 'mastodon/initial_state';
|
||||
import { searchEnabled } from 'mastodon/initial_state';
|
||||
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -16,17 +16,6 @@ const messages = defineMessages({
|
|||
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
|
||||
});
|
||||
|
||||
const labelForRecentSearch = search => {
|
||||
switch(search.get('type')) {
|
||||
case 'account':
|
||||
return `@${search.get('q')}`;
|
||||
case 'hashtag':
|
||||
return `#${search.get('q')}`;
|
||||
default:
|
||||
return search.get('q');
|
||||
}
|
||||
};
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -56,17 +45,6 @@ class Search extends PureComponent {
|
|||
options: [],
|
||||
};
|
||||
|
||||
defaultOptions = [
|
||||
{ label: <><mark>has:</mark> <FormattedList type='disjunction' value={['media', 'poll', 'embed']} /></>, action: e => { e.preventDefault(); this._insertText('has:') } },
|
||||
{ label: <><mark>is:</mark> <FormattedList type='disjunction' value={['reply', 'sensitive']} /></>, action: e => { e.preventDefault(); this._insertText('is:') } },
|
||||
{ label: <><mark>language:</mark> <FormattedMessage id='search_popout.language_code' defaultMessage='ISO language code' /></>, action: e => { e.preventDefault(); this._insertText('language:') } },
|
||||
{ label: <><mark>from:</mark> <FormattedMessage id='search_popout.user' defaultMessage='user' /></>, action: e => { e.preventDefault(); this._insertText('from:') } },
|
||||
{ label: <><mark>before:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('before:') } },
|
||||
{ label: <><mark>during:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('during:') } },
|
||||
{ label: <><mark>after:</mark> <FormattedMessage id='search_popout.specific_date' defaultMessage='specific date' /></>, action: e => { e.preventDefault(); this._insertText('after:') } },
|
||||
{ label: <><mark>in:</mark> <FormattedList type='disjunction' value={['all', 'library']} /></>, action: e => { e.preventDefault(); this._insertText('in:') } }
|
||||
];
|
||||
|
||||
setRef = c => {
|
||||
this.searchForm = c;
|
||||
};
|
||||
|
@ -92,7 +70,7 @@ class Search extends PureComponent {
|
|||
|
||||
handleKeyDown = (e) => {
|
||||
const { selectedOption } = this.state;
|
||||
const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions();
|
||||
const options = this._getOptions();
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
|
@ -122,9 +100,11 @@ class Search extends PureComponent {
|
|||
if (selectedOption === -1) {
|
||||
this._submit();
|
||||
} else if (options.length > 0) {
|
||||
options[selectedOption].action(e);
|
||||
options[selectedOption].action();
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
|
||||
break;
|
||||
case 'Delete':
|
||||
if (selectedOption > -1 && options.length > 0) {
|
||||
|
@ -167,7 +147,6 @@ class Search extends PureComponent {
|
|||
|
||||
router.history.push(`/tags/${query}`);
|
||||
onClickSearchResult(query, 'hashtag');
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleAccountClick = () => {
|
||||
|
@ -178,7 +157,6 @@ class Search extends PureComponent {
|
|||
|
||||
router.history.push(`/@${query}`);
|
||||
onClickSearchResult(query, 'account');
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleURLClick = () => {
|
||||
|
@ -186,7 +164,6 @@ class Search extends PureComponent {
|
|||
const { value, onOpenURL } = this.props;
|
||||
|
||||
onOpenURL(value, router.history);
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleStatusSearch = () => {
|
||||
|
@ -198,19 +175,13 @@ class Search extends PureComponent {
|
|||
};
|
||||
|
||||
handleRecentSearchClick = search => {
|
||||
const { onChange } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
if (search.get('type') === 'account') {
|
||||
router.history.push(`/@${search.get('q')}`);
|
||||
} else if (search.get('type') === 'hashtag') {
|
||||
router.history.push(`/tags/${search.get('q')}`);
|
||||
} else {
|
||||
onChange(search.get('q'));
|
||||
this._submit(search.get('type'));
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
};
|
||||
|
||||
handleForgetRecentSearchClick = search => {
|
||||
|
@ -223,33 +194,15 @@ class Search extends PureComponent {
|
|||
document.querySelector('.ui').parentElement.focus();
|
||||
}
|
||||
|
||||
_insertText (text) {
|
||||
const { value, onChange } = this.props;
|
||||
|
||||
if (value === '') {
|
||||
onChange(text);
|
||||
} else if (value[value.length - 1] === ' ') {
|
||||
onChange(`${value}${text}`);
|
||||
} else {
|
||||
onChange(`${value} ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
_submit (type) {
|
||||
const { onSubmit, openInRoute, value, onClickSearchResult } = this.props;
|
||||
const { onSubmit, openInRoute } = this.props;
|
||||
const { router } = this.context;
|
||||
|
||||
onSubmit(type);
|
||||
|
||||
if (value) {
|
||||
onClickSearchResult(value, type);
|
||||
}
|
||||
|
||||
if (openInRoute) {
|
||||
router.history.push('/search');
|
||||
}
|
||||
|
||||
this._unfocus();
|
||||
}
|
||||
|
||||
_getOptions () {
|
||||
|
@ -262,7 +215,7 @@ class Search extends PureComponent {
|
|||
const { recent } = this.props;
|
||||
|
||||
return recent.toArray().map(search => ({
|
||||
label: labelForRecentSearch(search),
|
||||
label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`,
|
||||
|
||||
action: () => this.handleRecentSearchClick(search),
|
||||
|
||||
|
@ -372,22 +325,6 @@ class Search extends PureComponent {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
|
||||
|
||||
{searchEnabled ? (
|
||||
<div className='search__popout__menu'>
|
||||
{this.defaultOptions.map(({ key, label, action }, i) => (
|
||||
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className='search__popout__menu__message'>
|
||||
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,36 +1,46 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
import { SearchSection } from 'mastodon/features/explore/components/search_section';
|
||||
|
||||
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import StatusContainer from '../../../containers/status_container';
|
||||
import { searchEnabled } from '../../../initial_state';
|
||||
|
||||
const INITIAL_PAGE_LIMIT = 10;
|
||||
|
||||
const withoutLastResult = list => {
|
||||
if (list.size > INITIAL_PAGE_LIMIT && list.size % INITIAL_PAGE_LIMIT === 1) {
|
||||
return list.skipLast(1);
|
||||
} else {
|
||||
return list;
|
||||
}
|
||||
};
|
||||
const messages = defineMessages({
|
||||
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
|
||||
});
|
||||
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
suggestions: ImmutablePropTypes.list.isRequired,
|
||||
fetchSuggestions: PropTypes.func.isRequired,
|
||||
expandSearch: PropTypes.func.isRequired,
|
||||
dismissSuggestion: PropTypes.func.isRequired,
|
||||
searchTerm: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.searchTerm === '') {
|
||||
this.props.fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
if (this.props.searchTerm === '') {
|
||||
this.props.fetchSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMoreAccounts = () => this.props.expandSearch('accounts');
|
||||
|
||||
handleLoadMoreStatuses = () => this.props.expandSearch('statuses');
|
||||
|
@ -38,52 +48,97 @@ class SearchResults extends ImmutablePureComponent {
|
|||
handleLoadMoreHashtags = () => this.props.expandSearch('hashtags');
|
||||
|
||||
render () {
|
||||
const { results } = this.props;
|
||||
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
|
||||
|
||||
let accounts, statuses, hashtags;
|
||||
if (searchTerm === '' && !suggestions.isEmpty()) {
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='trends'>
|
||||
<div className='trends__header'>
|
||||
<Icon id='user-plus' fixedWidth />
|
||||
<FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
|
||||
</div>
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
accounts = (
|
||||
<SearchSection title={<><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></>}>
|
||||
{withoutLastResult(results.get('accounts')).map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
{(results.get('accounts').size > INITIAL_PAGE_LIMIT && results.get('accounts').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||
</SearchSection>
|
||||
{suggestions && suggestions.map(suggestion => (
|
||||
<AccountContainer
|
||||
key={suggestion.get('account')}
|
||||
id={suggestion.get('account')}
|
||||
actionIcon={suggestion.get('source') === 'past_interactions' ? 'times' : null}
|
||||
actionTitle={suggestion.get('source') === 'past_interactions' ? intl.formatMessage(messages.dismissSuggestion) : null}
|
||||
onActionClick={dismissSuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
hashtags = (
|
||||
<SearchSection title={<><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></>}>
|
||||
{withoutLastResult(results.get('hashtags')).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
{(results.get('hashtags').size > INITIAL_PAGE_LIMIT && results.get('hashtags').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||
</SearchSection>
|
||||
let accounts, statuses, hashtags;
|
||||
let count = 0;
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
count += results.get('accounts').size;
|
||||
accounts = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5>
|
||||
|
||||
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
|
||||
|
||||
{results.get('accounts').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreAccounts} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
count += results.get('statuses').size;
|
||||
statuses = (
|
||||
<SearchSection title={<><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></>}>
|
||||
{withoutLastResult(results.get('statuses')).map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
{(results.get('statuses').size > INITIAL_PAGE_LIMIT && results.get('statuses').size % INITIAL_PAGE_LIMIT === 1) && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||
</SearchSection>
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||
|
||||
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
|
||||
|
||||
{results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
|
||||
</div>
|
||||
);
|
||||
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
|
||||
statuses = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
|
||||
|
||||
<div className='search-results__info'>
|
||||
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching posts by their content is not enabled on this Mastodon server.' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
count += results.get('hashtags').size;
|
||||
hashtags = (
|
||||
<div className='search-results__section'>
|
||||
<h5><Icon id='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5>
|
||||
|
||||
{results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
||||
|
||||
{results.get('hashtags').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreHashtags} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='search-results'>
|
||||
<div className='search-results__header'>
|
||||
<Icon id='search' fixedWidth />
|
||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
||||
<FormattedMessage id='search_results.total' defaultMessage='{count, plural, one {# result} other {# results}}' values={{ count }} />
|
||||
</div>
|
||||
|
||||
{accounts}
|
||||
{hashtags}
|
||||
{statuses}
|
||||
{hashtags}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SearchResults;
|
||||
export default injectIntl(SearchResults);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PureComponent } from 'react';
|
|||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
minWidth: `${18 * 1.28571429}px`,
|
||||
width: `${18 * 1.28571429}px`,
|
||||
};
|
||||
|
||||
export default class TextIconButton extends PureComponent {
|
||||
|
|
|
@ -15,7 +15,7 @@ import Search from '../components/search';
|
|||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['search', 'value']),
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
recent: state.getIn(['search', 'recent']).reverse(),
|
||||
recent: state.getIn(['search', 'recent']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue