diff --git a/.circleci/config.yml b/.circleci/config.yml
index 751ca95b1..6d89e670a 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -32,7 +32,7 @@ commands:
name: Install system dependencies
command: |
sudo apt-get update
- sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+ sudo apt-get install -y libicu-dev libidn11-dev
install-ruby-dependencies:
parameters:
ruby-version:
@@ -45,7 +45,7 @@ commands:
bundle config without 'development production'
name: Set bundler settings
- ruby/install-deps:
- bundler-version: '2.2.31'
+ bundler-version: '2.3.8'
key: ruby<< parameters.ruby-version >>-gems-v1
wait-db:
steps:
@@ -127,9 +127,18 @@ jobs:
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
+ - run:
+ command: ./bin/rails db:migrate VERSION=20180514140000
+ name: Run migrations up to v2.4.0
+ - run:
+ command: ./bin/rails tests:migrations:populate_v2_4
+ name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
+ - run:
+ command: ./bin/rails tests:migrations:check_database
+ name: Check migration result
test-two-step-migrations:
executor:
@@ -150,14 +159,25 @@ jobs:
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
+ - run:
+ command: ./bin/rails db:migrate VERSION=20180514140000
+ name: Run pre-deployment migrations up to v2.4.0
+ environment:
+ SKIP_POST_DEPLOYMENT_MIGRATIONS: true
+ - run:
+ command: ./bin/rails tests:migrations:populate_v2_4
+ name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
- evironment:
+ environment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
+ - run:
+ command: ./bin/rails tests:migrations:check_database
+ name: Check migration result
workflows:
version: 2
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..ac495e1c9
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,24 @@
+# [Choice] Ruby version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.1, 3.0, 2, 2.7, 2.6, 3-bullseye, 3.1-bullseye, 3.0-bullseye, 2-bullseye, 2.7-bullseye, 2.6-bullseye, 3-buster, 3.1-buster, 3.0-buster, 2-buster, 2.7-buster, 2.6-buster
+ARG VARIANT=3.1-bullseye
+FROM mcr.microsoft.com/vscode/devcontainers/ruby:${VARIANT}
+
+# 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"
+
+# [Choice] Node.js version: lts/*, 16, 14, 12, 10
+ARG NODE_VERSION="lts/*"
+RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"
+
+# [Optional] Uncomment this section to install additional OS packages.
+RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
+ && apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libpam-dev
+
+# [Optional] Uncomment this line to install additional gems.
+RUN gem install foreman
+
+# [Optional] Uncomment this line to install global node packages.
+RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g yarn" 2>&1
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..78e940763
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,26 @@
+{
+ "name": "Mastodon",
+ "dockerComposeFile": "docker-compose.yml",
+ "service": "app",
+ "workspaceFolder": "/workspaces/mastodon",
+
+ // Set *default* container specific settings.json values on container create.
+ "settings": {},
+
+ // Add the IDs of extensions you want installed when the container is created.
+ "extensions": [
+ "EditorConfig.EditorConfig",
+ "dbaeumer.vscode-eslint",
+ "rebornix.Ruby"
+ ],
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // This can be used to network with other containers or the host.
+ "forwardPorts": [3000, 4000],
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "bundle install --path vendor/bundle && yarn install && ./bin/rails db:setup",
+
+ // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
+ "remoteUser": "vscode"
+}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
new file mode 100644
index 000000000..906fce430
--- /dev/null
+++ b/.devcontainer/docker-compose.yml
@@ -0,0 +1,84 @@
+version: '3'
+
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ # Update 'VARIANT' to pick a version of Ruby: 3, 3.1, 3.0, 2, 2.7, 2.6
+ # Append -bullseye or -buster to pin to an OS version.
+ # Use -bullseye variants on local arm64/Apple Silicon.
+ VARIANT: "3.0-bullseye"
+ # Optional Node.js version to install
+ NODE_VERSION: "14"
+ volumes:
+ - ..:/workspaces/mastodon:cached
+ environment:
+ RAILS_ENV: development
+ NODE_ENV: development
+
+ REDIS_HOST: redis
+ REDIS_PORT: '6379'
+ DB_HOST: db
+ DB_USER: postgres
+ DB_PASS: postgres
+ DB_PORT: '5432'
+ ES_ENABLED: 'true'
+ ES_HOST: es
+ ES_PORT: '9200'
+ # Overrides default command so things don't shut down after the process ends.
+ command: sleep infinity
+ networks:
+ - external_network
+ - internal_network
+ user: vscode
+
+
+ db:
+ image: postgres:14-alpine
+ restart: unless-stopped
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_DB: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_HOST_AUTH_METHOD: trust
+ networks:
+ - internal_network
+
+ redis:
+ image: redis:6-alpine
+ restart: unless-stopped
+ volumes:
+ - redis-data:/data
+ networks:
+ - internal_network
+
+ es:
+ image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2
+ restart: unless-stopped
+ environment:
+ ES_JAVA_OPTS: -Xms512m -Xmx512m
+ cluster.name: es-mastodon
+ discovery.type: single-node
+ bootstrap.memory_lock: 'true'
+ volumes:
+ - es-data:/usr/share/elasticsearch/data
+ networks:
+ - internal_network
+ ulimits:
+ memlock:
+ soft: -1
+ hard: -1
+
+volumes:
+ postgres-data:
+ redis-data:
+ es-data:
+
+networks:
+ external_network:
+ internal_network:
+ internal: true
diff --git a/.env.production.sample b/.env.production.sample
index 082adaaed..341f64296 100644
--- a/.env.production.sample
+++ b/.env.production.sample
@@ -58,7 +58,7 @@ SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=587
SMTP_LOGIN=
SMTP_PASSWORD=
-SMTP_FROM_ADDRESS=notificatons@example.com
+SMTP_FROM_ADDRESS=notifications@example.com
# File storage (optional)
# -----------------------
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index fd6f74689..000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1,32 +0,0 @@
-# CODEOWNERS for mastodon/mastodon
-
-# Translators
-# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.
-# /app/javascript/mastodon/locales/fr.json @żelipapą
-# /app/views/user_mailer/*.fr.html.erb @żelipapą
-# /app/views/user_mailer/*.fr.text.erb @żelipapą
-# /config/locales/*.fr.yml @żelipapą
-# /config/locales/fr.yml @żelipapą
-
-# Polish
-/app/javascript/mastodon/locales/pl.json @m4sk1n
-/app/views/user_mailer/*.pl.html.erb @m4sk1n
-/app/views/user_mailer/*.pl.text.erb @m4sk1n
-/config/locales/*.pl.yml @m4sk1n
-/config/locales/pl.yml @m4sk1n
-
-# French
-/app/javascript/mastodon/locales/fr.json @aldarone
-/app/javascript/mastodon/locales/whitelist_fr.json @aldarone
-/app/views/user_mailer/*.fr.html.erb @aldarone
-/app/views/user_mailer/*.fr.text.erb @aldarone
-/config/locales/*.fr.yml @aldarone
-/config/locales/fr.yml @aldarone
-
-# Dutch
-/app/javascript/mastodon/locales/nl.json @jeroenpraat
-/app/javascript/mastodon/locales/whitelist_nl.json @jeroenpraat
-/app/views/user_mailer/*.nl.html.erb @jeroenpraat
-/app/views/user_mailer/*.nl.text.erb @jeroenpraat
-/config/locales/*.nl.yml @jeroenpraat
-/config/locales/nl.yml @jeroenpraat
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 9526e17db..be750a5e4 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,3 @@
patreon: mastodon
open_collective: mastodon
-github: [Gargron]
+custom: https://sponsor.joinmastodon.org
diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/2.feature_request.yml
index 00aad1341..6626c2876 100644
--- a/.github/ISSUE_TEMPLATE/2.feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/2.feature_request.yml
@@ -1,5 +1,6 @@
name: Feature Request
description: I have a suggestion
+labels: suggestion
body:
- type: markdown
attributes:
diff --git a/.github/ISSUE_TEMPLATE/3.support.md b/.github/ISSUE_TEMPLATE/3.support.md
deleted file mode 100644
index e2217da8b..000000000
--- a/.github/ISSUE_TEMPLATE/3.support.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Support
-about: Ask for help with your deployment
-title: DO NOT CREATE THIS ISSUE
----
-
-We primarily use GitHub as a bug and feature tracker. For usage questions, troubleshooting of deployments and other individual technical assistance, please use one of the resources below:
-
-- https://discourse.joinmastodon.org
-- #mastodon on irc.freenode.net
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 768868516..7c0dbaf67 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- - name: Mastodon Meta Discussion Board
- url: https://discourse.joinmastodon.org/
+ - name: GitHub Discussions
+ url: https://github.com/mastodon/mastodon/discussions
about: Please ask and answer questions here.
+ - name: Bug Bounty Program
+ url: https://app.intigriti.com/programs/mastodon/mastodonio/detail
+ about: Please report security vulnerabilities here.
diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml
index 58f2813d3..f8b4a751d 100644
--- a/.github/workflows/build-image.yml
+++ b/.github/workflows/build-image.yml
@@ -6,16 +6,22 @@ on:
- "main"
tags:
- "*"
+ pull_request:
+ paths:
+ - .github/workflows/build-image.yml
+ - Dockerfile
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
+ - uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+ if: github.event_name != 'pull_request'
- uses: docker/metadata-action@v3
id: meta
with:
@@ -25,10 +31,12 @@ jobs:
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
+ type=ref,event=pr
- uses: docker/build-push-action@v2
with:
context: .
- push: true
+ platforms: linux/amd64,linux/arm64
+ push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-to: type=inline
diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml
index 2e8f230f3..9cb98dd12 100644
--- a/.github/workflows/check-i18n.yml
+++ b/.github/workflows/check-i18n.yml
@@ -18,7 +18,7 @@ jobs:
- name: Install system dependencies
run: |
sudo apt-get update
- sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
+ sudo apt-get install -y libicu-dev libidn11-dev
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
diff --git a/.rubocop.yml b/.rubocop.yml
index 2af0f59bb..4948aea5a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -29,13 +29,17 @@ Layout/EmptyLineAfterMagicComment:
Layout/EmptyLineAfterGuardClause:
Enabled: false
+Layout/EmptyLineBetweenDefs:
+ AllowAdjacentOneLineDefs: true
+
Layout/EmptyLinesAroundAttributeAccessor:
Enabled: true
+Layout/FirstHashElementIndentation:
+ EnforcedStyle: consistent
+
Layout/HashAlignment:
Enabled: false
- # EnforcedHashRocketStyle: table
- # EnforcedColonStyle: table
Layout/SpaceAroundMethodCallOperator:
Enabled: true
diff --git a/AUTHORS.md b/AUTHORS.md
index 596451737..9fc5f44f1 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -12,31 +12,32 @@ and provided thanks to the work of the following contributors:
* [akihikodaki](https://github.com/akihikodaki)
* [mjankowski](https://github.com/mjankowski)
* [unarist](https://github.com/unarist)
+* [noellabo](https://github.com/noellabo)
* [abcang](https://github.com/abcang)
* [yiskah](https://github.com/yiskah)
-* [noellabo](https://github.com/noellabo)
-* [nolanlawson](https://github.com/nolanlawson)
* [mayaeh](https://github.com/mayaeh)
+* [nolanlawson](https://github.com/nolanlawson)
* [ysksn](https://github.com/ysksn)
+* [tribela](https://github.com/tribela)
* [sorin-davidoi](https://github.com/sorin-davidoi)
* [lynlynlynx](https://github.com/lynlynlynx)
* [m4sk1n](mailto:me@m4sk.in)
* [Marcin Mikołajczak](mailto:me@m4sk.in)
-* [tribela](https://github.com/tribela)
* [renatolond](https://github.com/renatolond)
+* [shleeable](https://github.com/shleeable)
* [alpaca-tc](https://github.com/alpaca-tc)
* [zunda](https://github.com/zunda)
* [nclm](https://github.com/nclm)
* [ineffyble](https://github.com/ineffyble)
-* [shleeable](https://github.com/shleeable)
+* [ariasuni](https://github.com/ariasuni)
* [Masoud Abkenar](mailto:ampbox@gmail.com)
* [blackle](https://github.com/blackle)
* [Quent-in](https://github.com/Quent-in)
* [JantsoP](https://github.com/JantsoP)
-* [ariasuni](https://github.com/ariasuni)
+* [Brawaru](https://github.com/Brawaru)
* [nullkal](https://github.com/nullkal)
* [yookoala](https://github.com/yookoala)
-* [Brawaru](https://github.com/Brawaru)
+* [dunn](https://github.com/dunn)
* [Aditoo17](https://github.com/Aditoo17)
* [Quenty31](https://github.com/Quenty31)
* [marek-lach](https://github.com/marek-lach)
@@ -46,10 +47,9 @@ and provided thanks to the work of the following contributors:
* [eramdam](https://github.com/eramdam)
* [Jeroen](mailto:jeroenpraat@users.noreply.github.com)
* [takayamaki](https://github.com/takayamaki)
-* [dunn](https://github.com/dunn)
* [masarakki](https://github.com/masarakki)
-* [ticky](https://github.com/ticky)
* [trwnh](https://github.com/trwnh)
+* [ticky](https://github.com/ticky)
* [ThisIsMissEm](https://github.com/ThisIsMissEm)
* [hinaloe](https://github.com/hinaloe)
* [hcmiya](https://github.com/hcmiya)
@@ -73,13 +73,13 @@ and provided thanks to the work of the following contributors:
* [MaciekBaron](https://github.com/MaciekBaron)
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
* [Sylvhem](https://github.com/Sylvhem)
+* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
* [MitarashiDango](https://github.com/MitarashiDango)
* [rinsuki](https://github.com/rinsuki)
* [angristan](https://github.com/angristan)
* [JeanGauthier](https://github.com/JeanGauthier)
* [kschaper](https://github.com/kschaper)
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
-* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
* [BenLubar](https://github.com/BenLubar)
* [mkljczk](https://github.com/mkljczk)
* [adbelle](https://github.com/adbelle)
@@ -89,11 +89,12 @@ and provided thanks to the work of the following contributors:
* [yhirano55](https://github.com/yhirano55)
* [devkral](https://github.com/devkral)
* [camponez](https://github.com/camponez)
-* [hugogameiro](https://github.com/hugogameiro)
+* [Hugo Gameiro](mailto:hmgameiro@gmail.com)
* [SerCom_KC](mailto:szescxz@gmail.com)
* [aschmitz](https://github.com/aschmitz)
* [mfmfuyu](https://github.com/mfmfuyu)
* [kedamaDQ](https://github.com/kedamaDQ)
+* [mashirozx](https://github.com/mashirozx)
* [fpiesche](https://github.com/fpiesche)
* [gandaro](https://github.com/gandaro)
* [johnsudaar](https://github.com/johnsudaar)
@@ -115,41 +116,41 @@ and provided thanks to the work of the following contributors:
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
* [fakenine](https://github.com/fakenine)
* [tsuwatch](https://github.com/tsuwatch)
+* [progval](https://github.com/progval)
* [victorhck](https://github.com/victorhck)
-* [manuelviens](https://github.com/manuelviens)
+* [manuelviens](mailto:manuelviens@users.noreply.github.com)
* [tateisu](https://github.com/tateisu)
* [fvh-P](https://github.com/fvh-P)
+* [lfuelling](https://github.com/lfuelling)
* [rtucker](https://github.com/rtucker)
* [Anna e só](mailto:contraexemplos@gmail.com)
* [dariusk](https://github.com/dariusk)
* [kazu9su](https://github.com/kazu9su)
-* [Komic](https://github.com/Komic)
+* [komic](https://github.com/komic)
* [lmorchard](https://github.com/lmorchard)
* [diomed](https://github.com/diomed)
* [Neetshin](mailto:neetshin@neetsh.in)
* [rainyday](https://github.com/rainyday)
* [tcitworld](https://github.com/tcitworld)
-* [ProgVal](https://github.com/ProgVal)
* [valentin2105](https://github.com/valentin2105)
* [yuntan](https://github.com/yuntan)
* [goofy-bz](mailto:goofy@babelzilla.org)
* [kadiix](https://github.com/kadiix)
* [kodacs](https://github.com/kodacs)
* [marcin mikołajczak](mailto:me@m4sk.in)
+* [berkes](https://github.com/berkes)
* [KScl](https://github.com/KScl)
* [sterdev](https://github.com/sterdev)
-* [mashirozx](https://github.com/mashirozx)
* [TheKinrar](https://github.com/TheKinrar)
-* [007lva](https://github.com/007lva)
* [AA4ch1](https://github.com/AA4ch1)
* [alexgleason](https://github.com/alexgleason)
-* [Bèr Kessels](mailto:ber@berk.es)
* [cpytel](https://github.com/cpytel)
* [northerner](https://github.com/northerner)
+* [weex](https://github.com/weex)
* [fhemberger](https://github.com/fhemberger)
* [Gomasy](https://github.com/Gomasy)
* [greysteil](https://github.com/greysteil)
-* [hendotcat](https://github.com/hendotcat)
+* [henrycatalinismith](https://github.com/henrycatalinismith)
* [d6rkaiz](https://github.com/d6rkaiz)
* [ladyisatis](https://github.com/ladyisatis)
* [JMendyk](https://github.com/JMendyk)
@@ -160,6 +161,8 @@ and provided thanks to the work of the following contributors:
* [pawelngei](https://github.com/pawelngei)
* [reneklacan](https://github.com/reneklacan)
* [ekiru](https://github.com/ekiru)
+* [Izorkin](https://github.com/Izorkin)
+* [unasuke](https://github.com/unasuke)
* [geta6](https://github.com/geta6)
* [happycoloredbanana](https://github.com/happycoloredbanana)
* [joenepraat](https://github.com/joenepraat)
@@ -168,11 +171,11 @@ and provided thanks to the work of the following contributors:
* [spla](mailto:sp@mastodont.cat)
* [tomfhowe](https://github.com/tomfhowe)
* [noraworld](https://github.com/noraworld)
-* [lfuelling](https://github.com/lfuelling)
* [aji-su](https://github.com/aji-su)
+* [ikuradon](https://github.com/ikuradon)
* [nzws](https://github.com/nzws)
* [duxovni](https://github.com/duxovni)
-* [smorimoto](https://github.com/smorimoto)
+* [SuperSandro2000](https://github.com/SuperSandro2000)
* [178inaba](https://github.com/178inaba)
* [acid-chicken](https://github.com/acid-chicken)
* [xgess](https://github.com/xgess)
@@ -194,37 +197,37 @@ and provided thanks to the work of the following contributors:
* [Dar13](https://github.com/Dar13)
* [nevillepark](https://github.com/nevillepark)
* [ornithocoder](https://github.com/ornithocoder)
-* [pwoolcoc](https://github.com/pwoolcoc)
+* [Paul Woolcock](mailto:paul@woolcock.us)
* [pierreozoux](https://github.com/pierreozoux)
* [qguv](https://github.com/qguv)
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
+* [rgroothuijsen](https://github.com/rgroothuijsen)
* [Sascha](mailto:sascha@serenitylabs.cloud)
* [harukasan](https://github.com/harukasan)
* [stamak](https://github.com/stamak)
* [Technowix](https://github.com/Technowix)
* [Zoeille](https://github.com/Zoeille)
-* [Thor Harald Johansen](mailto:thj@thj.no)
+* [Thorwegian](https://github.com/Thorwegian)
* [0x70b1a5](https://github.com/0x70b1a5)
* [gled-rs](https://github.com/gled-rs)
* [Valentin_NC](mailto:valentin.ouvrard@nautile.sarl)
* [R0ckweb](https://github.com/R0ckweb)
-* [Izorkin](https://github.com/Izorkin)
-* [unasuke](https://github.com/unasuke)
* [caasi](https://github.com/caasi)
+* [chandrn7](https://github.com/chandrn7)
* [chr-1x](https://github.com/chr-1x)
* [esetomo](https://github.com/esetomo)
* [foxiehkins](https://github.com/foxiehkins)
* [highemerly](https://github.com/highemerly)
* [hoodie](mailto:hoodiekitten@outlook.com)
* [kaiyou](https://github.com/kaiyou)
+* [007lva](https://github.com/007lva)
* [luzi82](https://github.com/luzi82)
* [slice](https://github.com/slice)
* [tmm576](https://github.com/tmm576)
* [unsmell](mailto:unsmell@users.noreply.github.com)
* [valerauko](https://github.com/valerauko)
+* [Grawl](https://github.com/Grawl)
* [chriswmartin](https://github.com/chriswmartin)
-* [SuperSandro2000](https://github.com/SuperSandro2000)
-* [ikuradon](https://github.com/ikuradon)
* [AndreLewin](https://github.com/AndreLewin)
* [0xflotus](https://github.com/0xflotus)
* [redtachyons](https://github.com/redtachyons)
@@ -234,18 +237,21 @@ and provided thanks to the work of the following contributors:
* [Andrew](mailto:andrewlchronister@gmail.com)
* [arielrodrigues](https://github.com/arielrodrigues)
* [aurelien-reeves](https://github.com/aurelien-reeves)
+* [BSKY](mailto:git@bsky.moe)
* [elegaanz](https://github.com/elegaanz)
* [estuans](https://github.com/estuans)
* [dissolve](https://github.com/dissolve)
* [PurpleBooth](https://github.com/PurpleBooth)
* [bradurani](https://github.com/bradurani)
* [wavebeem](https://github.com/wavebeem)
-* [bruwalfas](https://github.com/bruwalfas)
+* [thermosflasche](https://github.com/thermosflasche)
* [LottieVixen](https://github.com/LottieVixen)
* [wchristian](https://github.com/wchristian)
* [muffinista](https://github.com/muffinista)
* [cdutson](https://github.com/cdutson)
* [farlistener](https://github.com/farlistener)
+* [baby-gnu](https://github.com/baby-gnu)
+* [danieljakots](https://github.com/danieljakots)
* [divergentdave](https://github.com/divergentdave)
* [DavidLibeau](https://github.com/DavidLibeau)
* [dmerejkowsky](https://github.com/dmerejkowsky)
@@ -256,8 +262,10 @@ and provided thanks to the work of the following contributors:
* [unstabler](https://github.com/unstabler)
* [potato4d](https://github.com/potato4d)
* [h-izumi](https://github.com/h-izumi)
+* [HolgerHuo](https://github.com/HolgerHuo)
* [ErikXXon](https://github.com/ErikXXon)
* [ian-kelling](https://github.com/ian-kelling)
+* [eltociear](https://github.com/eltociear)
* [immae](https://github.com/immae)
* [J0WI](https://github.com/J0WI)
* [vahnj](https://github.com/vahnj)
@@ -283,10 +291,12 @@ and provided thanks to the work of the following contributors:
* [Nathaniel Suchy](mailto:me@lunorian.is)
* [ndarville](https://github.com/ndarville)
* [NimaBoscarino](https://github.com/NimaBoscarino)
+* [aquarla](https://github.com/aquarla)
* [Abzol](https://github.com/Abzol)
* [PatOnTheBack](https://github.com/PatOnTheBack)
* [xPaw](https://github.com/xPaw)
* [petzah](https://github.com/petzah)
+* [PeterDaveHello](https://github.com/PeterDaveHello)
* [ignisf](https://github.com/ignisf)
* [lumenwrites](https://github.com/lumenwrites)
* [remram44](https://github.com/remram44)
@@ -310,25 +320,30 @@ and provided thanks to the work of the following contributors:
* [yannicka](https://github.com/yannicka)
* [ikasoumen](https://github.com/ikasoumen)
* [zacanger](https://github.com/zacanger)
+* [l2dy](https://github.com/l2dy)
* [amazedkoumei](https://github.com/amazedkoumei)
* [anon5r](https://github.com/anon5r)
* [aus-social](https://github.com/aus-social)
+* [bsky](mailto:git@bsky.moe)
* [bsky](mailto:me@imbsky.net)
-* [chandrn7](https://github.com/chandrn7)
* [codl](https://github.com/codl)
* [cpsdqs](https://github.com/cpsdqs)
* [barzamin](https://github.com/barzamin)
* [gol-cha](https://github.com/gol-cha)
+* [gunchleoc](https://github.com/gunchleoc)
* [fhalna](https://github.com/fhalna)
* [haoyayoi](https://github.com/haoyayoi)
* [ik11235](https://github.com/ik11235)
* [kawax](https://github.com/kawax)
* [shrft](https://github.com/shrft)
+* [luigi](mailto:lvargas@rankia.com)
+* [luzpaz](https://github.com/luzpaz)
* [mbajur](https://github.com/mbajur)
* [matsurai25](https://github.com/matsurai25)
* [mecab](https://github.com/mecab)
* [nicobz25](https://github.com/nicobz25)
* [niwatori24](https://github.com/niwatori24)
+* [noiob](https://github.com/noiob)
* [oliverkeeble](https://github.com/oliverkeeble)
* [partev](https://github.com/partev)
* [pinfort](https://github.com/pinfort)
@@ -341,7 +356,6 @@ and provided thanks to the work of the following contributors:
* [vidarlee](https://github.com/vidarlee)
* [vjackson725](https://github.com/vjackson725)
* [wxcafe](https://github.com/wxcafe)
-* [Grawl](https://github.com/Grawl)
* [新都心(Neet Shin)](mailto:nucx@dio-vox.com)
* [clarfonthey](https://github.com/clarfonthey)
* [cygnan](https://github.com/cygnan)
@@ -390,7 +404,6 @@ and provided thanks to the work of the following contributors:
* [Brad Janke](mailto:brad.janke@gmail.com)
* [bclindner](https://github.com/bclindner)
* [brycied00d](https://github.com/brycied00d)
-* [berkes](https://github.com/berkes)
* [carlosjs23](https://github.com/carlosjs23)
* [cgxxx](https://github.com/cgxxx)
* [kibitan](https://github.com/kibitan)
@@ -411,7 +424,6 @@ and provided thanks to the work of the following contributors:
* [dalehenries](https://github.com/dalehenries)
* [daprice](https://github.com/daprice)
* [da2x](https://github.com/da2x)
-* [danieljakots](https://github.com/danieljakots)
* [codesections](https://github.com/codesections)
* [dar5hak](https://github.com/dar5hak)
* [kant](https://github.com/kant)
@@ -420,7 +432,7 @@ and provided thanks to the work of the following contributors:
* [caldwell](https://github.com/caldwell)
* [davidcelis](https://github.com/davidcelis)
* [davefp](https://github.com/davefp)
-* [yipdw](https://github.com/yipdw)
+* [hannahwhy](https://github.com/hannahwhy)
* [debanshuk](https://github.com/debanshuk)
* [mascali33](https://github.com/mascali33)
* [DerekNonGeneric](https://github.com/DerekNonGeneric)
@@ -445,20 +457,20 @@ and provided thanks to the work of the following contributors:
* [GenbuHase](https://github.com/GenbuHase)
* [nilsding](https://github.com/nilsding)
* [hattori6789](https://github.com/hattori6789)
-* [algernon](https://github.com/algernon)
-* [Fastbyte01](https://github.com/Fastbyte01)
-* [unrelentingtech](https://github.com/unrelentingtech)
-* [gfaivre](https://github.com/gfaivre)
-* [Fiaxhs](https://github.com/Fiaxhs)
-* [rasjonell](https://github.com/rasjonell)
-* [reedcourty](https://github.com/reedcourty)
-* [anneau](https://github.com/anneau)
-* [lanodan](https://github.com/lanodan)
-* [Harmon758](https://github.com/Harmon758)
-* [HellPie](https://github.com/HellPie)
-* [Habu-Kagumba](https://github.com/Habu-Kagumba)
-* [suzukaze](https://github.com/suzukaze)
-* [Hiromi-Kai](https://github.com/Hiromi-Kai)
+* [Gergely Nagy](mailto:algernon@users.noreply.github.com)
+* [Giuseppe Pignataro](mailto:rogepix@gmail.com)
+* [Greg V](mailto:greg@unrelenting.technology)
+* [Guewen FAIVRE](mailto:guewen.faivre@elao.com)
+* [Guillaume Lo Re](mailto:lowreg@gmail.com)
+* [Gurgen Hayrapetyan](mailto:info.gurgen@gmail.com)
+* [György Nádudvari](mailto:reedcourty@users.noreply.github.com)
+* [HIKARU KOBORI](mailto:hk.uec.univ@gmail.com)
+* [Haelwenn Monnier](mailto:lanodan@users.noreply.github.com)
+* [Harmon](mailto:harmon758@gmail.com)
+* [HellPie](mailto:hellpie@users.noreply.github.com)
+* [Herbert Kagumba](mailto:habukagumba@gmail.com)
+* [Hiroe Jun](mailto:jun.hiroe@gmail.com)
+* [Hiromi Kai](mailto:pie05041008@gmail.com)
* [Hisham Muhammad](mailto:hisham@gobolinux.org)
* [Hugo "Slaynash" Flores](mailto:hugoflores@hotmail.fr)
* [INAGAKI Hiroshi](mailto:musashino205@users.noreply.github.com)
@@ -466,7 +478,6 @@ and provided thanks to the work of the following contributors:
* [Ian McCowan](mailto:imccowan@gmail.com)
* [Ian McDowell](mailto:me@ianmcdowell.net)
* [Iijima Yasushi](mailto:kurage.cc@gmail.com)
-* [Ikko Ashimine](mailto:eltociear@gmail.com)
* [Ingo Blechschmidt](mailto:iblech@web.de)
* [J Yeary](mailto:usbsnowcrash@users.noreply.github.com)
* [Jack Michaud](mailto:jack-michaud@users.noreply.github.com)
@@ -486,6 +497,7 @@ and provided thanks to the work of the following contributors:
* [Jordan Guerder](mailto:jguerder@fr.pulseheberg.net)
* [Joseph Mingrone](mailto:jehops@users.noreply.github.com)
* [Josh Leeb-du Toit](mailto:mail@joshleeb.com)
+* [Josh Soref](mailto:2119212+jsoref@users.noreply.github.com)
* [Joshua Wood](mailto:josh@joshuawood.net)
* [Julien](mailto:tiwy57@users.noreply.github.com)
* [Julien Deswaef](mailto:juego@requiem4tv.com)
@@ -502,6 +514,7 @@ and provided thanks to the work of the following contributors:
* [Leo Wzukw](mailto:leowzukw@users.noreply.github.com)
* [Leonie](mailto:62470640+bubblineyuri@users.noreply.github.com)
* [Lex Alexander](mailto:l.alexander10@gmail.com)
+* [LinAGKar](mailto:linus.kardell@gmail.com)
* [Lorenz Diener](mailto:lorenzd@gmail.com)
* [Luc Didry](mailto:ldidry@users.noreply.github.com)
* [Lukas Burk](mailto:jemus42@users.noreply.github.com)
@@ -534,6 +547,7 @@ and provided thanks to the work of the following contributors:
* [Milton Mazzarri](mailto:milmazz@gmail.com)
* [Minku Lee](mailto:premist@me.com)
* [Minori Hiraoka](mailto:mnkai@users.noreply.github.com)
+* [MitarashiDango](mailto:mitarashi_dango@mail.matcha-soft.com)
* [Mitchell Hentges](mailto:mitch9654@gmail.com)
* [Mostafa Ahangarha](mailto:ahangarha@users.noreply.github.com)
* [Mouse Reeve](mailto:mousereeve@riseup.net)
@@ -553,7 +567,6 @@ and provided thanks to the work of the following contributors:
* [Norayr Chilingarian](mailto:norayr@arnet.am)
* [Noëlle Anthony](mailto:noelle.d.anthony@gmail.com)
* [N氏](mailto:uenok.htc@gmail.com)
-* [OSAMU SATO](mailto:satosamu@gmail.com)
* [Olivier Nicole](mailto:olivierthnicole@gmail.com)
* [Oskari Noppa](mailto:noppa@users.noreply.github.com)
* [Otakan](mailto:otakan951@gmail.com)
@@ -566,6 +579,7 @@ and provided thanks to the work of the following contributors:
* [Ratmir Karabut](mailto:rkarabut@sfmodern.ru)
* [Reto Kromer](mailto:retokromer@users.noreply.github.com)
* [Rob Watson](mailto:rfwatson@users.noreply.github.com)
+* [Rohan Sharma](mailto:i.am.lone.survivor@protonmail.com)
* [Ryan Freebern](mailto:ryan@freebern.org)
* [Ryan Wade](mailto:ryan.wade@protonmail.com)
* [Ryo Kajiwara](mailto:kfe-fecn6.prussian@s01.info)
@@ -595,6 +609,8 @@ and provided thanks to the work of the following contributors:
* [StefOfficiel](mailto:pichard.stephane@free.fr)
* [Steven Tappert](mailto:admin@dark-it.net)
* [Stéphane Guillou](mailto:stephane.guillou@member.fsf.org)
+* [Su Yang](mailto:soulteary@users.noreply.github.com)
+* [Sumak](mailto:44816995+kawsay@users.noreply.github.com)
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
* [Sébastien Santoro](mailto:dereckson@espace-win.org)
* [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
@@ -611,6 +627,7 @@ and provided thanks to the work of the following contributors:
* [Tomonori Murakami](mailto:crosslife777@gmail.com)
* [TomoyaShibata](mailto:wind.of.hometown@gmail.com)
* [Treyssat-Vincent Nino](mailto:treyssatvincent@users.noreply.github.com)
+* [Truong Nguyen](mailto:truongnmt.dev@gmail.com)
* [Udo Kramer](mailto:optik@fluffel.io)
* [Una](mailto:una@unascribed.com)
* [Ushitora Anqou](mailto:ushitora@anqou.net)
@@ -621,6 +638,7 @@ and provided thanks to the work of the following contributors:
* [Wenceslao Páez Chávez](mailto:wcpaez@gmail.com)
* [Wesley Ellis](mailto:tahnok@gmail.com)
* [Wiktor](mailto:wiktor@metacode.biz)
+* [Wonderfall](mailto:wonderfall@protonmail.com)
* [Wonderfall](mailto:wonderfall@schrodinger.io)
* [Y.Yamashiro](mailto:shukukei@mojizuri.jp)
* [YDrogen](mailto:ydrogen45@gmail.com)
@@ -634,11 +652,13 @@ and provided thanks to the work of the following contributors:
* [Yeechan Lu](mailto:wz.bluesnow@gmail.com)
* [Your Name](mailto:lorenzd@gmail.com)
* [Yusuke Abe](mailto:moonset20@gmail.com)
+* [Zach Neill](mailto:neillz@berea.edu)
* [Zachary Spector](mailto:logicaldash@gmail.com)
* [ZiiX](mailto:ziix@users.noreply.github.com)
* [asria-jp](mailto:is@alicematic.com)
* [ava](mailto:vladooku@users.noreply.github.com)
* [benklop](mailto:benklop@gmail.com)
+* [bobbyd0g](mailto:93697464+bobbyd0g@users.noreply.github.com)
* [bsky](mailto:git@imbsky.net)
* [caesarologia](mailto:lopesgemelli.1@gmail.com)
* [cbayerlein](mailto:c.bayerlein@gmail.com)
@@ -646,6 +666,7 @@ and provided thanks to the work of the following contributors:
* [chrolis](mailto:chrolis@users.noreply.github.com)
* [cormo](mailto:cormorant2+github@gmail.com)
* [d0p1](mailto:dopi-sama@hush.com)
+* [dogelover911](mailto:84288771+dogelover911@users.noreply.github.com)
* [dxwc](mailto:dxwc@users.noreply.github.com)
* [evilny0](mailto:evilny0@moomoocamp.net)
* [febrezo](mailto:felixbrezo@gmail.com)
@@ -656,6 +677,8 @@ and provided thanks to the work of the following contributors:
* [guigeekz](mailto:pattusg@gmail.com)
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
* [haosbvnker](mailto:github@chaosbunker.com)
+* [heguro](mailto:65112898+heguro@users.noreply.github.com)
+* [helloworldstack](mailto:66512512+helloworldstack@users.noreply.github.com)
* [ichi_i](mailto:51489410+ichi-i@users.noreply.github.com)
* [isati](mailto:phil@juchnowi.cz)
* [jacob](mailto:jacobherringtondeveloper@gmail.com)
@@ -671,7 +694,7 @@ and provided thanks to the work of the following contributors:
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
* [kuro5hin](mailto:rusty@kuro5hin.org)
* [leo60228](mailto:leo@60228.dev)
-* [luzpaz](mailto:luzpaz@users.noreply.github.com)
+* [matildepark](mailto:matilde.park@pm.me)
* [maxypy](mailto:maxime@mpigou.fr)
* [mhe](mailto:mail@marcus-herrmann.com)
* [mike castleman](mailto:m@mlcastle.net)
@@ -681,14 +704,15 @@ and provided thanks to the work of the following contributors:
* [muan](mailto:muan@github.com)
* [namelessGonbai](mailto:43787036+namelessgonbai@users.noreply.github.com)
* [neetshin](mailto:neetshin@neetsh.in)
-* [noiob](mailto:8197071+noiob@users.noreply.github.com)
* [notozeki](mailto:notozeki@users.noreply.github.com)
* [ntl-purism](mailto:57806346+ntl-purism@users.noreply.github.com)
* [nzws](mailto:git-yuzu@svk.jp)
+* [potpro](mailto:pptppctt@gmail.com)
* [proxy](mailto:51172302+3n-k1@users.noreply.github.com)
* [rch850](mailto:rich850@gmail.com)
* [roikale](mailto:roikale@users.noreply.github.com)
* [rysiekpl](mailto:rysiek@hackerspace.pl)
+* [sasanquaneuf](mailto:sasanquaneuf@gmail.com)
* [saturday06](mailto:dyob@lunaport.net)
* [scd31](mailto:57571338+scd31@users.noreply.github.com)
* [scriptjunkie](mailto:scriptjunkie@scriptjunkie.us)
@@ -698,6 +722,7 @@ and provided thanks to the work of the following contributors:
* [syui](mailto:syui@users.noreply.github.com)
* [tackeyy](mailto:mailto.takita.yusuke@gmail.com)
* [taicv](mailto:chuvantai@gmail.com)
+* [tkr](mailto:account@kgtkr.net)
* [tmyt](mailto:shigure@refy.net)
* [trevDev()](mailto:trev@trevdev.ca)
* [tsia](mailto:github@tsia.de)
@@ -707,6 +732,7 @@ and provided thanks to the work of the following contributors:
* [y-temp4](mailto:y.temp4@gmail.com)
* [ymmtmdk](mailto:ymmtmdk@gmail.com)
* [yoshipc](mailto:yoooo@yoshipc.net)
+* [zunda](mailto:zundan@gmail.com)
* [Özcan Zafer AYAN](mailto:ozcanzaferayan@gmail.com)
* [ばん](mailto:detteiu0321@gmail.com)
* [ふるふる](mailto:frfs@users.noreply.github.com)
@@ -726,107 +752,126 @@ This document is provided for informational purposes only. Since it is only upda
Following people have contributed to translation of Mastodon:
- GunChleoc (*Scottish Gaelic*)
-- ᛤᚤᛠᛥⴲ 👽 (KNTRO) (*Spanish, Argentina*)
-- adrmzz (*Sardinian*)
-- Hồ Nhất Duy (kantcer) (*Vietnamese*)
-- Zoltán Gera (gerazo) (*Hungarian*)
+- ケインツロ 👾 (KNTRO) (*Spanish, Argentina*)
- Sveinn í Felli (sveinki) (*Icelandic*)
-- qezwan (*Persian, Sorani (Kurdish)*)
-- NCAA (*Danish*)
-- Ramdziana F Y (rafeyu) (*Indonesian*)
-- taicv (*Vietnamese*)
-- ButterflyOfFire (BoFFire) (*French, Arabic, Kabyle*)
+- Hồ Nhất Duy (honhatduy) (*Vietnamese*)
+- Zoltán Gera (gerazo) (*Hungarian*)
+- Kristaps_M (*Latvian*)
+- NCAA (*French, Danish*)
+- adrmzz (*Sardinian*)
- Xosé M. (XoseM) (*Spanish, Galician*)
-- Evert Prants (IcyDiamond) (*Estonian*)
-- Besnik_b (*Albanian*)
+- Ramdziana F Y (rafeyu) (*Indonesian*)
+- Jeong Arm (Kjwon15) (*Spanish, Japanese, Korean, Esperanto*)
- Emanuel Pina (emanuelpina) (*Portuguese*)
-- Jeong Arm (Kjwon15) (*Japanese, Korean, Esperanto*)
-- Alix Rossi (palindromordnilap) (*French, Esperanto, Corsican*)
+- qezwan (*Persian, Sorani (Kurdish)*)
+- Besnik_b (*Albanian*)
+- ButterflyOfFire (BoFFire) (*French, Arabic, Kabyle*)
- Thai Localization (thl10n) (*Thai*)
+- Cyax (Cyaxares) (*Kurmanji (Kurdish)*)
+- taicv (*Vietnamese*)
- Daniele Lira Mereb (danilmereb) (*Portuguese, Brazilian*)
-- Joene (joenepraat) (*Dutch*)
-- Kristijan Tkalec (lapor) (*Slovenian*)
-- stan ionut (stanionut12) (*Romanian*)
- spla (*Spanish, Catalan*)
-- мачко (ma4ko) (*Bulgarian*)
-- 奈卜拉 (nebula_moe) (*Chinese Simplified*)
-- kamee (*Armenian*)
-- AJ-عجائب البرمجة (Esmail_Hazem) (*Arabic*)
-- Michal Stanke (mstanke) (*Czech*)
-- Danial Behzadi (danialbehzadi) (*Persian*)
-- borys_sh (*Ukrainian*)
-- Asier Iturralde Sarasola (aldatsa) (*Basque*)
-- Imre Kristoffer Eilertsen (DandelionSprout) (*Norwegian*)
+- Evert Prants (IcyDiamond) (*Estonian*)
- koyu (*German*)
+- Alix Rossi (palindromordnilap) (*French, Esperanto, Corsican*)
+- Joene (joenepraat) (*Dutch*)
+- stan ionut (stanionut12) (*Romanian*)
+- Mastodon 中文译者 (mastodon-linguist) (*Chinese Simplified*)
+- Kristijan Tkalec (lapor) (*Slovenian*)
+- Danial Behzadi (danialbehzadi) (*Persian*)
+- Asier Iturralde Sarasola (aldatsa) (*Basque*)
+- ManeraKai (*Arabic*)
+- мачко (ma4ko) (*Bulgarian*)
+- Roboron (*Spanish*)
+- Alessandro Levati (Oct326) (*Italian*)
+- xatier (*Chinese Traditional, Chinese Traditional, Hong Kong*)
+- Ondřej Pokorný (unextro) (*Czech*)
+- Alexander Sorokin (Brawaru) (*French, Catalan, Danish, German, Greek, Hungarian, Armenian, Korean, Portuguese, Russian, Albanian, Swedish, Ukrainian, Vietnamese, Galician*)
+- kamee (*Armenian*)
+- Michal Stanke (mstanke) (*Czech*)
+- borys_sh (*Ukrainian*)
+- Imre Kristoffer Eilertsen (DandelionSprout) (*Norwegian*)
- yeft (*Chinese Traditional, Chinese Traditional, Hong Kong*)
- Miguel Mayol (mitcoes) (*Spanish, Catalan*)
-- Sasha Sorokin (Brawaru) (*French, Catalan, Danish, German, Greek, Hungarian, Armenian, Korean, Russian, Albanian, Swedish, Ukrainian, Vietnamese, Galician*)
-- Roboron (*Spanish*)
+- Marek Ľach (mareklach) (*Polish, Slovak*)
+- Manuel Viens (manuelviens) (*French*)
+- Kimmo Kujansuu (mrkujansuu) (*Finnish*)
- Koala Yeung (yookoala) (*Chinese Traditional, Hong Kong*)
-- Ondřej Pokorný (unextro) (*Czech*)
+- enolp (*Asturian*)
- Osoitz (*Basque*)
- Peterandre (*Norwegian, Norwegian Nynorsk*)
- tzium (*Sardinian*)
-- Mélanie Chauvel (ariasuni) (*French, Arabic, Czech, German, Greek, Hungarian, Slovenian, Ukrainian, Chinese Simplified, Portuguese, Brazilian, Persian, Norwegian Nynorsk, Esperanto, Breton, Corsican, Sardinian, Kabyle*)
-- Iváns (Ivans_translator) (*Galician*)
- Maya Minatsuki (mayaeh) (*Japanese*)
-- Manuel Viens (manuelviens) (*French*)
-- Alessandro Levati (Oct326) (*Italian*)
+- Mélanie Chauvel (ariasuni) (*French, Arabic, Czech, German, Greek, Hungarian, Slovenian, Ukrainian, Chinese Simplified, Portuguese, Brazilian, Persian, Norwegian Nynorsk, Esperanto, Breton, Corsican, Sardinian, Kabyle*)
+- T. E. Kalaycı (tekrei) (*Turkish*)
+- Takeçi (polygoat) (*French, Italian*)
+- Galician Translator (Galician_translator) (*Galician*)
- lamnatos (*Greek*)
- Sean Young (assanges) (*Chinese Traditional*)
- tolstoevsky (*Russian*)
-- enolp (*Asturian*)
+- Ihor Hordiichuk (ihor_ck) (*Ukrainian*)
+- Ali Demirtaş (alidemirtas) (*Turkish*)
- Jasmine Cam Andrever (gourmas) (*Cornish*)
+- coxde (*Chinese Simplified*)
- gagik_ (*Armenian*)
- Masoud Abkenar (mabkenar) (*Persian*)
- arshat (*Kazakh*)
- Marcin Mikołajczak (mkljczkk) (*Czech, Polish, Russian*)
-- Marek Ľach (mareklach) (*Polish, Slovak*)
-- Ali Demirtaş (alidemirtas) (*Turkish*)
+- Jeff Huang (s8321414) (*Chinese Traditional*)
- Blak Ouille (BlakOuille16) (*French*)
+- e (diveedd) (*Kurmanji (Kurdish)*)
- Em St Cenydd (cancennau) (*Welsh*)
- Diluns (*Occitan*)
-- Muha Aliss (muhaaliss) (*Turkish*)
+- Nurul Azeera Hidayah @ Muhammad Nur Hidayat Yasuyoshi (MNH48.moe) (mnh48) (*Malay*)
+- Tagomago (tagomago) (*French, Spanish*)
- Jurica (ahjk) (*Croatian*)
- Aditoo17 (*Czech*)
+- Tigran (tigransimonyan) (*Armenian*)
- vishnuvaratharajan (*Tamil*)
- pulmonarycosignerkindness (*Swedish*)
+- calypsoopenmail (*French*)
- cybergene (cyber-gene) (*Japanese*)
-- Takeçi (polygoat) (*French, Italian*)
-- xatier (*Chinese Traditional*)
-- Ihor Hordiichuk (ihor_ck) (*Ukrainian*)
+- Bran_Ruz (*Breton*)
+- Gearguy (*Finnish*)
+- GiorgioHerbie (*Italian*)
+- Balázs Meskó (mesko.balazs) (*Czech, Hungarian*)
+- Martin (miles) (*Slovenian*)
- regulartranslator (*Portuguese, Brazilian*)
+- Saederup92 (*Danish*)
- ozzii (*French, Serbian (Cyrillic)*)
- Irfan (Irfan_Radz) (*Malay*)
-- Saederup92 (*Danish*)
-- Akarshan Biswas (biswasab) (*Bengali, Sanskrit*)
- Yi-Jyun Pan (pan93412) (*Chinese Traditional*)
+- ClearlyClaire (*French, Icelandic*)
+- Akarshan Biswas (biswasab) (*Bengali, Sanskrit*)
+- Kristoffer Grundström (Umeaboy) (*Swedish*)
- Rafael H L Moretti (Moretti) (*Portuguese, Brazilian*)
- d5Ziif3K (*Ukrainian*)
-- GiorgioHerbie (*Italian*)
+- හෙළබස (HelaBasa) (*Sinhala*)
+- xpil (*Polish*)
+- Rojdayek (*Kurmanji (Kurdish)*)
- christalleras (*Norwegian Nynorsk*)
+- Allen Zhong (AstroProfundis) (*Chinese Simplified*)
- Taloran (*Norwegian Nynorsk*)
-- ThibG (*French, Icelandic*)
+- Sokratis Alichanidis (alichani) (*Greek*)
+- Catalina (catalina.st) (*Romanian*)
- otrapersona (*Spanish, Spanish, Mexico*)
-- Store (HelaBasa) (*Sinhala*)
+- Ryo (DrRyo) (*Korean*)
- Mauzi (*German, Swedish*)
- atarashiako (*Chinese Simplified*)
-- 101010 (101010pl) (*Polish*)
- erictapen (*German*)
-- Tagomago (tagomago) (*French, Spanish*)
+- 101010 (101010pl) (*Polish*)
- Jaz-Michael King (jazmichaelking) (*Welsh*)
-- coxde (*Chinese Simplified*)
-- T. E. Kalaycı (tekrei) (*Turkish*)
+- axi (*Finnish*)
- silkevicious (*Italian*)
- Floxu (fredrikdim1) (*Norwegian Nynorsk*)
-- Ryo (DrRyo) (*Korean*)
+- NadieAishi (*Spanish, Spanish, Mexico*)
- Bertil Hedkvist (Berrahed) (*Swedish*)
- William(ѕ)ⁿ (wmlgr) (*Spanish*)
+- Eshagh (eshagh79) (*Persian*)
+- LNDDYL (*Chinese Traditional*)
- norayr (*Armenian*)
- Satnam S Virdi (pika10singh) (*Punjabi*)
- Tiago Epifânio (tfve) (*Portuguese*)
-- Balázs Meskó (mesko.balazs) (*Hungarian*)
-- Sokratis Alichanidis (alichani) (*Greek*)
- Mentor Gashi (mentorgashi.com) (*Albanian*)
- carolinagiorno (*Portuguese, Brazilian*)
- Hayk Khachatryan (brutusromanus123) (*Armenian*)
@@ -834,394 +879,472 @@ Following people have contributed to translation of Mastodon:
- Bharat Kumar (Marwari) (*Hindi*)
- Austra Muizniece (aus_m) (*Latvian*)
- ThonyVezbe (*Breton*)
+- Just Spanish (7_7) (*Spanish, Mexico*)
- v4vachan (*Malayalam*)
+- bilfri (*Danish*)
- dkdarshan760 (*Sanskrit*)
-- tykayn (*French*)
-- axi (*Finnish*)
-- Selyan Slimane AMIRI (SelyanKab) (*Kabyle*)
- Timur Seber (seber) (*Tatar*)
+- Slimane Selyan AMIRI (SelyanKab) (*Kabyle*)
+- VaiTon (*Italian*)
+- Vik (ViktorOn) (*Danish, Russian*)
+- tykayn (*French*)
+- GCardo (*Portuguese, Brazilian*)
- taoxvx (*Danish*)
- Hrach Mkrtchyan (mhrach87) (*Armenian*)
- sabri (thetomatoisavegetable) (*Spanish, Spanish, Argentina*)
- Dewi (Unkorneg) (*French, Breton*)
- CoelacanthusHex (*Chinese Simplified*)
-- syncopams (*Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*)
- Rhys Harrison (rhedders) (*Esperanto*)
-- Hakim Oubouali (zenata1) (*Standard Moroccan Tamazight*)
+- syncopams (*Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*)
- SteinarK (*Norwegian Nynorsk*)
+- Maxine B. Vågnes (vagnes) (*Norwegian, Norwegian Nynorsk*)
+- Hakim Oubouali (zenata1) (*Standard Moroccan Tamazight*)
+- ahangarha (*Persian*)
- Lalo Tafolla (lalotafo) (*Spanish, Spanish, Mexico*)
-- Mathias B. Vagnes (vagnes) (*Norwegian*)
- dashersyed (*Urdu (Pakistan)*)
-- Acolyte (666noob404) (*Ukrainian*)
- Conight Wang (xfddwhh) (*Chinese Simplified*)
- liffon (*Swedish*)
- Damjan Dimitrioski (gnud) (*Macedonian*)
+- Rikard Linde (rikardlinde) (*Swedish*)
+- rondnunes (*Portuguese, Brazilian*)
+- strubbl (*German*)
- PPNplus (*Thai*)
+- Frontier Translation Ltd. (frontier-translation) (*Chinese Simplified*)
- shioko (*Chinese Simplified*)
+- Kahina Mess (K_hina) (*Kabyle*)
- ZiriSut (*Kabyle*)
-- Evgeny Petrov (kondra007) (*Russian*)
+- Groosha (groosha) (*Russian*)
+- Hexandcube (hexandcube) (*Polish*)
- Gwenn (Belvar) (*Breton*)
+- 游荡 (MamaShip) (*Chinese Simplified*)
- StanleyFrew (*French*)
+- mynameismonkey (*Welsh*)
+- Edward Navarro (EdwardNavarro) (*Spanish*)
- Nikita Epifanov (Nikets) (*Russian*)
- jaranta (*Finnish*)
- Slobodan Simić (Слободан Симић) (slsimic) (*Serbian (Cyrillic)*)
+- retiolus (*Catalan*)
+- iVampireSP (*Chinese Simplified, Chinese Traditional*)
- Felicia Jongleur (midsommar) (*Swedish*)
- Denys (dector) (*Ukrainian*)
-- iVampireSP (*Chinese Simplified, Chinese Traditional*)
-- Pukima (pukimaaa) (*German*)
-- 游荡 (MamaShip) (*Chinese Simplified*)
+- Mo_der Steven (SakuraPuare) (*Chinese Simplified*)
- Vanege (*Esperanto*)
-- Rikard Linde (rikardlinde) (*Swedish*)
- Jess Rafn (therealyez) (*Danish*)
-- strubbl (*German*)
- Stasiek Michalski (hellcp) (*Polish*)
- dxwc (*Bengali*)
-- jmontane (*Catalan*)
+- Filbert Salim (gamesbert6) (*Indonesian*)
- Liboide (*Spanish*)
-- Hexandcube (hexandcube) (*Polish*)
+- jmontane (*Catalan*)
- Chris Kay (chriskarasoulis) (*Greek*)
- Johan Schiff (schyffel) (*Swedish*)
+- Rex_sa (rex07) (*Arabic*)
- Arunmozhi (tecoholic) (*Tamil*)
- zer0-x (ZER0-X) (*Arabic*)
- kat (katktv) (*Russian, Ukrainian*)
- Lauren Liberda (selfisekai) (*Polish*)
-- mynameismonkey (*Welsh*)
- oti4500 (*Hungarian, Ukrainian*)
+- Delta (Delta-Time) (*Japanese*)
+- Michael Zeevi (maze88) (*Hebrew*)
+- SarfarazAhmed (*Urdu (Pakistan)*)
- Mats Gunnar Ahlqvist (goqbi) (*Swedish*)
- diazepan (*Spanish, Spanish, Argentina*)
- marzuquccen (*Kabyle*)
+- atriix (*Swedish*)
- VictorCorreia (victorcorreia1984) (*Afrikaans*)
-- Tigran (tigransimonyan) (*Armenian*)
+- Remito (remitocat) (*Japanese*)
+- AlexKoala (alexkoala) (*Korean*)
- Juan José Salvador Piedra (JuanjoSalvador) (*Spanish*)
- BurekzFinezt (*Serbian (Cyrillic)*)
+- 森の子リスのミーコの大冒険 (Phroneris) (*Japanese*)
+- asnomgtu (*Hungarian*)
- SHeija (*Finnish*)
-- Gearguy (*Finnish*)
-- atriix (*Swedish*)
+- Врабац (Slovorad) (*Serbian (Cyrillic)*)
+- Dženan (Dzenan) (*Swedish*)
- Jack R (isaac.97_WT) (*Spanish*)
- antonyho (*Chinese Traditional, Hong Kong*)
-- asnomgtu (*Hungarian*)
-- ahangarha (*Persian*)
+- FreddyG (*Esperanto*)
- andruhov (*Russian, Ukrainian*)
- phena109 (*Chinese Traditional, Hong Kong*)
- Aryamik Sharma (Aryamik) (*Swedish, Hindi*)
- Unmual (*Spanish*)
-- 森の子リスのミーコの大冒険 (Phroneris) (*Japanese*)
+- Adrián Graña (alaris83) (*Spanish*)
+- cruz2020 (*Portuguese*)
+- vpei (*Chinese Simplified*)
- るいーね (ruine) (*Japanese*)
- Sam Tux (imahbub) (*Bengali*)
-- Kristoffer Grundström (Umeaboy) (*Swedish*)
- igordrozniak (*Polish*)
+- Michał Sidor (michcioperz) (*Polish*)
- Isaac Huang (caasih) (*Chinese Traditional*)
- AW Unad (awcodify) (*Indonesian*)
-- Allen Zhong (AstroProfundis) (*Chinese Simplified*)
+- 1Alino (*Slovak*)
- Cutls (cutls) (*Japanese*)
-- Falling Snowdin (tghgg) (*Vietnamese*)
-- Ray (Ipsumry) (*Spanish*)
-- Gianfranco Fronteddu (gianfro.gianfro) (*Sardinian*)
-- Rasmus Lindroth (RasmusLindroth) (*Swedish*)
-- Andrea Lo Iacono (niels0n) (*Italian*)
+- Goudarz Jafari (Goudarz) (*Persian*)
- Parodper (*Galician*)
+- 1 (Ipsumry) (*Spanish*)
+- Falling Snowdin (tghgg) (*Vietnamese*)
+- Rasmus Lindroth (RasmusLindroth) (*Swedish*)
+- Gianfranco Fronteddu (gianfro.gianfro) (*Sardinian*)
+- Andrea Lo Iacono (niels0n) (*Italian*)
- fucsia (*Italian*)
-- NadieAishi (*Spanish, Spanish, Mexico*)
+- Vedran Serbu (SerenoXGen) (*Croatian*)
- Kinshuk Sunil (kinshuksunil) (*Hindi*)
- Ullas Joseph (ullasjoseph) (*Malayalam*)
-- Goudarz Jafari (Goudarz) (*Persian*)
+- al_._ (*German, Russian*)
+- Matthías Páll Gissurarson (icetritlo) (*Icelandic*)
+- Percy (kecrily) (*Chinese Simplified*)
- Yu-Pai Liu (tedliou) (*Chinese Traditional*)
+- KcKcZi (*Chinese Simplified*)
- Amarin Cemthong (acitmaster) (*Thai*)
- Johannes Nilsson (nlssn) (*Swedish*)
- juanda097 (juanda-097) (*Spanish*)
+- xsml (*Chinese Simplified*)
- Anunnakey (*Macedonian*)
- erikkemp (*Dutch*)
- erikstl (*Esperanto*)
-- bobchao (*Chinese Traditional*)
- twpenguin (*Chinese Traditional*)
-- MadeInSteak (*Finnish*)
+- Po-chiang Chao (bobchao) (*Chinese Traditional*)
+- Marcus Myge (mygg-priv) (*Norwegian*)
- Esther (esthermations) (*Portuguese*)
+- MadeInSteak (*Finnish*)
- t_aus_m (*German*)
+- serapolis (*Japanese, Chinese Simplified, Chinese Traditional, Chinese Traditional, Hong Kong*)
- Heimen Stoffels (Vistaus) (*Dutch*)
- Rajarshi Guha (rajarshiguha) (*Bengali*)
-- Mo_der Steven (SakuraPuare) (*Chinese Simplified*)
- Gopal Sharma (gopalvirat) (*Hindi*)
-- arethsu (*Swedish*)
-- Carlos Solís (csolisr) (*Esperanto*)
+- Linnéa (lesbian_subnet) (*Swedish*)
+- 北䑓如法 (Nyoho) (*Japanese*)
+- abidin toumi (Zet24) (*Arabic*)
- Tofiq Abdula (Xwla) (*Sorani (Kurdish)*)
+- Carlos Solís (csolisr) (*Esperanto*)
+- Yamagishi Kazutoshi (ykzts) (*Japanese, Vietnamese, Icelandic, Sorani (Kurdish)*)
- Parthan S Ramanujam (parthan) (*Tamil*)
- Kasper Nymand (KasperNymand) (*Danish*)
-- Jeff Huang (s8321414) (*Chinese Traditional*)
-- TS (morte) (*Finnish*)
- subram (*Turkish*)
+- TS (morte) (*Finnish*)
- SensDeViata (*Ukrainian*)
- Ptrcmd (ptrcmd) (*Chinese Traditional*)
+- megaleo (*Portuguese, Brazilian*)
- SergioFMiranda (*Portuguese, Brazilian*)
-- Percy (scvoet) (*Chinese Simplified*)
-- Vivek K J (Vivekkj) (*Malayalam*)
- hiroTS (*Chinese Traditional*)
+- Vivek K J (Vivekkj) (*Malayalam*)
+- arielcostas3 (*Galician*)
- johne32rus23 (*Russian*)
- AzureNya (*Chinese Simplified*)
- OctolinGamer (octolingamer) (*Portuguese, Brazilian*)
+- filippodb (*Italian*)
- Ram varma (ram4varma) (*Tamil*)
-- 北䑓如法 (Nyoho) (*Japanese*)
+- sanser (*Russian*)
+- Y.Yamashiro (uist1idrju3i) (*Japanese*)
- Pukima (Pukimaa) (*German*)
- diorama (*Italian*)
-- Daniel Dimitrov (daniel.dimitrov) (*Bulgarian*)
- frumble (*German*)
+- Daniel Dimitrov (daniel.dimitrov) (*Bulgarian*)
- kekkepikkuni (*Tamil*)
+- MODcraft (*Chinese Simplified*)
- oorsutri (*Tamil*)
- Neo_Chen (NeoChen1024) (*Chinese Traditional*)
- Nithin V (Nithin896) (*Tamil*)
-- Marcus Myge (mygg-priv) (*Norwegian*)
- Miro Rauhala (mirorauhala) (*Finnish*)
-- AlexKoala (alexkoala) (*Korean*)
+- Oymate (*Bengali*)
- ಚಿರಾಗ್ ನಟರಾಜ್ (chiraag-nataraj) (*Kannada*)
-- Aswin C (officialcjunior) (*Malayalam*)
- Guillaume Turchini (orion78fr) (*French*)
+- Aswin C (officialcjunior) (*Malayalam*)
- Ganesh D (auntgd) (*Marathi*)
+- Yuval Nehemia (yuvalne) (*Hebrew*)
- mawoka-myblock (mawoka) (*German*)
- dragnucs2 (*Arabic*)
- Ryan Ho (koungho) (*Chinese Traditional*)
-- Pedro Henrique (exploronauta) (*Portuguese, Brazilian*)
- Tejas Harad (h_tejas) (*Marathi*)
+- Pedro Henrique (exploronauta) (*Portuguese, Brazilian*)
+- Amir Reza (ElAmir) (*Persian*)
+- Tatsuto "Laminne" Yamamoto (laminne) (*Japanese*)
- Vasanthan (vasanthan) (*Tamil*)
- 硫酸鶏 (acid_chicken) (*Japanese*)
-- clarmin b8 (clarminb8) (*Sorani (Kurdish)*)
- programizer (*German*)
+- clarmin b8 (clarminb8) (*Sorani (Kurdish)*)
- manukp (*Malayalam*)
-- earth dweller (sanethoughtyt) (*Marathi*)
- psymyn (*Hebrew*)
+- earth dweller (sanethoughtyt) (*Marathi*)
+- Marek Ľach (marek-lach) (*Slovak*)
- meijerivoi (toilet) (*Finnish*)
- essaar (*Tamil*)
+- Nemuj (Dentrado) (*Japanese, Esperanto*)
- serubeena (*Swedish*)
- Rintan (*Japanese*)
- Karol Kosek (krkkPL) (*Polish*)
-- Khó͘ Tiat-lêng (khotiatleng) (*Chinese Traditional, Taigi*)
-- Hernik (hernik27) (*Czech*)
+- Khó͘ Tiatlêng (khotiatleng) (*Chinese Traditional, Taigi*)
- valarivan (*Tamil*)
-- kuchengrab (*German*)
+- Hernik (hernik27) (*Czech*)
+- revarioba (*Spanish*)
- friedbeans (*Croatian*)
+- kuchengrab (*German*)
- Abi Turi (abi123) (*Georgian*)
- Hinaloe (hinaloe) (*Japanese*)
- Sebastián Andil (Selrond) (*Slovak*)
-- KEINOS (*Japanese*)
-- filippodb (*Italian*)
+- Ifnuth (*German*)
- Asbjørn Olling (a2) (*Danish*)
+- KEINOS (*Japanese*)
- Balázs Meskó (meskobalazs) (*Hungarian*)
+- Artem Mikhalitsin (artemmikhalitsin) (*Russian*)
+- Algustionesa Yoshi (algustionesa) (*Indonesian*)
- Bottle (suryasalem2010) (*Tamil*)
- Wrya ali (John12) (*Sorani (Kurdish)*)
- JzshAC (*Chinese Simplified*)
+- siamano (*Thai, Esperanto*)
+- gnu-ewm (*Polish*)
- Antillion (antillion99) (*Spanish*)
- Steven Tappert (sammy8806) (*German*)
- Reg3xp (*Persian*)
- Wassim EL BOUHAMIDI (elbouhamidiw) (*Arabic*)
+- Maciej Błędkowski (mble) (*Polish*)
- gowthamanb (*Tamil*)
- hiphipvargas (*Portuguese*)
-- Ch. (sftblw) (*Korean*)
+- tunisiano187 (*French*)
- Arttu Ylhävuori (arttu.ylhavuori) (*Finnish*)
-- tctovsli (*Norwegian Nynorsk*)
-- Timo Tijhof (Krinkle) (*Dutch*)
+- Ch. (sftblw) (*Korean*)
+- eorn (*Breton*)
+- Jona (88wcJoWl) (*Spanish*)
- Mikkel B. Goldschmidt (mikkelbjoern) (*Danish*)
+- Timo Tijhof (Krinkle) (*Dutch*)
+- Ka2n (kaanmetu) (*Turkish*)
+- tctovsli (*Norwegian Nynorsk*)
- mecqor labi (mecqorlabi) (*Persian*)
- Odyssey346 (alexader612) (*Norwegian*)
-- Yamagishi Kazutoshi (ykzts) (*Japanese, Icelandic, Sorani (Kurdish)*)
-- Eban (ebanDev) (*French, Esperanto*)
- vjasiegd (*Polish*)
+- Eban (ebanDev) (*French, Esperanto*)
- SamitiMed (samiti3d) (*Thai*)
- Nícolas Lavinicki (nclavinicki) (*Portuguese, Brazilian*)
-- snatcher (*Portuguese, Brazilian*)
- Rekan Adl (rekan-adl1) (*Sorani (Kurdish)*)
+- Antara2Cinta (Se7enTime) (*Indonesian*)
+- Yassine Aït-El-Mouden (yaitelmouden) (*Standard Moroccan Tamazight*)
- VSx86 (*Russian*)
- umelard (*Hebrew*)
-- Antara2Cinta (Se7enTime) (*Indonesian*)
- parnikkapore (*Thai*)
+- Lagash (lagash) (*Esperanto*)
- Sherwan Othman (sherwanothman11) (*Sorani (Kurdish)*)
-- Yassine Aït-El-Mouden (yaitelmouden) (*Standard Moroccan Tamazight*)
- SKELET (*Danish*)
+- Exbu (*Dutch*)
+- Chine Sebastien (chine.sebastien) (*French*)
- Fei Yang (Fei1Yang) (*Chinese Traditional*)
+- A A (sebastien.chine) (*French*)
- Ğani (freegnu) (*Tatar*)
-- Renato "Lond" Cerqueira (renatolond) (*Portuguese, Brazilian*)
- enipra (*Armenian*)
-- ALEM FARID (faridatcemlulaqbayli) (*Kabyle*)
+- Renato "Lond" Cerqueira (renatolond) (*Portuguese, Brazilian*)
- musix (*Persian*)
- ギャラ (gyara) (*Japanese, Chinese Simplified*)
+- ALEM FARID (faridatcemlulaqbayli) (*Kabyle*)
- Hougo (hougo) (*French*)
-- ybardapurkar (*Marathi*)
-- 亜緯丹穂 (ayiniho) (*Japanese*)
-- Adrián Lattes (haztecaso) (*Spanish*)
- Mordi Sacks (MordiSacks) (*Hebrew*)
- Trinsec (*Dutch*)
+- Adrián Lattes (haztecaso) (*Spanish*)
- Tigran's Tips (tigrank08) (*Armenian*)
-- TracyJacks (*Chinese Simplified*)
+- 亜緯丹穂 (ayiniho) (*Japanese*)
+- ybardapurkar (*Marathi*)
- Szabolcs Gál (galszabolcs810624) (*Hungarian*)
- Vladislav Săcrieriu (vladislavs14) (*Romanian*)
-- danreznik (*Hebrew*)
+- TracyJacks (*Chinese Simplified*)
- rasheedgm (*Kannada*)
-- omquylzu (*Latvian*)
-- c6ristian (*German*)
-- Belkacem Mohammed (belkacem77) (*Kabyle*)
+- danreznik (*Hebrew*)
+- Cirelli (cirelli94) (*Italian*)
+- Siddharastro Doraku (sidharastro) (*Spanish, Mexico*)
- lexxai (*Ukrainian*)
+- omquylzu (*Latvian*)
- Navjot Singh (nspeaks) (*Hindi*)
+- mkljczk (*Polish*)
+- Belkacem Mohammed (belkacem77) (*Kabyle*)
+- c6ristian (*German*)
+- damascene (*Arabic*)
- Ozai (*German*)
- Sahak Petrosyan (petrosyan) (*Armenian*)
-- Oymate (*Bengali*)
- Viorel-Cătălin Răpițeanu (rapiteanu) (*Romanian*)
-- siamano (*Thai, Esperanto*)
- Siddhartha Sarathi Basu (quinoa_biryani) (*Bengali*)
- Pachara Chantawong (pachara2202) (*Thai*)
-- Zijian Zhao (jobs2512821228) (*Chinese Simplified*)
- Skew (noan.perrot) (*French*)
-- mkljczk (*Polish*)
+- Zijian Zhao (jobs2512821228) (*Chinese Simplified*)
+- Overflow Cat (OverflowCat) (*Chinese Simplified, Chinese Traditional*)
+- dbeaver (*German*)
+- zordsdavini (*Lithuanian*)
- Guru Prasath Anandapadmanaban (guruprasath) (*Tamil*)
- turtle836 (*German*)
- Marcepanek_ (thekingmarcepan) (*Polish*)
-- Lamin (laminne) (*Japanese*)
-- Yann Aguettaz (yann-a) (*French*)
- Feruz Oripov (FeruzOripov) (*Russian*)
-- serapolis (*Chinese Simplified, Chinese Traditional*)
+- Yann Aguettaz (yann-a) (*French*)
- Mick Onio (xgc.redes) (*Asturian*)
- Malik Mann (dermalikmann) (*German*)
+- padulafacundo (*Spanish*)
- dadosch (*German*)
-- r3dsp1 (*Chinese Traditional, Hong Kong*)
- hg6 (*Hindi*)
- Tianqi Zhang (tina.zhang040609) (*Chinese Simplified*)
-- padulafacundo (*Spanish*)
+- r3dsp1 (*Chinese Traditional, Hong Kong*)
- johannes hove-henriksen (J0hsHH) (*Norwegian*)
- Orlando Murcio (Atos20) (*Spanish, Mexico*)
-- Padraic Calpin (padraic-padraic) (*Slovenian*)
- cenegd (*Chinese Simplified*)
-- piupiupiudiu (*Chinese Simplified*)
+- Youngeon Lee (YoungeonLee) (*Korean*)
- shdy (*German*)
+- Umi (mtrumi) (*Chinese Simplified, Chinese Traditional, Hong Kong*)
+- Padraic Calpin (padraic-padraic) (*Slovenian*)
- Ильзира Рахматуллина (rahmatullinailzira53) (*Tatar*)
-- Hugh Liu (youloveonlymeh) (*Chinese Simplified*)
+- piupiupiudiu (*Chinese Simplified*)
- Pixelcode (realpixelcode) (*German*)
+- Dennis Reimund (reimunddennis7) (*German*)
- Yogesh K S (yogi) (*Kannada*)
- Adithya K (adithyak04) (*Malayalam*)
-- Dennis Reimund (reimunddennis7) (*German*)
+- DAI JIE (daijie) (*Chinese Simplified*)
+- Hugh Liu (youloveonlymeh) (*Chinese Simplified*)
- Rakino (rakino) (*Chinese Simplified*)
-- Michał Sidor (michcioperz) (*Polish*)
-- AmazighNM (*Kabyle*)
-- Miquel Sabaté Solà (mssola) (*Catalan*)
+- ZQYD (*Chinese Simplified*)
+- X.M (kimonoki) (*Chinese Simplified*)
+- boni777 (*Chinese Simplified*)
- Jothipazhani Nagarajan (jothipazhani.n) (*Tamil*)
-- hallomaurits (*Dutch*)
+- Miquel Sabaté Solà (mssola) (*Catalan*)
+- Stanisław Jelnicki (JelNiSlaw) (*Polish*)
+- AmazighNM (*Kabyle*)
- alnd hezh (alndhezh) (*Sorani (Kurdish)*)
+- CloudSet (*Chinese Simplified*)
- Clash Clans (KURD12345) (*Sorani (Kurdish)*)
-- Solid Rhino (SolidRhino) (*Dutch*)
- Metehan Özyürek (MetehanOzyurek) (*Turkish*)
+- Paula SIMON (EncoreEutIlFalluQueJeLeSusse) (*French*)
+- Solid Rhino (SolidRhino) (*Dutch*)
+- nua_kr (*Korean*)
+- hallomaurits (*Dutch*)
- 林水溶 (shuiRong) (*Chinese Simplified*)
-- Sébastien Feugère (smonff) (*French*)
-- Y.Yamashiro (uist1idrju3i) (*Japanese*)
+- rikrise (*Swedish*)
- Takeshi Umeda (noellabo) (*Japanese*)
- k_taka (peaceroad) (*Japanese*)
-- hussama (*Portuguese, Brazilian*)
+- Sébastien Feugère (smonff) (*French*)
- Hallo Abdullah (hallo_hamza12) (*Sorani (Kurdish)*)
-- Ashok314 (ashok314) (*Hindi*)
-- PifyZ (*French*)
-- OminousCry (*Russian*)
+- hussama (*Portuguese, Brazilian*)
+- EzigboOmenana (*Cornish*)
- Robert Yano (throwcalmbobaway) (*Spanish, Mexico*)
-- Tom_ (*Czech*)
-- Tagada (Tagadda) (*French*)
-- shafouz (*Portuguese, Brazilian*)
- Yasin İsa YILDIRIM (redsfyre) (*Turkish*)
+- PifyZ (*French*)
+- Tagada (Tagadda) (*French*)
- eichkat3r (*German*)
+- Ashok314 (ashok314) (*Hindi*)
+- Zlr- (cZeler) (*French*)
- SnDer (*Dutch*)
-- Kahina Mess (K_hina) (*Kabyle*)
-- Swati Sani (swatisani) (*Urdu (Pakistan)*)
-- Kk (kishorkumara3) (*Kannada*)
-- Daniel M. (daniconil) (*Catalan*)
+- OminousCry (*Russian*)
+- Adam Sapiński (Adamos9898) (*Polish*)
+- Tom_ (*Czech*)
+- shafouz (*Portuguese, Brazilian*)
- Shrinivasan T (tshrinivasan) (*Tamil*)
-- 夜楓Yoka (Yoka2627) (*Chinese Simplified*)
-- Nathaël Noguès (NatNgs) (*French*)
+- Kk (kishorkumara3) (*Kannada*)
+- Swati Sani (swatisani) (*Urdu (Pakistan)*)
+- papayaisnotafood (*Chinese Traditional*)
- さっかりんにーさん (saccharin23) (*Japanese*)
-- Rex_sa (rex07) (*Arabic*)
+- Daniel M. (daniconil) (*Catalan*)
+- César Daniel Cavanzo Quintero (LeinadCQ) (*Esperanto*)
+- Nathaël Noguès (NatNgs) (*French*)
+- 夜楓Yoka (Yoka2627) (*Chinese Simplified*)
+- Mt Front (mtfront) (*Chinese Simplified*)
+- Artem (Artem4ik) (*Russian*)
- Robin van der Vliet (RobinvanderVliet) (*Esperanto*)
-- Vikatakavi (*Kannada*)
- Tradjincal (tradjincal) (*French*)
-- pullopen (*Chinese Simplified*)
- SusVersiva (*Catalan*)
-- Marvin (magicmarvman) (*German*)
- Zinkokooo (*Basque*)
-- Livingston Samuel (livingston) (*Tamil*)
-- CyberAmoeba (pseudoobscura) (*Chinese Simplified*)
-- tsundoker (*Malayalam*)
-- eorn (*Breton*)
-- prabhjot (*Hindi*)
-- mmokhi (*Persian*)
+- Marvin (magicmarvman) (*German*)
+- Vikatakavi (*Kannada*)
+- pullopen (*Chinese Simplified*)
- sergioaraujo1 (*Portuguese, Brazilian*)
+- prabhjot (*Hindi*)
+- CyberAmoeba (pseudoobscura) (*Chinese Simplified*)
+- mmokhi (*Persian*)
- Entelekheia-ousia (*Chinese Simplified*)
-- Pierre Morvan (Iriep) (*Breton*)
-- oscfd (*Spanish*)
+- Livingston Samuel (livingston) (*Tamil*)
+- tsundoker (*Malayalam*)
- skaaarrr (*German*)
-- mkljczk (mykylyjczyk) (*Polish*)
-- fedot (*Russian*)
+- Pierre Morvan (Iriep) (*Breton*)
- Paz Galindo (paz.almendra.g) (*Spanish*)
+- fedot (*Russian*)
+- mkljczk (mykylyjczyk) (*Polish*)
- Ricardo Colin (rysard) (*Spanish*)
- Philipp Fischbeck (PFischbeck) (*German*)
+- oscfd (*Spanish*)
- Zoé Bőle (zoe1337) (*German*)
-- EzigboOmenana (*Cornish*)
- GaggiX (*Italian*)
-- Lukas Fülling (lfuelling) (*German*)
- JackXu (Merman-Jack) (*Chinese Simplified*)
+- Lukas Fülling (lfuelling) (*German*)
- ralozkolya (*Georgian*)
-- Apple (blackteaovo) (*Chinese Simplified*)
-- asala4544 (*Basque*)
-- Xurxo Guerra (xguerrap) (*Galician*)
-- qwerty287 (*German*)
-- Anoop (anoopp) (*Malayalam*)
-- pezcurrel (*Italian*)
-- Samir Tighzert (samir_t7) (*Kabyle*)
+- Jason Gibson (barberpike606) (*Slovenian, Chinese Simplified*)
- Dremski (*Bulgarian*)
-- Dennis Reimund (reimund_dennis) (*German*)
-- ru_mactunnag (*Scottish Gaelic*)
-- Nocta (*French*)
+- Kaede (kaedech) (*Japanese*)
- Aymeric (AymBroussier) (*French*)
- mashirozx (*Chinese Simplified*)
+- María José Vera (mjverap) (*Spanish*)
+- asala4544 (*Basque*)
+- ronee (*Kurmanji (Kurdish)*)
+- qwerty287 (*German*)
+- pezcurrel (*Italian*)
+- Anoop (anoopp) (*Malayalam*)
+- Apple (blackteaovo) (*Chinese Simplified*)
+- Lilian Nabati (Lilounab49) (*French*)
+- ru_mactunnag (*Scottish Gaelic*)
+- Nocta (*French*)
+- Tangcuyu (*Chinese Simplified*)
+- Dennis Reimund (reimund_dennis) (*German*)
- Albatroz Jeremias (albjeremias) (*Portuguese*)
-- Matias Lavik (matiaslavik) (*Norwegian Nynorsk*)
-- Amith Raj Shetty (amithraj1989) (*Kannada*)
-- abidin toumi (Zet24) (*Arabic*)
-- mikel (mikelalas) (*Spanish*)
-- OpenAlgeria (*Arabic*)
-- random_person (*Spanish*)
-- Sais Lakshmanan (Saislakshmanan) (*Tamil*)
-- Trond Boksasp (boksasp) (*Norwegian*)
-- xpac1985 (xpac) (*German*)
-- Zlr- (cZeler) (*French*)
-- Mohammad Adnan Mahmood (adnanmig) (*Arabic*)
-- mimikun (*Japanese*)
-- smedvedev (*Russian*)
-- asretro (*Chinese Traditional, Hong Kong*)
+- Xurxo Guerra (xguerrap) (*Galician*)
+- Samir Tighzert (samir_t7) (*Kabyle*)
+- lokalisoija (*Finnish*)
+- codl (*French*)
+- thisdudeisvegan (braydofficial) (*German*)
- tamaina (*Japanese*)
+- Matias Lavik (matiaslavik) (*Norwegian Nynorsk*)
- Aman Alam (aalam) (*Punjabi*)
+- Holger Huo (holgerhuo) (*Chinese Simplified*)
+- Amith Raj Shetty (amithraj1989) (*Kannada*)
+- mimikun (*Japanese*)
+- Ragnars Eggerts (rmegg1933) (*Latvian*)
- ÀŘǾŚ PÀŚĦÀÍ (arospashai) (*Sorani (Kurdish)*)
-- Kaede (kaedech) (*Japanese*)
+- smedvedev (*Russian*)
+- Sais Lakshmanan (Saislakshmanan) (*Tamil*)
+- Mohammad Adnan Mahmood (adnanmig) (*Arabic*)
+- OpenAlgeria (*Arabic*)
+- Trond Boksasp (boksasp) (*Norwegian*)
- Doug (douglasalvespe) (*Portuguese, Brazilian*)
+- Mohd Bilal (mdb571) (*Malayalam*)
- Fleva (*Sardinian*)
-- Abijeet Patro (Abijeet) (*Basque*)
-- SamOak (*Portuguese, Brazilian*)
-- Aries (orlea) (*Japanese*)
-- Bartek Fijałkowski (brateq) (*Polish*)
-- NeverMine17 (*Russian*)
-- Brodi (brodi1) (*Dutch*)
-- Ács Zoltán (zoli111) (*Hungarian*)
-- capiscuas (*Spanish*)
-- Benjamin Cobb (benjamincobb) (*German*)
-- djoerd (*Dutch*)
-- waweic (*German*)
-- Amir Kurdo (kuraking202) (*Sorani (Kurdish)*)
-- dobrado (*Portuguese, Brazilian*)
-- Baban Abdulrahman (baban.abdulrehman) (*Sorani (Kurdish)*)
-- dcapillae (*Spanish*)
-- Azad ahmad (dashty) (*Sorani (Kurdish)*)
-- Salh_haji6 (*Sorani (Kurdish)*)
-- Ranj A Abdulqadir (RanjAhmed) (*Sorani (Kurdish)*)
-- tateisu (*Japanese*)
-- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*)
-- ebrezhoneg (*Breton*)
-- 于晚霞 (xissshawww) (*Chinese Simplified*)
-- silverscat_3 (SilversCat) (*Japanese*)
-- centumix (*Japanese*)
-- umonaca (*Chinese Simplified*)
-- Ni Futchi (futchitwo) (*Japanese*)
-- おさ (osapon) (*Japanese*)
-- kavitha129 (*Tamil*)
-- Hannah (Aniqueper1) (*Chinese Simplified*)
-- Jiniux (*Italian*)
-- Jari Ronkainen (ronchaine) (*Finnish*)
+- xpac1985 (xpac) (*German*)
+- mikel (mikelalas) (*Spanish*)
+- random_person (*Spanish*)
+- asretro (*Chinese Traditional, Hong Kong*)
+- Arĝentakato (argxentakato) (*Japanese*)
- Nithya Mary (nithyamary25) (*Tamil*)
+- Azad ahmad (dashty) (*Sorani (Kurdish)*)
+- Bartek Fijałkowski (brateq) (*Polish*)
+- ebrezhoneg (*Breton*)
+- maksutheam (*Finnish*)
+- majorblazr (*Danish*)
+- Jill H. (kokakiwi) (*French*)
+- Patrice Boivin (patriceboivin58) (*French*)
+- centumix (*Japanese*)
+- 江尚寒 (jiangshanghan) (*Chinese Simplified*)
+- hud5634j (*Spanish*)
+- おさ (osapon) (*Japanese*)
+- Jiniux (*Italian*)
+- Hannah (Aniqueper1) (*Chinese Simplified*)
+- Ni Futchi (futchitwo) (*Japanese*)
+- dobrado (*Portuguese, Brazilian*)
+- dcapillae (*Spanish*)
+- Ranj A Abdulqadir (RanjAhmed) (*Sorani (Kurdish)*)
+- Kurdish Translator (Kurdish.boy) (*Sorani (Kurdish)*)
+- Amir Kurdo (kuraking202) (*Sorani (Kurdish)*)
+- umonaca (*Chinese Simplified*)
+- Jari Ronkainen (ronchaine) (*Finnish*)
+- djoerd (*Dutch*)
+- Savarín Electrográfico Marmota Intergalactica (herrero.maty) (*Spanish*)
+- 于晚霞 (xissshawww) (*Chinese Simplified*)
+- tateisu (*Japanese*)
+- NeverMine17 (*Russian*)
+- soheilkhanalipur (*Persian*)
+- SamOak (*Portuguese, Brazilian*)
+- kavitha129 (*Tamil*)
+- Salh_haji6 (*Sorani (Kurdish)*)
+- Brodi (brodi1) (*Dutch*)
+- capiscuas (*Spanish*)
+- HSD Channel (kvdbve34) (*Russian*)
+- Abijeet Patro (Abijeet) (*Basque*)
+- Ács Zoltán (zoli111) (*Hungarian*)
+- Benjamin Cobb (benjamincobb) (*German*)
+- waweic (*German*)
+- Aries (orlea) (*Japanese*)
diff --git a/Aptfile b/Aptfile
index b2cbad714..9235141ad 100644
--- a/Aptfile
+++ b/Aptfile
@@ -4,10 +4,8 @@ libicu-dev
libidn11
libidn11-dev
libpq-dev
-libprotobuf-dev
libxdamage1
libxfixes3
-protobuf-compiler
zlib1g-dev
libcairo2
libcroco3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e9d6ea1d..519c561a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,194 @@ Changelog
All notable changes to this project will be documented in this file.
+## Unreleased
+### Added
+
+- **Add support for post editing** ([Gargron](https://github.com/mastodon/mastodon/pull/16697), [Gargron](https://github.com/mastodon/mastodon/pull/17727), [Gargron](https://github.com/mastodon/mastodon/pull/17728), [Gargron](https://github.com/mastodon/mastodon/pull/17320), [Gargron](https://github.com/mastodon/mastodon/pull/17404), [Gargron](https://github.com/mastodon/mastodon/pull/17390), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17335), [Gargron](https://github.com/mastodon/mastodon/pull/17696), [Gargron](https://github.com/mastodon/mastodon/pull/17745), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17740), [Gargron](https://github.com/mastodon/mastodon/pull/17697), [Gargron](https://github.com/mastodon/mastodon/pull/17648), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17531), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17499), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17498), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17380), [Gargron](https://github.com/mastodon/mastodon/pull/17373), [Gargron](https://github.com/mastodon/mastodon/pull/17334), [Gargron](https://github.com/mastodon/mastodon/pull/17333), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17699), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17748))
+ - Previous versions remain available for perusal and comparison
+ - People who reblogged a post are notified when it's edited
+ - New REST APIs:
+ - `PUT /api/v1/statuses/:id`
+ - `GET /api/v1/statuses/:id/history`
+ - `GET /api/v1/statuses/:id/source`
+ - New streaming API event:
+ - `update`
+- **Add appeals for moderator decisions** ([Gargron](https://github.com/mastodon/mastodon/pull/17364), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17725), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17566), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17652), [Gargron](https://github.com/mastodon/mastodon/pull/17616), [Gargron](https://github.com/mastodon/mastodon/pull/17615), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17554), [Gargron](https://github.com/mastodon/mastodon/pull/17523))
+ - All default moderator decisions now notify the affected user by e-mail
+ - They now link to an appeal page instead of suggesting replying to the e-mail
+ - They can now be found in account settings and not just e-mail
+ - Users can submit one appeal within 20 days of the decision
+ - Moderators can approve or reject the appeal
+- **Add notifications for posts deleted by moderators** ([Gargron](https://github.com/mastodon/mastodon/pull/17204), [Gargron](https://github.com/mastodon/mastodon/pull/17668), [Gargron](https://github.com/mastodon/mastodon/pull/17746), [Gargron](https://github.com/mastodon/mastodon/pull/17679), [Gargron](https://github.com/mastodon/mastodon/pull/17487))
+ - New, redesigned report view in admin UI
+ - Common report actions now only take one click to complete
+ - Deleting posts or marking as sensitive from report now notifies user
+ - Reports can be categorized by reason and specific rules violated
+ - The reasons are automatically cited in the notifications, except for spam
+ - Marking posts as sensitive now federates using post editing
+- **Add explore page with trending posts and links** ([Gargron](https://github.com/mastodon/mastodon/pull/17123), [Gargron](https://github.com/mastodon/mastodon/pull/17431), [Gargron](https://github.com/mastodon/mastodon/pull/16917), [Gargron](https://github.com/mastodon/mastodon/pull/17677), [Gargron](https://github.com/mastodon/mastodon/pull/16938), [Gargron](https://github.com/mastodon/mastodon/pull/17044), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16978), [Gargron](https://github.com/mastodon/mastodon/pull/16979), [tribela](https://github.com/mastodon/mastodon/pull/17066), [Gargron](https://github.com/mastodon/mastodon/pull/17072), [Gargron](https://github.com/mastodon/mastodon/pull/17403), [noiob](https://github.com/mastodon/mastodon/pull/17624), [mayaeh](https://github.com/mastodon/mastodon/pull/17755), [mayaeh](https://github.com/mastodon/mastodon/pull/17757), [Gargron](https://github.com/mastodon/mastodon/pull/17760), [mayaeh](https://github.com/mastodon/mastodon/pull/17762))
+ - Hashtag trends algorithm is extended to work for posts and links
+ - Links are only considered if they have an adequate preview card
+ - Preview card generation has been improved to support structured data
+ - Links can only trend if the publisher (domain) has been approved
+ - Posts can only trend if the author has been approved
+ - Individual approval and rejection for posts and links is also available
+ - Moderators are notified about pending trends at most once every 2 hours
+ - Posts and link trends are language-specific
+ - Search page is redesigned into explore page in web UI
+ - Discovery tab is coming soon in official iOS and Android apps
+ - New REST APIs:
+ - `GET /api/v1/trends/links`
+ - `GET /api/v1/trends/statuses`
+ - `GET /api/v1/trends/tags` (alias of `GET /api/v1/trends`)
+ - `GET /api/v1/admin/trends/links`
+ - `GET /api/v1/admin/trends/statuses`
+ - `GET /api/v1/admin/trends/tags`
+- **Add graphs and retention metrics to admin dashboard** ([Gargron](https://github.com/mastodon/mastodon/pull/16829), [Gargron](https://github.com/mastodon/mastodon/pull/17617), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17570), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16910), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16909), [mashirozx](https://github.com/mastodon/mastodon/pull/16884), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16854))
+ - Dashboard shows more numbers with development over time
+ - Other data such as most used interface languages and sign-up sources
+ - User retention graph shows how many new users stick around
+ - New REST APIs:
+ - `POST /api/v1/admin/measures`
+ - `POST /api/v1/admin/dimensions`
+ - `POST /api/v1/admin/retention`
+- Add `GET /api/v1/accounts/familiar_followers` to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17700))
+- Add `POST /api/v1/accounts/:id/remove_from_followers` to REST API ([noellabo](https://github.com/mastodon/mastodon/pull/16864))
+- Add `category` and `rule_ids` params to `POST /api/v1/reports` IN REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17492), [Gargron](https://github.com/mastodon/mastodon/pull/17682), [Gargron](https://github.com/mastodon/mastodon/pull/17713))
+ - `category` can be one of: `spam`, `violation`, `other` (default)
+ - `rule_ids` must reference `rules` returned in `GET /api/v1/instance`
+- Add global `lang` param to REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17464), [Gargron](https://github.com/mastodon/mastodon/pull/17592))
+- Add `types` param to `GET /api/v1/notifications` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17767))
+- **Add notifications for moderators about new sign-ups** ([Gargron](https://github.com/mastodon/mastodon/pull/16953), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17629))
+ - When a new user confirms e-mail, moderators receive a notification
+ - New streaming API event:
+ - `admin.sign_up`
+- Add authentication history ([Gargron](https://github.com/mastodon/mastodon/pull/16408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16428), [baby-gnu](https://github.com/mastodon/mastodon/pull/16654))
+- Add ability to automatically delete old posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16529), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17691), [tribela](https://github.com/mastodon/mastodon/pull/16653))
+- Add ability to pin private posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16954), [tribela](https://github.com/mastodon/mastodon/pull/17326), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17304), [MitarashiDango](https://github.com/mastodon/mastodon/pull/17647))
+- Add ability to filter search results by author using `from:` syntax ([tribela](https://github.com/mastodon/mastodon/pull/16526))
+- Add ability to delete canonical email blocks in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16644))
+- Add ability to purge undeliverable domains in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16686), [tribela](https://github.com/mastodon/mastodon/pull/17210), [tribela](https://github.com/mastodon/mastodon/pull/17741), [tribela](https://github.com/mastodon/mastodon/pull/17209))
+- Add ability to disable e-mail token authentication for specific users in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16427))
+- **Add ability to suspend accounts in batches in admin UI** ([Gargron](https://github.com/mastodon/mastodon/pull/17009), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17301), [Gargron](https://github.com/mastodon/mastodon/pull/17444))
+ - New, redesigned accounts list in admin UI
+ - Batch suspensions are meant to help clean up spam and bot accounts
+ - They do not generate notifications
+- Add ability to filter reports by origin of target account in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/16487))
+- Add support for login through OpenID Connect ([chandrn7](https://github.com/mastodon/mastodon/pull/16221))
+- Add lazy loading for emoji picker in web UI ([mashirozx](https://github.com/mastodon/mastodon/pull/16907), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17011))
+- Add single option votes tooltip in polls in web UI ([Brawaru](https://github.com/mastodon/mastodon/pull/16849))
+- Add confirmation modal when closing media edit modal with unsaved changes in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16518))
+- Add support for fetching Create and Announce activities by URI in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16383))
+- Add `S3_FORCE_SINGLE_REQUEST` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16866))
+- Add `OMNIAUTH_ONLY` environment variable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17288), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17345))
+- Add `ES_USER` and `ES_PASS` environment variables for Elasticsearch authentication ([tribela](https://github.com/mastodon/mastodon/pull/16890))
+- Add `CAS_SECURITY_ASSUME_EMAIL_IS_VERIFIED` environment variable ([baby-gnu](https://github.com/mastodon/mastodon/pull/16655))
+- Add ability to pass specific domains to `tootctl accounts cull` ([tribela](https://github.com/mastodon/mastodon/pull/16511))
+- Add `--by-uri` option to `tootctl domains purge` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16434))
+- Add `--batch-size` option to `tootctl search deploy` ([aquarla](https://github.com/mastodon/mastodon/pull/17049))
+- Add `--remove-orphans` option to `tootctl statuses remove` ([noellabo](https://github.com/mastodon/mastodon/pull/17067))
+
+### Changed
+
+- Change design of federation pages in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17704), [noellabo](https://github.com/mastodon/mastodon/pull/17735), [Gargron](https://github.com/mastodon/mastodon/pull/17765))
+- Change design of account cards in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17689))
+- Change `follow` scope to be covered by `read` and `write` scopes in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17678))
+- Change design of authorized applications page ([Gargron](https://github.com/mastodon/mastodon/pull/17656), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17686))
+- Change e-mail domain blocks to block IPs dynamically ([Gargron](https://github.com/mastodon/mastodon/pull/17635), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17650), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17649))
+- Change report modal to include category selection in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17565), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17734), [Gargron](https://github.com/mastodon/mastodon/pull/17654), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17632))
+- Change reblogs to not count towards hashtag trends anymore ([Gargron](https://github.com/mastodon/mastodon/pull/17501))
+- Change languages to be listed under standard instead of native name in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17485))
+- Change routing paths to use usernames in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/16171), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16772), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16773), [mashirozx](https://github.com/mastodon/mastodon/pull/16793), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17060))
+- Change list title input design in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17092))
+- Change "Opt-in to profile directory" preference to be general discoverability preference ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16637))
+- Change API rate limits to use /64 masking on IPv6 addresses ([tribela](https://github.com/mastodon/mastodon/pull/17588), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17600), [zunda](https://github.com/mastodon/mastodon/pull/17590))
+- Change allowed formats for locally uploaded custom emojis to include GIF ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17706), [Gargron](https://github.com/mastodon/mastodon/pull/17759))
+- Change error message when chosen password is too long ([rgroothuijsen](https://github.com/mastodon/mastodon/pull/17082))
+- Change minimum required Elasticsearch version from 6 to 7 ([noellabo](https://github.com/mastodon/mastodon/pull/16915))
+
+### Removed
+
+- Remove profile directory link from main navigation panel in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17688))
+- **Remove language detection through cld3** ([Gargron](https://github.com/mastodon/mastodon/pull/17478), [ykzts](https://github.com/mastodon/mastodon/pull/17539), [Gargron](https://github.com/mastodon/mastodon/pull/17496), [Gargron](https://github.com/mastodon/mastodon/pull/17722))
+ - cld3 is very inaccurate on short-form content even with unique alphabets
+ - Post language can be overriden individually using `language` param
+ - Otherwise, it defaults to the user's interface language
+- Remove support for `OAUTH_REDIRECT_AT_SIGN_IN` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17287))
+ - Use `OMNIAUTH_ONLY` instead
+- Remove Keybase integration ([Gargron](https://github.com/mastodon/mastodon/pull/17045))
+- Remove old columns and indexes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17245), [Gargron](https://github.com/mastodon/mastodon/pull/16409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17191))
+- Remove shortcodes from newly-created media attachments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16730), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16763))
+
+### Deprecated
+
+- `GET /api/v1/trends` → `GET /api/v1/trends/tags`
+- OAuth `follow` scope → `read` and/or `write`
+- `text` attribute on `DELETE /api/v1/statuses/:id` → `GET /api/v1/statuses/:id/source`
+
+### Fixed
+
+- Fix web manifest not permitting PWA usage from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714))
+- Fix not being able to edit media attachments for scheduled posts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17690))
+- Fix subscribed relay activities being recorded as boosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17571))
+- Fix streaming API server error messages when JSON parsing fails not specifying the source ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17559))
+- Fix browsers autofilling new password field with old password ([mashirozx](https://github.com/mastodon/mastodon/pull/17702))
+- Fix text being invisible before fonts load in web UI ([tribela](https://github.com/mastodon/mastodon/pull/16330))
+- Fix public profile pages of unconfirmed users being accessible ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17385), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17457))
+- Fix nil error when trying to fetch key for signature verification ([Gargron](https://github.com/mastodon/mastodon/pull/17747))
+- Fix null values being included in some indexes ([Gargron](https://github.com/mastodon/mastodon/pull/17711))
+- Fix `POST /api/v1/emails/confirmations` not being available after sign-up ([Gargron](https://github.com/mastodon/mastodon/pull/17743))
+- Fix rare race condition when reblogged post is deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17693), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17730))
+- Fix being able to add more than 4 hashtags to hashtag column in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17729))
+- Fix data integrity of featured tags ([Gargron](https://github.com/mastodon/mastodon/pull/17712))
+- Fix performance of account timelines ([Gargron](https://github.com/mastodon/mastodon/pull/17709))
+- Fix returning empty `
` tag for blank account `note` in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17687))
+- Fix leak of existence of otherwise inaccessible posts in REST API ([Gargron](https://github.com/mastodon/mastodon/pull/17684))
+- Fix not showing loading indicator when searching in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/17655))
+- Fix media modal footer's “external link” not being a link ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17561))
+- Fix reply button on media modal not giving focus to compose form ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17626))
+- Fix some media attachments being converted with too high framerates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17619))
+- Fix sign in token and warning emails failing to send when contact e-mail address is malformed ([helloworldstack](https://github.com/mastodon/mastodon/pull/17589))
+- Fix opening the emoji picker scrolling the single-column view to the top ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17579))
+- Fix edge case where settings/admin page sidebar would be incorrectly hidden ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17580))
+- Fix performance of server-side filtering ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17575))
+- Fix privacy policy link not being visible on small screens ([Gargron](https://github.com/mastodon/mastodon/pull/17533))
+- Fix duplicate accounts when searching by IP range in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/17524), [tribela](https://github.com/mastodon/mastodon/pull/17150))
+- Fix error when performing a batch action on posts in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17532))
+- Fix deletes not being signed in authorized fetch mode ([Gargron](https://github.com/mastodon/mastodon/pull/17484))
+- Fix Undo Announce sometimes inlining the originally Announced status ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17516))
+- Fix localization of cold-start follow recommendations ([Gargron](https://github.com/mastodon/mastodon/pull/17479), [Gargron](https://github.com/mastodon/mastodon/pull/17486))
+- Fix replies collection incorrectly looping ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17462))
+- Fix errors when multiple Delete are received for a given actor ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17460))
+- Fixed prototype pollution bug and only allow trusted origin ([r0hanSH](https://github.com/mastodon/mastodon/pull/17420))
+- Fix text being incorrectly pre-selected in composer textarea on /share ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17339))
+- Fix SMTP_ENABLE_STARTTLS_AUTO/SMTP_TLS/SMTP_SSL environment variables don't work ([kgtkr](https://github.com/mastodon/mastodon/pull/17216))
+- Fix media upload specific rate limits only being applied to v1 endpoint in REST API ([tribela](https://github.com/mastodon/mastodon/pull/17272))
+- Fix media descriptions not being used for client-side filtering ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17206))
+- Fix cold-start follow recommendation favouring older accounts due to wrong sorting ([noellabo](https://github.com/mastodon/mastodon/pull/17126))
+- Fix not redirect to the right page after authenticating with WebAuthn ([heguro](https://github.com/mastodon/mastodon/pull/17098))
+- Fix searching for additional hashtags in hashtag column ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17054))
+- Fix color of hashtag column settings inputs ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17058))
+- Fix performance of `tootctl statuses remove` ([noellabo](https://github.com/mastodon/mastodon/pull/17052))
+- Fix `tootctl accounts cull` not excluding domains on timeouts and certificate issues ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16433))
+- Fix 404 error when filtering admin action logs by non-existent target account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16643))
+- Fix error when accessing streaming API without any OAuth scopes ([Brawaru](https://github.com/mastodon/mastodon/pull/16823))
+- Fix follow request count not updating when new follow requests arrive over streaming API in web UI ([matildepark](https://github.com/mastodon/mastodon/pull/16652))
+- Fix error when unsuspending a local account ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16605))
+- Fix crash when a notification contains a not yet processed media attachment in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16573))
+- Fix wrong color of download button in audio player in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16572))
+- Fix notes for others accounts not being deleted when an account is deleted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16579))
+- Fix error when logging occurrence of unsupported video file ([noellabo](https://github.com/mastodon/mastodon/pull/16581))
+- Fix wrong elements in trends widget being hidden on smaller screens in web UI ([tribela](https://github.com/mastodon/mastodon/pull/16570))
+- Fix link to about page being displayed in limited federation mode ([weex](https://github.com/mastodon/mastodon/pull/16432))
+- Fix styling of boost button in media modal not reflecting ability to boost ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16387))
+- Fix OCR failure when erroneous lang data is in cache ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16386))
+- Fix downloading media from blocked domains in `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914))
+- Fix login form being displayed on landing page when already logged in ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17348))
+- Fix polling for media processing status too frequently in web UI ([tribela](https://github.com/mastodon/mastodon/pull/17271))
+- Fix hashtag autocomplete overriding user-typed case ([weex](https://github.com/mastodon/mastodon/pull/16460))
+- Fix WebAuthn authentication setup to not prompt for PIN ([truongnmt](https://github.com/mastodon/mastodon/pull/16545))
+
## [3.4.6] - 2022-02-03
### Fixed
@@ -87,7 +275,7 @@ All notable changes to this project will be documented in this file.
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
-- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
+- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
@@ -420,7 +608,7 @@ All notable changes to this project will be documented in this file.
- Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674))
- Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673))
- Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675))
-- Fix inefficieny when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
+- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
- Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534))
- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287))
- Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282))
@@ -507,7 +695,7 @@ All notable changes to this project will be documented in this file.
- Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260))
- In web UI, toots cannot be marked as sensitive unless there is media attached
- However, it's possible to do via API or ActivityPub
- - Thumnails of link previews of such posts now use blurhash in web UI
+ - Thumbnails of link previews of such posts now use blurhash in web UI
- The Card entity in REST API has a new `blurhash` attribute
- Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763))
- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195))
@@ -530,7 +718,7 @@ All notable changes to this project will be documented in this file.
- The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent`
- The background color is chosen from the most dominant color around the edges of the thumbnail
- The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm
- - The most satured color of the two is designated as the accent color
+ - The most saturated color of the two is designated as the accent color
- The one with the highest W3C contrast is designated as the foreground color
- If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern
- Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292))
@@ -556,7 +744,7 @@ All notable changes to this project will be documented in this file.
- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373))
- Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892))
- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834))
-- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
+- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954))
- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015))
- Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930))
@@ -572,14 +760,14 @@ All notable changes to this project will be documented in this file.
- `EMAIL_DOMAIN_WHITELIST` → `EMAIL_DOMAIN_ALLOWLIST`
- CLI option changed:
- `tootctl domains purge --whitelist-mode` → `tootctl domains purge --limited-federation-mode`
-- Remove some unnecessary database indices ([lfuelling](https://github.com/mastodon/mastodon/pull/13695), [noellabo](https://github.com/mastodon/mastodon/pull/14259))
+- Remove some unnecessary database indexes ([lfuelling](https://github.com/mastodon/mastodon/pull/13695), [noellabo](https://github.com/mastodon/mastodon/pull/14259))
- Remove unnecessary Node.js version upper bound ([ykzts](https://github.com/mastodon/mastodon/pull/14139))
### Fixed
- Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394))
-- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
-- Fix RSS feeds not being cachable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
+- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
+- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
- Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365))
- Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339))
- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061))
@@ -591,7 +779,7 @@ All notable changes to this project will be documented in this file.
- Fix new posts pushing down origin of opened dropdown in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14348))
- Fix timeline markers not being saved sometimes ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13887), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13889), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14155))
- Fix CSV uploads being rejected ([noellabo](https://github.com/mastodon/mastodon/pull/13835))
-- Fix incompatibility with ElasticSearch 7.x ([noellabo](https://github.com/mastodon/mastodon/pull/13828))
+- Fix incompatibility with Elasticsearch 7.x ([noellabo](https://github.com/mastodon/mastodon/pull/13828))
- Fix being able to search posts where you're in the target audience but not actively mentioned ([noellabo](https://github.com/mastodon/mastodon/pull/13829))
- Fix non-local posts appearing on local-only hashtag timelines in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/13827))
- Fix `tootctl media remove-orphans` choking on unknown files in storage ([Gargron](https://github.com/mastodon/mastodon/pull/13765))
@@ -706,7 +894,7 @@ All notable changes to this project will be documented in this file.
- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490))
- Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480))
- Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475))
-- Fix varioues issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
+- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
- Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441))
- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445))
- Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436))
@@ -1195,7 +1383,7 @@ All notable changes to this project will be documented in this file.
- Fix URLs appearing twice in errors of ActivityPub::DeliveryWorker ([Gargron](https://github.com/mastodon/mastodon/pull/11231))
- Fix support for HTTP proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11245))
- Fix HTTP requests to IPv6 hosts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/11240))
-- Fix error in ElasticSearch index import ([mayaeh](https://github.com/mastodon/mastodon/pull/11192))
+- Fix error in Elasticsearch index import ([mayaeh](https://github.com/mastodon/mastodon/pull/11192))
- Fix duplicate account error when seeding development database ([ysksn](https://github.com/mastodon/mastodon/pull/11366))
- Fix performance of session clean-up scheduler ([abcang](https://github.com/mastodon/mastodon/pull/11871))
- Fix older migrations not running ([zunda](https://github.com/mastodon/mastodon/pull/11377))
@@ -1205,8 +1393,8 @@ All notable changes to this project will be documented in this file.
- Fix muted text color not applying to all text ([trwnh](https://github.com/mastodon/mastodon/pull/11996))
- Fix follower/following lists resetting on back-navigation in web UI ([Gargron](https://github.com/mastodon/mastodon/pull/11986))
- Fix n+1 query when approving multiple follow requests ([abcang](https://github.com/mastodon/mastodon/pull/12004))
-- Fix records not being indexed into ElasticSearch sometimes ([Gargron](https://github.com/mastodon/mastodon/pull/12024))
-- Fix needlessly indexing unsearchable statuses into ElasticSearch ([Gargron](https://github.com/mastodon/mastodon/pull/12041))
+- Fix records not being indexed into Elasticsearch sometimes ([Gargron](https://github.com/mastodon/mastodon/pull/12024))
+- Fix needlessly indexing unsearchable statuses into Elasticsearch ([Gargron](https://github.com/mastodon/mastodon/pull/12041))
- Fix new user bootstrapping crashing when to-be-followed accounts are invalid ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/12037))
- Fix featured hashtag URL being interpreted as media or replies tab ([Gargron](https://github.com/mastodon/mastodon/pull/12048))
- Fix account counters being overwritten by parallel writes ([Gargron](https://github.com/mastodon/mastodon/pull/12045))
@@ -1496,7 +1684,7 @@ All notable changes to this project will be documented in this file.
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212))
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059))
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339))
-- Change web UI to not not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
+- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378))
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924))
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289))
@@ -1673,7 +1861,7 @@ All notable changes to this project will be documented in this file.
- Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583))
- Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625))
- Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378))
-- Do not ignore federated reports targetting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
+- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
- Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688))
- Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689))
- Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438))
diff --git a/Dockerfile b/Dockerfile
index c6287b5a7..1b3661561 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -51,7 +51,7 @@ RUN npm install -g npm@latest && \
gem install bundler && \
apt-get update && \
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
- libpq-dev libprotobuf-dev protobuf-compiler shared-mime-info
+ libpq-dev shared-mime-info
COPY Gemfile* package.json yarn.lock /opt/mastodon/
@@ -88,7 +88,7 @@ RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selectio
RUN apt-get update && \
apt-get -y --no-install-recommends install \
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
- libicu66 libprotobuf17 libidn11 libyaml-0-2 \
+ libicu66 libidn11 libyaml-0-2 \
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
ln -s /opt/mastodon /mastodon && \
gem install bundler && \
diff --git a/Gemfile b/Gemfile
index afed1ac94..44f11c84f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,8 +6,8 @@ ruby '>= 2.5.0', '< 3.1.0'
gem 'pkg-config', '~> 1.4'
gem 'rexml', '~> 3.2'
-gem 'puma', '~> 5.5'
-gem 'rails', '~> 6.1.4'
+gem 'puma', '~> 5.6'
+gem 'rails', '~> 6.1.5'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 1.2'
gem 'rack', '~> 2.2.3'
@@ -18,20 +18,18 @@ gem 'makara', '~> 0.5'
gem 'pghero', '~> 2.8'
gem 'dotenv-rails', '~> 2.7'
-gem 'aws-sdk-s3', '~> 1.111', require: false
+gem 'aws-sdk-s3', '~> 1.113', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
-gem 'kt-paperclip', '~> 7.0'
+gem 'kt-paperclip', '~> 7.1'
gem 'blurhash', '~> 0.1'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
-gem 'bootsnap', '~> 1.10.2', require: false
+gem 'bootsnap', '~> 1.10.3', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
-gem 'iso-639'
gem 'chewy', '~> 7.2'
-gem 'cld3', '~> 3.4.4'
gem 'devise', '~> 4.8'
gem 'devise-two-factor', '~> 4.0'
@@ -42,6 +40,7 @@ end
gem 'net-ldap', '~> 0.17'
gem 'omniauth-cas', '~> 2.0'
gem 'omniauth-saml', '~> 1.10'
+gem 'gitlab-omniauth-openid-connect', '~>0.5.0', require: 'omniauth_openid_connect'
gem 'omniauth', '~> 1.9'
gem 'omniauth-rails_csrf_protection', '~> 0.1'
@@ -67,9 +66,9 @@ gem 'oj', '~> 3.13'
gem 'ox', '~> 2.14'
gem 'parslet'
gem 'posix-spawn'
-gem 'pundit', '~> 2.1'
+gem 'pundit', '~> 2.2'
gem 'premailer-rails'
-gem 'rack-attack', '~> 6.5'
+gem 'rack-attack', '~> 6.6'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 6.0'
gem 'rails-settings-cached', '~> 0.6'
@@ -78,7 +77,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 2.1'
gem 'ruby-progressbar', '~> 1.11'
gem 'sanitize', '~> 6.0'
-gem 'scenic', '~> 1.5'
+gem 'scenic', '~> 1.6'
gem 'sidekiq', '~> 6.4'
gem 'sidekiq-scheduler', '~> 3.1'
gem 'sidekiq-unique-jobs', '~> 7.1'
@@ -100,12 +99,12 @@ gem 'json-ld-preloaded', '~> 3.2'
gem 'rdf-normalize', '~> 0.5'
group :development, :test do
- gem 'fabrication', '~> 2.24'
+ gem 'fabrication', '~> 2.27'
gem 'fuubar', '~> 2.5'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.9'
gem 'pry-rails', '~> 0.3'
- gem 'rspec-rails', '~> 5.0'
+ gem 'rspec-rails', '~> 5.1'
end
group :production, :test do
@@ -115,7 +114,7 @@ end
group :test do
gem 'capybara', '~> 3.36'
gem 'climate_control', '~> 0.2'
- gem 'faker', '~> 2.19'
+ gem 'faker', '~> 2.20'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.1'
@@ -126,7 +125,7 @@ end
group :development do
gem 'active_record_query_trace', '~> 1.8'
- gem 'annotate', '~> 3.1'
+ gem 'annotate', '~> 3.2'
gem 'better_errors', '~> 2.9'
gem 'binding_of_caller', '~> 1.0'
gem 'bullet', '~> 7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index dc5b33964..610503749 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,40 +1,40 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.1.4.4)
- actionpack (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actioncable (6.1.5)
+ actionpack (= 6.1.5)
+ activesupport (= 6.1.5)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.4.4)
- actionpack (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionmailbox (6.1.5)
+ actionpack (= 6.1.5)
+ activejob (= 6.1.5)
+ activerecord (= 6.1.5)
+ activestorage (= 6.1.5)
+ activesupport (= 6.1.5)
mail (>= 2.7.1)
- actionmailer (6.1.4.4)
- actionpack (= 6.1.4.4)
- actionview (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionmailer (6.1.5)
+ actionpack (= 6.1.5)
+ actionview (= 6.1.5)
+ activejob (= 6.1.5)
+ activesupport (= 6.1.5)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.1.4.4)
- actionview (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionpack (6.1.5)
+ actionview (= 6.1.5)
+ activesupport (= 6.1.5)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.4.4)
- actionpack (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ actiontext (6.1.5)
+ actionpack (= 6.1.5)
+ activerecord (= 6.1.5)
+ activestorage (= 6.1.5)
+ activesupport (= 6.1.5)
nokogiri (>= 1.8.5)
- actionview (6.1.4.4)
- activesupport (= 6.1.4.4)
+ actionview (6.1.5)
+ activesupport (= 6.1.5)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@@ -45,22 +45,22 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.8)
- activejob (6.1.4.4)
- activesupport (= 6.1.4.4)
+ activejob (6.1.5)
+ activesupport (= 6.1.5)
globalid (>= 0.3.6)
- activemodel (6.1.4.4)
- activesupport (= 6.1.4.4)
- activerecord (6.1.4.4)
- activemodel (= 6.1.4.4)
- activesupport (= 6.1.4.4)
- activestorage (6.1.4.4)
- actionpack (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activesupport (= 6.1.4.4)
- marcel (~> 1.0.0)
+ activemodel (6.1.5)
+ activesupport (= 6.1.5)
+ activerecord (6.1.5)
+ activemodel (= 6.1.5)
+ activesupport (= 6.1.5)
+ activestorage (6.1.5)
+ actionpack (= 6.1.5)
+ activejob (= 6.1.5)
+ activerecord (= 6.1.5)
+ activesupport (= 6.1.5)
+ marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.4.4)
+ activesupport (6.1.5)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -68,28 +68,30 @@ GEM
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
+ aes_key_wrap (1.1.0)
airbrussh (1.4.0)
sshkit (>= 1.6.1, != 1.7.0)
android_key_attestation (0.3.0)
- annotate (3.1.1)
- activerecord (>= 3.2, < 7.0)
+ annotate (3.2.0)
+ activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_encrypted (3.1.0)
encryptor (~> 3.0.0)
+ attr_required (1.0.1)
awrence (1.1.1)
aws-eventstream (1.2.0)
- aws-partitions (1.549.0)
- aws-sdk-core (3.125.5)
+ aws-partitions (1.558.0)
+ aws-sdk-core (3.127.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
- aws-sdk-kms (1.53.0)
- aws-sdk-core (~> 3, >= 3.125.0)
+ aws-sdk-kms (1.55.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.111.3)
- aws-sdk-core (~> 3, >= 3.125.0)
+ aws-sdk-s3 (1.113.0)
+ aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
@@ -102,11 +104,11 @@ GEM
bindata (2.4.10)
binding_of_caller (1.0.0)
debug_inspector (>= 0.0.1)
- blurhash (0.1.5)
+ blurhash (0.1.6)
ffi (~> 1.14)
- bootsnap (1.10.2)
+ bootsnap (1.10.3)
msgpack (~> 1.2)
- brakeman (5.2.0)
+ brakeman (5.2.1)
browser (4.2.0)
brpoplpush-redis_script (0.1.2)
concurrent-ruby (~> 1.0, >= 1.0.5)
@@ -126,7 +128,7 @@ GEM
sshkit (>= 1.9.0)
capistrano-bundler (2.0.1)
capistrano (~> 3.1)
- capistrano-rails (1.6.1)
+ capistrano-rails (1.6.2)
capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3)
capistrano-rbenv (2.2.0)
@@ -147,13 +149,11 @@ GEM
activesupport
cbor (0.5.9.6)
charlock_holmes (0.7.7)
- chewy (7.2.3)
+ chewy (7.2.4)
activesupport (>= 5.2)
elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl
chunky_png (1.4.0)
- cld3 (3.4.4)
- ffi (>= 1.1.0, < 1.16.0)
climate_control (0.2.0)
coderay (1.1.3)
color_diff (0.1)
@@ -183,7 +183,7 @@ GEM
devise_pam_authenticatable2 (9.2.0)
devise (>= 4.0.0)
rpam2 (~> 4.0)
- diff-lcs (1.4.4)
+ diff-lcs (1.5.0)
discard (1.2.1)
activerecord (>= 4.2, < 8)
docile (1.3.4)
@@ -208,31 +208,35 @@ GEM
multi_json
encryptor (3.0.0)
erubi (1.10.0)
- et-orbi (1.2.4)
+ et-orbi (1.2.6)
tzinfo
excon (0.76.0)
- fabrication (2.24.0)
- faker (2.19.0)
- i18n (>= 1.6, < 2)
- faraday (1.8.0)
+ fabrication (2.27.0)
+ faker (2.20.0)
+ i18n (>= 1.8.11, < 2)
+ faraday (1.9.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
- faraday-httpclient (~> 1.0.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
- faraday-net_http_persistent (~> 1.1)
+ faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
- multipart-post (>= 1.2, < 3)
+ faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.3)
+ multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
fast_blank (1.0.1)
fastimage (2.2.6)
ffi (1.15.5)
@@ -252,12 +256,16 @@ GEM
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
- fugit (1.4.5)
+ fugit (1.5.2)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4)
fuubar (2.5.1)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
+ gitlab-omniauth-openid-connect (0.5.0)
+ addressable (~> 2.7)
+ omniauth (~> 1.9)
+ openid_connect (~> 1.2)
globalid (1.0.0)
activesupport (>= 5.0)
hamlit (2.13.0)
@@ -284,10 +292,11 @@ GEM
domain_name (~> 0.5)
http-form_data (2.3.0)
http_accept_language (2.1.1)
+ httpclient (2.8.3)
httplog (1.5.0)
rack (>= 1.0)
rainbow (>= 2.0.0)
- i18n (1.8.11)
+ i18n (1.10.0)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.37)
activesupport (>= 4.0.2)
@@ -301,10 +310,13 @@ GEM
terminal-table (>= 1.5.1)
idn-ruby (0.1.4)
ipaddress (0.8.3)
- iso-639 (0.3.5)
- jmespath (1.5.0)
+ jmespath (1.6.0)
json (2.5.1)
json-canonicalization (0.3.0)
+ json-jwt (1.13.0)
+ activesupport (>= 4.2)
+ aes_key_wrap
+ bindata
json-ld (3.2.0)
htmlentities (~> 4.3)
json-canonicalization (~> 0.3)
@@ -329,7 +341,7 @@ GEM
activerecord
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
- kt-paperclip (7.0.1)
+ kt-paperclip (7.1.1)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
marcel (~> 1.0.1)
@@ -353,14 +365,14 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.13.0)
+ loofah (2.15.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
mini_mime (>= 0.1.1)
makara (0.5.1)
activerecord (>= 5.2.0)
- marcel (1.0.1)
+ marcel (1.0.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
matrix (0.4.2)
@@ -371,9 +383,9 @@ GEM
nokogiri (~> 1.10)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2021.1115)
+ mime-types-data (3.2022.0105)
mini_mime (1.1.2)
- mini_portile2 (2.7.1)
+ mini_portile2 (2.8.0)
minitest (5.15.0)
msgpack (1.4.4)
multi_json (1.15.0)
@@ -383,8 +395,8 @@ GEM
net-ssh (>= 2.6.5, < 7.0.0)
net-ssh (6.1.0)
nio4r (2.5.8)
- nokogiri (1.13.1)
- mini_portile2 (~> 2.7.0)
+ nokogiri (1.13.3)
+ mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nsa (0.2.8)
activesupport (>= 4.2, < 7)
@@ -405,17 +417,27 @@ GEM
omniauth-saml (1.10.3)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.9)
+ openid_connect (1.2.0)
+ activemodel
+ attr_required (>= 1.0.0)
+ json-jwt (>= 1.5.0)
+ rack-oauth2 (>= 1.6.1)
+ swd (>= 1.0.0)
+ tzinfo
+ validate_email
+ validate_url
+ webfinger (>= 1.0.1)
openssl (2.2.0)
openssl-signature_algorithm (0.4.0)
orm_adapter (0.5.0)
- ox (2.14.6)
+ ox (2.14.10)
parallel (1.21.0)
parser (3.1.0.0)
ast (~> 2.4.1)
parslet (2.0.0)
pastel (0.8.0)
tty-color (~> 0.5)
- pg (1.3.0)
+ pg (1.3.4)
pghero (2.8.2)
activerecord (>= 5)
pkg-config (1.4.7)
@@ -437,35 +459,41 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.6)
- puma (5.5.2)
+ puma (5.6.2)
nio4r (~> 2.0)
- pundit (2.1.1)
+ pundit (2.2.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.6.0)
rack (2.2.3)
- rack-attack (6.5.0)
+ rack-attack (6.6.0)
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
+ rack-oauth2 (1.16.0)
+ activesupport
+ attr_required
+ httpclient
+ json-jwt (>= 1.11.0)
+ rack (>= 2.1.0)
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rails (6.1.4.4)
- actioncable (= 6.1.4.4)
- actionmailbox (= 6.1.4.4)
- actionmailer (= 6.1.4.4)
- actionpack (= 6.1.4.4)
- actiontext (= 6.1.4.4)
- actionview (= 6.1.4.4)
- activejob (= 6.1.4.4)
- activemodel (= 6.1.4.4)
- activerecord (= 6.1.4.4)
- activestorage (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ rails (6.1.5)
+ actioncable (= 6.1.5)
+ actionmailbox (= 6.1.5)
+ actionmailer (= 6.1.5)
+ actionpack (= 6.1.5)
+ actiontext (= 6.1.5)
+ actionview (= 6.1.5)
+ activejob (= 6.1.5)
+ activemodel (= 6.1.5)
+ activerecord (= 6.1.5)
+ activestorage (= 6.1.5)
+ activesupport (= 6.1.5)
bundler (>= 1.15.0)
- railties (= 6.1.4.4)
+ railties (= 6.1.5)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@@ -481,11 +509,11 @@ GEM
railties (>= 6.0.0, < 7)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
- railties (6.1.4.4)
- actionpack (= 6.1.4.4)
- activesupport (= 6.1.4.4)
+ railties (6.1.5)
+ actionpack (= 6.1.5)
+ activesupport (= 6.1.5)
method_source
- rake (>= 0.13)
+ rake (>= 12.2)
thor (~> 1.0)
rainbow (3.1.1)
rake (13.0.6)
@@ -494,7 +522,7 @@ GEM
rdf-normalize (0.5.0)
rdf (~> 3.2)
redis (4.5.1)
- redis-namespace (1.8.1)
+ redis-namespace (1.8.2)
redis (>= 3.0.4)
regexp_parser (2.2.0)
request_store (1.5.0)
@@ -505,19 +533,19 @@ GEM
rexml (3.2.5)
rotp (6.2.0)
rpam2 (4.0.2)
- rqrcode (2.1.0)
+ rqrcode (2.1.1)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
- rspec-core (3.10.1)
- rspec-support (~> 3.10.0)
- rspec-expectations (3.10.1)
+ rspec-core (3.11.0)
+ rspec-support (~> 3.11.0)
+ rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
- rspec-mocks (3.10.2)
+ rspec-support (~> 3.11.0)
+ rspec-mocks (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
- rspec-rails (5.0.2)
+ rspec-support (~> 3.11.0)
+ rspec-rails (5.1.1)
actionpack (>= 5.2)
activesupport (>= 5.2)
railties (>= 5.2)
@@ -528,10 +556,10 @@ GEM
rspec-sidekiq (3.1.0)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
- rspec-support (3.10.3)
+ rspec-support (3.11.0)
rspec_junit_formatter (0.5.1)
rspec-core (>= 2, < 4, != 2.12.0)
- rubocop (1.25.0)
+ rubocop (1.25.1)
parallel (~> 1.10)
parser (>= 3.1.0.0)
rainbow (>= 2.2.2, < 4.0)
@@ -551,32 +579,32 @@ GEM
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.5)
- rufus-scheduler (3.7.0)
+ rufus-scheduler (3.8.1)
fugit (~> 1.1, >= 1.1.6)
safety_net_attestation (0.4.0)
jwt (~> 2.0)
sanitize (6.0.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
- scenic (1.5.5)
+ scenic (1.6.0)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (3.0.0)
- sidekiq (6.4.0)
+ sidekiq (6.4.1)
connection_pool (>= 2.2.2)
rack (~> 2.0)
redis (>= 4.2.0)
sidekiq-bulk (0.2.0)
sidekiq
- sidekiq-scheduler (3.1.0)
+ sidekiq-scheduler (3.1.1)
e2mmap
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
- sidekiq-unique-jobs (7.1.12)
+ sidekiq-unique-jobs (7.1.15)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 5.0, < 8.0)
@@ -602,11 +630,15 @@ GEM
sshkit (1.21.2)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
- stackprof (0.2.17)
+ stackprof (0.2.19)
statsd-ruby (1.5.0)
stoplight (2.2.1)
strong_migrations (0.7.9)
activerecord (>= 5)
+ swd (1.2.0)
+ activesupport (>= 3)
+ attr_required (>= 0.0.5)
+ httpclient (>= 2.4)
temple (0.8.2)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
@@ -641,6 +673,12 @@ GEM
unf_ext (0.0.8)
unicode-display_width (2.1.0)
uniform_notifier (1.14.2)
+ validate_email (0.1.6)
+ activemodel (>= 3.0)
+ mail (>= 2.2.5)
+ validate_url (1.0.13)
+ activemodel (>= 3.0.0)
+ public_suffix
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.0.0.alpha1)
@@ -653,6 +691,9 @@ GEM
safety_net_attestation (~> 0.4.0)
securecompare (~> 1.0)
tpm-key_attestation (~> 0.9.0)
+ webfinger (1.1.0)
+ activesupport
+ httpclient (>= 2.4)
webmock (3.14.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
@@ -672,7 +713,7 @@ GEM
xorcist (1.1.2)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.5.3)
+ zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -681,12 +722,12 @@ DEPENDENCIES
active_model_serializers (~> 0.10)
active_record_query_trace (~> 1.8)
addressable (~> 2.8)
- annotate (~> 3.1)
- aws-sdk-s3 (~> 1.111)
+ annotate (~> 3.2)
+ aws-sdk-s3 (~> 1.113)
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
- bootsnap (~> 1.10.2)
+ bootsnap (~> 1.10.3)
brakeman (~> 5.2)
browser
bullet (~> 7.0)
@@ -698,7 +739,6 @@ DEPENDENCIES
capybara (~> 3.36)
charlock_holmes (~> 0.7.7)
chewy (~> 7.2)
- cld3 (~> 3.4.4)
climate_control (~> 0.2)
color_diff (~> 0.1)
concurrent-ruby
@@ -710,13 +750,14 @@ DEPENDENCIES
doorkeeper (~> 5.5)
dotenv-rails (~> 2.7)
ed25519 (~> 1.3)
- fabrication (~> 2.24)
- faker (~> 2.19)
+ fabrication (~> 2.27)
+ faker (~> 2.20)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
fog-openstack (~> 0.3)
fuubar (~> 2.5)
+ gitlab-omniauth-openid-connect (~> 0.5.0)
hamlit-rails (~> 0.2)
hiredis (~> 0.6)
htmlentities (~> 4.3)
@@ -725,11 +766,10 @@ DEPENDENCIES
httplog (~> 1.5.0)
i18n-tasks (~> 0.9)
idn-ruby
- iso-639
json-ld
json-ld-preloaded (~> 3.2)
kaminari (~> 1.2)
- kt-paperclip (~> 7.0)
+ kt-paperclip (~> 7.1)
letter_opener (~> 1.7)
letter_opener_web (~> 2.0)
link_header (~> 0.0)
@@ -757,12 +797,12 @@ DEPENDENCIES
private_address_check (~> 0.5)
pry-byebug (~> 3.9)
pry-rails (~> 0.3)
- puma (~> 5.5)
- pundit (~> 2.1)
+ puma (~> 5.6)
+ pundit (~> 2.2)
rack (~> 2.2.3)
- rack-attack (~> 6.5)
+ rack-attack (~> 6.6)
rack-cors (~> 1.1)
- rails (~> 6.1.4)
+ rails (~> 6.1.5)
rails-controller-testing (~> 1.0)
rails-i18n (~> 6.0)
rails-settings-cached (~> 0.6)
@@ -771,14 +811,14 @@ DEPENDENCIES
redis-namespace (~> 1.8)
rexml (~> 3.2)
rqrcode (~> 2.1)
- rspec-rails (~> 5.0)
+ rspec-rails (~> 5.1)
rspec-sidekiq (~> 3.1)
rspec_junit_formatter (~> 0.5)
rubocop (~> 1.25)
rubocop-rails (~> 2.13)
ruby-progressbar (~> 1.11)
sanitize (~> 6.0)
- scenic (~> 1.5)
+ scenic (~> 1.6)
sidekiq (~> 6.4)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.1)
diff --git a/README.md b/README.md
index 8aa575b45..4b48e071d 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ Click below to **learn more** in a video:
- [View sponsors](https://joinmastodon.org/sponsors)
- [Blog](https://blog.joinmastodon.org)
- [Documentation](https://docs.joinmastodon.org)
-- [Browse Mastodon servers](https://joinmastodon.org/#getting-started)
+- [Browse Mastodon servers](https://joinmastodon.org/communities)
- [Browse Mastodon apps](https://joinmastodon.org/apps)
[patreon]: https://www.patreon.com/mastodon
@@ -92,7 +92,7 @@ You can open issues for bugs you've found or features you think are missing. You
## License
-Copyright (C) 2016-2021 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.
diff --git a/SECURITY.md b/SECURITY.md
index 9d351fce6..5531a306e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,13 +1,19 @@
# 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 should submit the report through our [Bug Bounty Program][bug-bounty]. Alternatively, you can reach us at .
+
+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
+
+A "vulnerability in Mastodon" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us.
+
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
-| 3.4.x | :white_check_mark: |
-| 3.3.x | :white_check_mark: |
-| < 3.3 | :x: |
+| 3.4.x | Yes |
+| 3.3.x | Yes |
+| < 3.3 | No |
-## Reporting a Vulnerability
-
-hello@joinmastodon.org
+[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail
diff --git a/Vagrantfile b/Vagrantfile
index e086ddd98..3e73d9e47 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -33,11 +33,9 @@ sudo apt-get install \
redis-tools \
postgresql \
postgresql-contrib \
- protobuf-compiler \
yarn \
libicu-dev \
libidn11-dev \
- libprotobuf-dev \
libreadline-dev \
libpam0g-dev \
-y
diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb
index 7090c3f37..365a1483b 100644
--- a/app/chewy/statuses_index.rb
+++ b/app/chewy/statuses_index.rb
@@ -66,7 +66,7 @@ class StatusesIndex < Chewy::Index
field :id, type: 'long'
field :account_id, type: 'long'
- field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
+ field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.ordered_media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content'
end
diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb
index b2aab56a5..cd3992502 100644
--- a/app/controllers/activitypub/outboxes_controller.rb
+++ b/app/controllers/activitypub/outboxes_controller.rb
@@ -62,7 +62,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
return unless page_requested?
@statuses = cache_collection_paginated_by_id(
- @account.statuses.permitted_for(@account, signed_request_account),
+ AccountStatusesFilter.new(@account, signed_request_account).results,
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb
index fde6c861f..4ff7cfa08 100644
--- a/app/controllers/activitypub/replies_controller.rb
+++ b/app/controllers/activitypub/replies_controller.rb
@@ -63,15 +63,29 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
end
def next_page
- only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
+ if only_other_accounts?
+ # Only consider remote accounts
+ return nil if @replies.size < DESCENDANTS_LIMIT
- account_status_replies_url(
- @account,
- @status,
- page: true,
- min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
- only_other_accounts: only_other_accounts
- )
+ account_status_replies_url(
+ @account,
+ @status,
+ page: true,
+ min_id: @replies&.last&.id,
+ only_other_accounts: true
+ )
+ else
+ # For now, we're serving only self-replies, but next page might be other accounts
+ next_only_other_accounts = @replies&.last&.account_id != @account.id || @replies.size < DESCENDANTS_LIMIT
+
+ account_status_replies_url(
+ @account,
+ @status,
+ page: true,
+ min_id: next_only_other_accounts ? nil : @replies&.last&.id,
+ only_other_accounts: next_only_other_accounts
+ )
+ end
end
def page_params
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index e7f56e243..e0ae71b9f 100644
--- a/app/controllers/admin/accounts_controller.rb
+++ b/app/controllers/admin/accounts_controller.rb
@@ -28,7 +28,7 @@ module Admin
@deletion_request = @account.deletion_request
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
@moderation_notes = @account.targeted_moderation_notes.latest
- @warnings = @account.strikes.custom.latest
+ @warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
@domain_block = DomainBlock.rule_for(@account.domain)
end
@@ -146,7 +146,7 @@ module Admin
end
def filter_params
- params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
+ params.slice(:page, *AccountFilter::KEYS).permit(:page, *AccountFilter::KEYS)
end
def form_account_batch_params
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index f0a935411..e376baab2 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -8,6 +8,7 @@ module Admin
@pending_users_count = User.pending.count
@pending_reports_count = Report.unresolved.count
@pending_tags_count = Tag.pending_review.count
+ @pending_appeals_count = Appeal.pending.count
end
private
diff --git a/app/controllers/admin/disputes/appeals_controller.rb b/app/controllers/admin/disputes/appeals_controller.rb
new file mode 100644
index 000000000..32e5e2f6f
--- /dev/null
+++ b/app/controllers/admin/disputes/appeals_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class Admin::Disputes::AppealsController < Admin::BaseController
+ before_action :set_appeal, except: :index
+
+ def index
+ authorize :appeal, :index?
+
+ @appeals = filtered_appeals.page(params[:page])
+ end
+
+ def approve
+ authorize @appeal, :approve?
+ log_action :approve, @appeal
+ ApproveAppealService.new.call(@appeal, current_account)
+ redirect_to disputes_strike_path(@appeal.strike)
+ end
+
+ def reject
+ authorize @appeal, :approve?
+ log_action :reject, @appeal
+ @appeal.reject!(current_account)
+ UserMailer.appeal_rejected(@appeal.account.user, @appeal)
+ redirect_to disputes_strike_path(@appeal.strike)
+ end
+
+ private
+
+ def filtered_appeals
+ Admin::AppealFilter.new(filter_params.with_defaults(status: 'pending')).results.includes(strike: :account)
+ end
+
+ def filter_params
+ params.slice(:page, *Admin::AppealFilter::KEYS).permit(:page, *Admin::AppealFilter::KEYS)
+ end
+
+ def set_appeal
+ @appeal = Appeal.find(params[:id])
+ end
+end
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index b140c454c..16defc1ea 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -56,10 +56,6 @@ module Admin
end
end
- def show
- authorize @domain_block, :show?
- end
-
def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block)
diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb
index f7bdfb0c5..a4bbbba5b 100644
--- a/app/controllers/admin/email_domain_blocks_controller.rb
+++ b/app/controllers/admin/email_domain_blocks_controller.rb
@@ -6,7 +6,20 @@ module Admin
def index
authorize :email_domain_block, :index?
+
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
+ @form = Form::EmailDomainBlockBatch.new
+ end
+
+ def batch
+ @form = Form::EmailDomainBlockBatch.new(form_email_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form.save
+ rescue ActionController::ParameterMissing
+ flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected')
+ rescue Mastodon::NotPermittedError
+ flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
+ ensure
+ redirect_to admin_email_domain_blocks_path
end
def new
@@ -19,41 +32,27 @@ module Admin
@email_domain_block = EmailDomainBlock.new(resource_params)
- if @email_domain_block.save
- log_action :create, @email_domain_block
+ if action_from_button == 'save'
+ EmailDomainBlock.transaction do
+ @email_domain_block.save!
+ log_action :create, @email_domain_block
- if @email_domain_block.with_dns_records?
- hostnames = []
- ips = []
+ (@email_domain_block.other_domains || []).uniq.each do |domain|
+ next if EmailDomainBlock.where(domain: domain).exists?
- Resolv::DNS.open do |dns|
- dns.timeouts = 5
-
- hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
-
- ([@email_domain_block.domain] + hostnames).uniq.each do |hostname|
- ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
- ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
- end
- end
-
- (hostnames + ips).each do |hostname|
- another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block)
- log_action :create, another_email_domain_block if another_email_domain_block.save
+ other_email_domain_block = EmailDomainBlock.create!(domain: domain, parent: @email_domain_block)
+ log_action :create, other_email_domain_block
end
end
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
+ set_resolved_records
render :new
end
- end
-
- def destroy
- authorize @email_domain_block, :destroy?
- @email_domain_block.destroy!
- log_action :destroy, @email_domain_block
- redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
+ rescue ActiveRecord::RecordInvalid
+ set_resolved_records
+ render :new
end
private
@@ -62,8 +61,27 @@ module Admin
@email_domain_block = EmailDomainBlock.find(params[:id])
end
+ def set_resolved_records
+ Resolv::DNS.open do |dns|
+ dns.timeouts = 5
+ @resolved_records = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a
+ end
+ end
+
def resource_params
- params.require(:email_domain_block).permit(:domain, :with_dns_records)
+ params.require(:email_domain_block).permit(:domain, other_domains: [])
+ end
+
+ def form_email_domain_block_batch_params
+ params.require(:form_email_domain_block_batch).permit(email_domain_block_ids: [])
+ end
+
+ def action_from_button
+ if params[:delete]
+ 'delete'
+ elsif params[:save]
+ 'save'
+ end
end
end
end
diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb
index 306ec1f53..5c82331de 100644
--- a/app/controllers/admin/instances_controller.rb
+++ b/app/controllers/admin/instances_controller.rb
@@ -4,28 +4,26 @@ module Admin
class InstancesController < BaseController
before_action :set_instances, only: :index
before_action :set_instance, except: :index
- before_action :set_exhausted_deliveries_days, only: :show
def index
authorize :instance, :index?
+ preload_delivery_failures!
end
def show
authorize :instance, :show?
+ @time_period = (6.days.ago.to_date...Time.now.utc.to_date)
end
def destroy
authorize :instance, :destroy?
-
Admin::DomainPurgeWorker.perform_async(@instance.domain)
-
log_action :destroy, @instance
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
end
def clear_delivery_errors
authorize :delivery, :clear_delivery_errors?
-
@instance.delivery_failure_tracker.clear_failures!
redirect_to admin_instance_path(@instance.domain)
end
@@ -33,11 +31,9 @@ module Admin
def restart_delivery
authorize :delivery, :restart_delivery?
- last_unavailable_domain = unavailable_domain
-
- if last_unavailable_domain.present?
+ if @instance.unavailable?
@instance.delivery_failure_tracker.track_success!
- log_action :destroy, last_unavailable_domain
+ log_action :destroy, @instance.unavailable_domain
end
redirect_to admin_instance_path(@instance.domain)
@@ -45,8 +41,7 @@ module Admin
def stop_delivery
authorize :delivery, :stop_delivery?
-
- UnavailableDomain.create(domain: @instance.domain)
+ unavailable_domain = UnavailableDomain.create!(domain: @instance.domain)
log_action :create, unavailable_domain
redirect_to admin_instance_path(@instance.domain)
end
@@ -57,12 +52,11 @@ module Admin
@instance = Instance.find(params[:id])
end
- def set_exhausted_deliveries_days
- @exhausted_deliveries_days = @instance.delivery_failure_tracker.exhausted_deliveries_days
- end
-
def set_instances
@instances = filtered_instances.page(params[:page])
+ end
+
+ def preload_delivery_failures!
warning_domains_map = DeliveryFailureTracker.warning_domains_map
@instances.each do |instance|
@@ -70,10 +64,6 @@ module Admin
end
end
- def unavailable_domain
- UnavailableDomain.find_by(domain: @instance.domain)
- end
-
def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end
diff --git a/app/controllers/admin/relationships_controller.rb b/app/controllers/admin/relationships_controller.rb
index f8a95cfc8..085ded21c 100644
--- a/app/controllers/admin/relationships_controller.rb
+++ b/app/controllers/admin/relationships_controller.rb
@@ -9,7 +9,8 @@ module Admin
def index
authorize :account, :index?
- @accounts = RelationshipFilter.new(@account, filter_params).results.page(params[:page]).per(PER_PAGE)
+ @accounts = RelationshipFilter.new(@account, filter_params).results.includes(:account_stat, user: [:ips, :invite_request]).page(params[:page]).per(PER_PAGE)
+ @form = Form::AccountBatch.new
end
private
diff --git a/app/controllers/admin/reports/actions_controller.rb b/app/controllers/admin/reports/actions_controller.rb
new file mode 100644
index 000000000..5cb5c744f
--- /dev/null
+++ b/app/controllers/admin/reports/actions_controller.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class Admin::Reports::ActionsController < Admin::BaseController
+ before_action :set_report
+
+ def create
+ authorize @report, :show?
+
+ case action_from_button
+ when 'delete', 'mark_as_sensitive'
+ status_batch_action = Admin::StatusBatchAction.new(
+ type: action_from_button,
+ status_ids: @report.status_ids,
+ current_account: current_account,
+ report_id: @report.id,
+ send_email_notification: !@report.spam?
+ )
+
+ status_batch_action.save!
+ when 'silence', 'suspend'
+ account_action = Admin::AccountAction.new(
+ type: action_from_button,
+ report_id: @report.id,
+ target_account: @report.target_account,
+ current_account: current_account,
+ send_email_notification: !@report.spam?
+ )
+
+ account_action.save!
+ end
+
+ redirect_to admin_reports_path
+ end
+
+ private
+
+ def set_report
+ @report = Report.find(params[:report_id])
+ end
+
+ def action_from_button
+ if params[:delete]
+ 'delete'
+ elsif params[:mark_as_sensitive]
+ 'mark_as_sensitive'
+ elsif params[:silence]
+ 'silence'
+ elsif params[:suspend]
+ 'suspend'
+ end
+ end
+end
diff --git a/app/controllers/admin/statuses_controller.rb b/app/controllers/admin/statuses_controller.rb
index 8d039b281..817c0caa9 100644
--- a/app/controllers/admin/statuses_controller.rb
+++ b/app/controllers/admin/statuses_controller.rb
@@ -29,8 +29,9 @@ module Admin
end
def after_create_redirect_path
- if @status_batch_action.report_id.present?
- admin_report_path(@status_batch_action.report_id)
+ report_id = @status_batch_action&.report_id || params[:report_id]
+ if report_id.present?
+ admin_report_path(report_id)
else
admin_account_statuses_path(params[:account_id], current_params)
end
@@ -48,6 +49,10 @@ module Admin
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
end
+ def current_params
+ params.slice(:media, :page).permit(:media, :page)
+ end
+
def action_from_button
if params[:report]
'report'
diff --git a/app/controllers/admin/trends/links/preview_card_providers_controller.rb b/app/controllers/admin/trends/links/preview_card_providers_controller.rb
index 2c26e03f3..40a466cd6 100644
--- a/app/controllers/admin/trends/links/preview_card_providers_controller.rb
+++ b/app/controllers/admin/trends/links/preview_card_providers_controller.rb
@@ -5,11 +5,11 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
authorize :preview_card_provider, :index?
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
- @form = Form::PreviewCardProviderBatch.new
+ @form = Trends::PreviewCardProviderBatch.new
end
def batch
- @form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form = Trends::PreviewCardProviderBatch.new(trends_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
@@ -20,15 +20,15 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
private
def filtered_preview_card_providers
- PreviewCardProviderFilter.new(filter_params).results
+ Trends::PreviewCardProviderFilter.new(filter_params).results
end
def filter_params
- params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
+ params.slice(:page, *Trends::PreviewCardProviderFilter::KEYS).permit(:page, *Trends::PreviewCardProviderFilter::KEYS)
end
- def form_preview_card_provider_batch_params
- params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
+ def trends_preview_card_provider_batch_params
+ params.require(:trends_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
end
def action_from_button
diff --git a/app/controllers/admin/trends/links_controller.rb b/app/controllers/admin/trends/links_controller.rb
index 619b37deb..434eec5fe 100644
--- a/app/controllers/admin/trends/links_controller.rb
+++ b/app/controllers/admin/trends/links_controller.rb
@@ -5,11 +5,11 @@ class Admin::Trends::LinksController < Admin::BaseController
authorize :preview_card, :index?
@preview_cards = filtered_preview_cards.page(params[:page])
- @form = Form::PreviewCardBatch.new
+ @form = Trends::PreviewCardBatch.new
end
def batch
- @form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form = Trends::PreviewCardBatch.new(trends_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
@@ -20,26 +20,26 @@ class Admin::Trends::LinksController < Admin::BaseController
private
def filtered_preview_cards
- PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
+ Trends::PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
end
def filter_params
- params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
+ params.slice(:page, *Trends::PreviewCardFilter::KEYS).permit(:page, *Trends::PreviewCardFilter::KEYS)
end
- def form_preview_card_batch_params
- params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
+ def trends_preview_card_batch_params
+ params.require(:trends_preview_card_batch).permit(:action, preview_card_ids: [])
end
def action_from_button
if params[:approve]
'approve'
- elsif params[:approve_all]
- 'approve_all'
+ elsif params[:approve_providers]
+ 'approve_providers'
elsif params[:reject]
'reject'
- elsif params[:reject_all]
- 'reject_all'
+ elsif params[:reject_providers]
+ 'reject_providers'
end
end
end
diff --git a/app/controllers/admin/trends/statuses_controller.rb b/app/controllers/admin/trends/statuses_controller.rb
new file mode 100644
index 000000000..766242738
--- /dev/null
+++ b/app/controllers/admin/trends/statuses_controller.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class Admin::Trends::StatusesController < Admin::BaseController
+ def index
+ authorize :status, :index?
+
+ @statuses = filtered_statuses.page(params[:page])
+ @form = Trends::StatusBatch.new
+ end
+
+ def batch
+ @form = Trends::StatusBatch.new(trends_status_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form.save
+ rescue ActionController::ParameterMissing
+ flash[:alert] = I18n.t('admin.accounts.no_account_selected')
+ ensure
+ redirect_to admin_trends_statuses_path(filter_params)
+ end
+
+ private
+
+ def filtered_statuses
+ Trends::StatusFilter.new(filter_params.with_defaults(trending: 'all')).results.includes(:account, :media_attachments, :active_mentions)
+ end
+
+ def filter_params
+ params.slice(:page, *Trends::StatusFilter::KEYS).permit(:page, *Trends::StatusFilter::KEYS)
+ end
+
+ def trends_status_batch_params
+ params.require(:trends_status_batch).permit(:action, status_ids: [])
+ end
+
+ def action_from_button
+ if params[:approve]
+ 'approve'
+ elsif params[:approve_accounts]
+ 'approve_accounts'
+ elsif params[:reject]
+ 'reject'
+ elsif params[:reject_accounts]
+ 'reject_accounts'
+ end
+ end
+end
diff --git a/app/controllers/admin/trends/tags_controller.rb b/app/controllers/admin/trends/tags_controller.rb
index 91ff33d40..f4d1ec0d1 100644
--- a/app/controllers/admin/trends/tags_controller.rb
+++ b/app/controllers/admin/trends/tags_controller.rb
@@ -5,11 +5,11 @@ class Admin::Trends::TagsController < Admin::BaseController
authorize :tag, :index?
@tags = filtered_tags.page(params[:page])
- @form = Form::TagBatch.new
+ @form = Trends::TagBatch.new
end
def batch
- @form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
+ @form = Trends::TagBatch.new(trends_tag_batch_params.merge(current_account: current_account, action: action_from_button))
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
@@ -20,15 +20,15 @@ class Admin::Trends::TagsController < Admin::BaseController
private
def filtered_tags
- TagFilter.new(filter_params).results
+ Trends::TagFilter.new(filter_params).results
end
def filter_params
- params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
+ params.slice(:page, *Trends::TagFilter::KEYS).permit(:page, *Trends::TagFilter::KEYS)
end
- def form_tag_batch_params
- params.require(:form_tag_batch).permit(:action, tag_ids: [])
+ def trends_tag_batch_params
+ params.require(:trends_tag_batch).permit(:action, tag_ids: [])
end
def action_from_button
diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index b863d8643..72c30dec7 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -5,6 +5,7 @@ class Api::BaseController < ApplicationController
DEFAULT_ACCOUNTS_LIMIT = 40
include RateLimitHeaders
+ include AccessTokenTrackingConcern
skip_before_action :store_current_location
skip_before_action :require_functional!, unless: :whitelist_mode?
diff --git a/app/controllers/api/v1/accounts/familiar_followers_controller.rb b/app/controllers/api/v1/accounts/familiar_followers_controller.rb
new file mode 100644
index 000000000..b0bd8018a
--- /dev/null
+++ b/app/controllers/api/v1/accounts/familiar_followers_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
+ before_action -> { doorkeeper_authorize! :read, :'read:follows' }
+ before_action :require_user!
+ before_action :set_accounts
+
+ def index
+ render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer
+ end
+
+ private
+
+ def set_accounts
+ @accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact
+ end
+
+ def familiar_followers
+ FamiliarFollowersPresenter.new(@accounts, current_user.account_id)
+ end
+
+ def account_ids
+ Array(params[:id]).map(&:to_i)
+ end
+end
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 2c027ea76..38c9f5a20 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -22,53 +22,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
end
def cached_account_statuses
- statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
-
- statuses.merge!(only_media_scope) if truthy_param?(:only_media)
- statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
- statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
- statuses.merge!(hashtag_scope) if params[:tagged].present?
-
cache_collection_paginated_by_id(
- statuses,
+ AccountStatusesFilter.new(@account, current_account, params).results,
Status,
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end
- def permitted_account_statuses
- @account.statuses.permitted_for(@account, current_account)
- end
-
- def only_media_scope
- Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
- end
-
- def pinned_scope
- @account.pinned_statuses.permitted_for(@account, current_account)
- end
-
- def no_replies_scope
- Status.without_replies
- end
-
- def no_reblogs_scope
- Status.without_reblogs
- end
-
- def hashtag_scope
- tag = Tag.find_normalized(params[:tagged])
-
- if tag
- Status.tagged_with(tag.id)
- else
- Status.none
- end
- end
-
def pagination_params(core_params)
- params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
+ params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end
def insert_pagination_headers
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 5c47158e0..5134bfb94 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -2,9 +2,9 @@
class Api::V1::AccountsController < Api::BaseController
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
- before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
- before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
- before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
+ before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
+ before_action -> { doorkeeper_authorize! :follow, :write, :'write:mutes' }, only: [:mute, :unmute]
+ before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, only: [:block, :unblock]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
before_action :require_user!, except: [:show, :create]
diff --git a/app/controllers/api/v1/admin/trends/links_controller.rb b/app/controllers/api/v1/admin/trends/links_controller.rb
new file mode 100644
index 000000000..63b3d9358
--- /dev/null
+++ b/app/controllers/api/v1/admin/trends/links_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::Trends::LinksController < Api::BaseController
+ protect_from_forgery with: :exception
+
+ before_action -> { authorize_if_got_token! :'admin:read' }
+ before_action :require_staff!
+ before_action :set_links
+
+ def index
+ render json: @links, each_serializer: REST::Trends::LinkSerializer
+ end
+
+ private
+
+ def set_links
+ @links = Trends.links.query.limit(limit_param(10))
+ end
+end
diff --git a/app/controllers/api/v1/admin/trends/statuses_controller.rb b/app/controllers/api/v1/admin/trends/statuses_controller.rb
new file mode 100644
index 000000000..86633cc74
--- /dev/null
+++ b/app/controllers/api/v1/admin/trends/statuses_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Api::V1::Admin::Trends::StatusesController < Api::BaseController
+ protect_from_forgery with: :exception
+
+ before_action -> { authorize_if_got_token! :'admin:read' }
+ before_action :require_staff!
+ before_action :set_statuses
+
+ def index
+ render json: @statuses, each_serializer: REST::StatusSerializer
+ end
+
+ private
+
+ def set_statuses
+ @statuses = cache_collection(Trends.statuses.query.limit(limit_param(DEFAULT_STATUSES_LIMIT)), Status)
+ end
+end
diff --git a/app/controllers/api/v1/admin/trends/tags_controller.rb b/app/controllers/api/v1/admin/trends/tags_controller.rb
index 4815af31e..5cc4c269d 100644
--- a/app/controllers/api/v1/admin/trends/tags_controller.rb
+++ b/app/controllers/api/v1/admin/trends/tags_controller.rb
@@ -14,6 +14,6 @@ class Api::V1::Admin::Trends::TagsController < Api::BaseController
private
def set_tags
- @tags = Trends.tags.get(false, limit_param(10))
+ @tags = Trends.tags.query.limit(limit_param(10))
end
end
diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb
index 586cdfca9..a65e762c9 100644
--- a/app/controllers/api/v1/blocks_controller.rb
+++ b/app/controllers/api/v1/blocks_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::BlocksController < Api::BaseController
- before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }
+ before_action -> { doorkeeper_authorize! :follow, :read, :'read:blocks' }
before_action :require_user!
after_action :insert_pagination_headers
diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb
index 5bb02d834..1891261b9 100644
--- a/app/controllers/api/v1/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/domain_blocks_controller.rb
@@ -3,8 +3,8 @@
class Api::V1::DomainBlocksController < Api::BaseController
BLOCK_LIMIT = 100
- before_action -> { doorkeeper_authorize! :follow, :'read:blocks' }, only: :show
- before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, except: :show
+ before_action -> { doorkeeper_authorize! :follow, :read, :'read:blocks' }, only: :show
+ before_action -> { doorkeeper_authorize! :follow, :write, :'write:blocks' }, except: :show
before_action :require_user!
after_action :insert_pagination_headers, only: :show
diff --git a/app/controllers/api/v1/emails/confirmations_controller.rb b/app/controllers/api/v1/emails/confirmations_controller.rb
index f1d9954d0..3faaea2fb 100644
--- a/app/controllers/api/v1/emails/confirmations_controller.rb
+++ b/app/controllers/api/v1/emails/confirmations_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::Emails::ConfirmationsController < Api::BaseController
- before_action :doorkeeper_authorize!
+ before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
before_action :require_user_owned_by_application!
before_action :require_user_not_confirmed!
@@ -19,6 +19,6 @@ class Api::V1::Emails::ConfirmationsController < Api::BaseController
end
def require_user_not_confirmed!
- render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden if current_user.confirmed? || current_user.unconfirmed_email.blank?
+ render json: { error: 'This method is only available while the e-mail is awaiting confirmation' }, status: :forbidden unless !current_user.confirmed? || current_user.unconfirmed_email.present?
end
end
diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb
index f4b2a74d0..54ff0e11d 100644
--- a/app/controllers/api/v1/follow_requests_controller.rb
+++ b/app/controllers/api/v1/follow_requests_controller.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class Api::V1::FollowRequestsController < Api::BaseController
- before_action -> { doorkeeper_authorize! :follow, :'read:follows' }, only: :index
- before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, except: :index
+ before_action -> { doorkeeper_authorize! :follow, :read, :'read:follows' }, only: :index
+ before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, except: :index
before_action :require_user!
after_action :insert_pagination_headers, only: :index
@@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
def authorize
AuthorizeFollowService.new.call(account, current_account)
- NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
+ LocalNotificationWorker.perform_async(current_account.id, Follow.find_by(account: account, target_account: current_account).id, 'Follow', 'follow')
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
end
diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb
index a2a919a3e..f9c935bf3 100644
--- a/app/controllers/api/v1/media_controller.rb
+++ b/app/controllers/api/v1/media_controller.rb
@@ -20,7 +20,7 @@ class Api::V1::MediaController < Api::BaseController
end
def update
- @media_attachment.update!(media_attachment_params)
+ @media_attachment.update!(updateable_media_attachment_params)
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
@@ -31,7 +31,7 @@ class Api::V1::MediaController < Api::BaseController
end
def set_media_attachment
- @media_attachment = current_account.media_attachments.unattached.find(params[:id])
+ @media_attachment = current_account.media_attachments.where(status_id: nil).find(params[:id])
end
def check_processing
@@ -42,6 +42,10 @@ class Api::V1::MediaController < Api::BaseController
params.permit(:file, :thumbnail, :description, :focus)
end
+ def updateable_media_attachment_params
+ params.permit(:thumbnail, :description, :focus)
+ end
+
def file_type_error
{ error: 'File type of uploaded media could not be verified' }
end
diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb
index fd52511d7..6cde53a2a 100644
--- a/app/controllers/api/v1/mutes_controller.rb
+++ b/app/controllers/api/v1/mutes_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Api::V1::MutesController < Api::BaseController
- before_action -> { doorkeeper_authorize! :follow, :'read:mutes' }
+ before_action -> { doorkeeper_authorize! :follow, :read, :'read:mutes' }
before_action :require_user!
after_action :insert_pagination_headers
diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb
index f9d780839..6d464997e 100644
--- a/app/controllers/api/v1/notifications_controller.rb
+++ b/app/controllers/api/v1/notifications_controller.rb
@@ -35,13 +35,18 @@ class Api::V1::NotificationsController < Api::BaseController
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
+
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
cache_collection(target_statuses, Status)
end
end
def browserable_account_notifications
- current_account.notifications.without_suspended.browserable(exclude_types, from_account)
+ current_account.notifications.without_suspended.browserable(
+ types: Array(browserable_params[:types]),
+ exclude_types: Array(browserable_params[:exclude_types]),
+ from_account_id: browserable_params[:account_id]
+ )
end
def target_statuses_from_notifications
@@ -72,17 +77,11 @@ class Api::V1::NotificationsController < Api::BaseController
@notifications.first.id
end
- def exclude_types
- val = params.permit(exclude_types: [])[:exclude_types] || []
- val = [val] unless val.is_a?(Enumerable)
- val
- end
-
- def from_account
- params[:account_id]
+ def browserable_params
+ params.permit(:account_id, types: [], exclude_types: [])
end
def pagination_params(core_params)
- params.slice(:limit, :exclude_types).permit(:limit, exclude_types: []).merge(core_params)
+ params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params)
end
end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index e10083d45..8ff6c8fe5 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -10,9 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
@report = ReportService.new.call(
current_account,
reported_account,
- status_ids: reported_status_ids,
- comment: report_params[:comment],
- forward: report_params[:forward]
+ report_params
)
render json: @report, serializer: REST::ReportSerializer
@@ -20,19 +18,11 @@ class Api::V1::ReportsController < Api::BaseController
private
- def reported_status_ids
- reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
- end
-
- def status_ids
- Array(report_params[:status_ids])
- end
-
def reported_account
Account.find(report_params[:account_id])
end
def report_params
- params.permit(:account_id, :comment, :forward, status_ids: [])
+ params.permit(:account_id, :comment, :category, :forward, status_ids: [], rule_ids: [])
end
end
diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb
index c2c1fac5d..7fe73a6f5 100644
--- a/app/controllers/api/v1/statuses/histories_controller.rb
+++ b/app/controllers/api/v1/statuses/histories_controller.rb
@@ -7,7 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
before_action :set_status
def show
- render json: @status.edits, each_serializer: REST::StatusEditSerializer
+ render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
end
private
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 98b1776ea..3fe137bfd 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -3,13 +3,14 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
- before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
- before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
+ before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
+ before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
override_rate_limit_headers :create, family: :statuses
+ override_rate_limit_headers :update, family: :statuses
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
@@ -35,24 +36,44 @@ class Api::V1::StatusesController < Api::BaseController
end
def create
- @status = PostStatusService.new.call(current_user.account,
- text: status_params[:status],
- thread: @thread,
- media_ids: status_params[:media_ids],
- sensitive: status_params[:sensitive],
- spoiler_text: status_params[:spoiler_text],
- visibility: status_params[:visibility],
- scheduled_at: status_params[:scheduled_at],
- application: doorkeeper_token.application,
- poll: status_params[:poll],
- idempotency: request.headers['Idempotency-Key'],
- with_rate_limit: true)
+ @status = PostStatusService.new.call(
+ current_user.account,
+ text: status_params[:status],
+ thread: @thread,
+ media_ids: status_params[:media_ids],
+ sensitive: status_params[:sensitive],
+ spoiler_text: status_params[:spoiler_text],
+ visibility: status_params[:visibility],
+ language: status_params[:language],
+ scheduled_at: status_params[:scheduled_at],
+ application: doorkeeper_token.application,
+ poll: status_params[:poll],
+ idempotency: request.headers['Idempotency-Key'],
+ with_rate_limit: true
+ )
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
+ def update
+ @status = Status.where(account: current_account).find(params[:id])
+ authorize @status, :update?
+
+ UpdateStatusService.new.call(
+ @status,
+ current_account.id,
+ text: status_params[:status],
+ media_ids: status_params[:media_ids],
+ sensitive: status_params[:sensitive],
+ spoiler_text: status_params[:spoiler_text],
+ poll: status_params[:poll]
+ )
+
+ render json: @status, serializer: REST::StatusSerializer
+ end
+
def destroy
- @status = Status.where(account_id: current_user.account).find(params[:id])
+ @status = Status.where(account: current_account).find(params[:id])
authorize @status, :destroy?
@status.discard
@@ -72,8 +93,9 @@ class Api::V1::StatusesController < Api::BaseController
end
def set_thread
- @thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
- rescue ActiveRecord::RecordNotFound
+ @thread = Status.find(status_params[:in_reply_to_id]) if status_params[:in_reply_to_id].present?
+ authorize(@thread, :show?) if @thread.present?
+ rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end
@@ -84,6 +106,7 @@ class Api::V1::StatusesController < Api::BaseController
:sensitive,
:spoiler_text,
:visibility,
+ :language,
:scheduled_at,
media_ids: [],
poll: [
diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb
index 1c3ab1e1c..ad20e7f8b 100644
--- a/app/controllers/api/v1/trends/links_controller.rb
+++ b/app/controllers/api/v1/trends/links_controller.rb
@@ -12,10 +12,14 @@ class Api::V1::Trends::LinksController < Api::BaseController
def set_links
@links = begin
if Setting.trends
- Trends.links.get(true, limit_param(10))
+ links_from_trends
else
[]
end
end
end
+
+ def links_from_trends
+ Trends.links.query.allowed.in_locale(content_locale).limit(limit_param(10))
+ end
end
diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb
new file mode 100644
index 000000000..d4ec97ae5
--- /dev/null
+++ b/app/controllers/api/v1/trends/statuses_controller.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Api::V1::Trends::StatusesController < Api::BaseController
+ before_action :set_statuses
+
+ def index
+ render json: @statuses, each_serializer: REST::StatusSerializer
+ end
+
+ private
+
+ def set_statuses
+ @statuses = begin
+ if Setting.trends
+ cache_collection(statuses_from_trends, Status)
+ else
+ []
+ end
+ end
+ end
+
+ def statuses_from_trends
+ scope = Trends.statuses.query.allowed.in_locale(content_locale)
+ scope = scope.filtered_for(current_account) if user_signed_in?
+ scope.limit(limit_param(DEFAULT_STATUSES_LIMIT))
+ end
+end
diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb
index 947b53de2..1334b72d2 100644
--- a/app/controllers/api/v1/trends/tags_controller.rb
+++ b/app/controllers/api/v1/trends/tags_controller.rb
@@ -12,7 +12,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
def set_tags
@tags = begin
if Setting.trends
- Trends.tags.get(true, limit_param(10))
+ Trends.tags.query.allowed.limit(limit_param(10))
else
[]
end
diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb
index bed57fc54..5167928e9 100644
--- a/app/controllers/api/web/push_subscriptions_controller.rb
+++ b/app/controllers/api/web/push_subscriptions_controller.rb
@@ -17,16 +17,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
data = {
policy: 'all',
-
- alerts: {
- follow: alerts_enabled,
- follow_request: alerts_enabled,
- favourite: alerts_enabled,
- reblog: alerts_enabled,
- mention: alerts_enabled,
- poll: alerts_enabled,
- status: alerts_enabled,
- },
+ alerts: Notification::TYPES.index_with { alerts_enabled },
}
data.deep_merge!(data_params) if params[:data]
@@ -61,6 +52,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def data_params
- @data_params ||= params.require(:data).permit(:policy, alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
+ @data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES)
end
end
diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb
index 991a50b03..f9cf6d655 100644
--- a/app/controllers/auth/omniauth_callbacks_controller.rb
+++ b/app/controllers/auth/omniauth_callbacks_controller.rb
@@ -4,8 +4,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
skip_before_action :verify_authenticity_token
def self.provides_callback_for(provider)
- provider_id = provider.to_s.chomp '_oauth2'
-
define_method provider do
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
@@ -20,7 +18,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
)
sign_in_and_redirect @user, event: :authentication
- set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
+ set_flash_message(:notice, :success, kind: Devise.omniauth_configs[provider].strategy.display_name.capitalize) if is_navigational_format?
else
session["devise.#{provider}_data"] = request.env['omniauth.auth']
redirect_to new_user_registration_url
@@ -33,7 +31,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def after_sign_in_path_for(resource)
- if resource.email_verified?
+ if resource.email_present?
root_path
else
auth_setup_path(missing_email: '1')
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index f37e906fd..1c3adbd78 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -9,6 +9,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :check_enabled_registrations, only: [:new, :create]
before_action :configure_sign_up_params, only: [:create]
before_action :set_sessions, only: [:edit, :update]
+ before_action :set_strikes, only: [:edit, :update]
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
@@ -111,8 +112,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_invite
- invite = invite_code.present? ? Invite.find_by(code: invite_code) : nil
- @invite = invite&.valid_for_use? ? invite : nil
+ @invite = begin
+ invite = Invite.find_by(code: invite_code) if invite_code.present?
+ invite if invite&.valid_for_use?
+ end
end
def determine_layout
@@ -123,6 +126,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController
@sessions = current_user.session_activations
end
+ def set_strikes
+ @strikes = current_account.strikes.recent.latest
+ end
+
def require_not_suspended!
forbidden if current_account.suspended?
end
diff --git a/app/controllers/concerns/access_token_tracking_concern.rb b/app/controllers/concerns/access_token_tracking_concern.rb
new file mode 100644
index 000000000..cf60cfb99
--- /dev/null
+++ b/app/controllers/concerns/access_token_tracking_concern.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module AccessTokenTrackingConcern
+ extend ActiveSupport::Concern
+
+ ACCESS_TOKEN_UPDATE_FREQUENCY = 24.hours.freeze
+
+ included do
+ before_action :update_access_token_last_used
+ end
+
+ private
+
+ def update_access_token_last_used
+ doorkeeper_token.update_last_used(request) if access_token_needs_update?
+ end
+
+ def access_token_needs_update?
+ doorkeeper_token.present? && (doorkeeper_token.last_used_at.nil? || doorkeeper_token.last_used_at < ACCESS_TOKEN_UPDATE_FREQUENCY.ago)
+ end
+end
diff --git a/app/controllers/concerns/authorization.rb b/app/controllers/concerns/authorization.rb
index 95a37e379..05260cc8b 100644
--- a/app/controllers/concerns/authorization.rb
+++ b/app/controllers/concerns/authorization.rb
@@ -3,7 +3,7 @@
module Authorization
extend ActiveSupport::Concern
- include Pundit
+ include Pundit::Authorization
def pundit_user
current_account
diff --git a/app/controllers/concerns/localized.rb b/app/controllers/concerns/localized.rb
index fe1142f34..ede299d5a 100644
--- a/app/controllers/concerns/localized.rb
+++ b/app/controllers/concerns/localized.rb
@@ -7,27 +7,28 @@ module Localized
around_action :set_locale
end
- def set_locale
- locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
- locale ||= session[:locale] ||= default_locale
- locale = default_locale unless I18n.available_locales.include?(locale.to_sym)
-
- I18n.with_locale(locale) do
- yield
- end
+ def set_locale(&block)
+ I18n.with_locale(requested_locale || I18n.default_locale, &block)
end
private
- def default_locale
- if ENV['DEFAULT_LOCALE'].present?
- I18n.default_locale
- else
- request_locale || I18n.default_locale
- end
+ def requested_locale
+ requested_locale_name = available_locale_or_nil(params[:lang])
+ requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
+ requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
+ requested_locale_name
end
- def request_locale
- http_accept_language.language_region_compatible_from(I18n.available_locales)
+ def http_accept_language
+ HttpAcceptLanguage::Parser.new(request.headers.fetch('Accept-Language')).language_region_compatible_from(I18n.available_locales) if request.headers.key?('Accept-Language')
+ end
+
+ def available_locale_or_nil(locale_name)
+ locale_name.to_sym if locale_name.present? && I18n.available_locales.include?(locale_name.to_sym)
+ end
+
+ def content_locale
+ @content_locale ||= I18n.locale.to_s.split(/[_-]/).first
end
end
diff --git a/app/controllers/concerns/session_tracking_concern.rb b/app/controllers/concerns/session_tracking_concern.rb
index 45361b019..eaaa4ac59 100644
--- a/app/controllers/concerns/session_tracking_concern.rb
+++ b/app/controllers/concerns/session_tracking_concern.rb
@@ -3,7 +3,7 @@
module SessionTrackingConcern
extend ActiveSupport::Concern
- UPDATE_SIGN_IN_HOURS = 24
+ SESSION_UPDATE_FREQUENCY = 24.hours.freeze
included do
before_action :set_session_activity
@@ -17,6 +17,6 @@ module SessionTrackingConcern
end
def session_needs_update?
- !current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
+ !current_session.nil? && current_session.updated_at < SESSION_UPDATE_FREQUENCY.ago
end
end
diff --git a/app/controllers/concerns/user_tracking_concern.rb b/app/controllers/concerns/user_tracking_concern.rb
index 45f3aab0d..e960cce53 100644
--- a/app/controllers/concerns/user_tracking_concern.rb
+++ b/app/controllers/concerns/user_tracking_concern.rb
@@ -3,7 +3,7 @@
module UserTrackingConcern
extend ActiveSupport::Concern
- UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
+ SIGN_IN_UPDATE_FREQUENCY = 24.hours.freeze
included do
before_action :update_user_sign_in
@@ -16,6 +16,6 @@ module UserTrackingConcern
end
def user_needs_sign_in_update?
- user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago)
+ user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < SIGN_IN_UPDATE_FREQUENCY.ago)
end
end
diff --git a/app/controllers/disputes/appeals_controller.rb b/app/controllers/disputes/appeals_controller.rb
new file mode 100644
index 000000000..eefd92b5a
--- /dev/null
+++ b/app/controllers/disputes/appeals_controller.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class Disputes::AppealsController < Disputes::BaseController
+ before_action :set_strike
+
+ def create
+ authorize @strike, :appeal?
+
+ @appeal = AppealService.new.call(@strike, appeal_params[:text])
+
+ redirect_to disputes_strike_path(@strike), notice: I18n.t('disputes.strikes.appealed_msg')
+ rescue ActiveRecord::RecordInvalid => e
+ @appeal = e.record
+ render template: 'disputes/strikes/show'
+ end
+
+ private
+
+ def set_strike
+ @strike = current_account.strikes.find(params[:strike_id])
+ end
+
+ def appeal_params
+ params.require(:appeal).permit(:text)
+ end
+end
diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb
new file mode 100644
index 000000000..865146b5c
--- /dev/null
+++ b/app/controllers/disputes/base_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Disputes::BaseController < ApplicationController
+ include Authorization
+
+ layout 'admin'
+
+ skip_before_action :require_functional!
+
+ before_action :set_body_classes
+ before_action :authenticate_user!
+
+ private
+
+ def set_body_classes
+ @body_classes = 'admin'
+ end
+end
diff --git a/app/controllers/disputes/strikes_controller.rb b/app/controllers/disputes/strikes_controller.rb
new file mode 100644
index 000000000..d85dcb4d5
--- /dev/null
+++ b/app/controllers/disputes/strikes_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Disputes::StrikesController < Disputes::BaseController
+ before_action :set_strike, only: [:show]
+
+ def index
+ @strikes = current_account.strikes.latest
+ end
+
+ def show
+ authorize @strike, :show?
+
+ @appeal = @strike.appeal || @strike.build_appeal
+ end
+
+ private
+
+ def set_strike
+ @strike = AccountWarning.find(params[:id])
+ end
+end
diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb
index b3589a39f..f3f8336c9 100644
--- a/app/controllers/follower_accounts_controller.rb
+++ b/app/controllers/follower_accounts_controller.rb
@@ -15,13 +15,13 @@ class FollowerAccountsController < ApplicationController
format.html do
expires_in 0, public: true unless user_signed_in?
- next if @account.user_hides_network?
+ next if @account.hide_collections?
follows
end
format.json do
- raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
+ raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
@@ -82,7 +82,7 @@ class FollowerAccountsController < ApplicationController
end
def restrict_fields_to
- if page_requested? || !@account.user_hides_network?
+ if page_requested? || !@account.hide_collections?
# Return all fields
else
%i(id type total_items)
diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb
index 8a72dc475..9d7f4c9bf 100644
--- a/app/controllers/following_accounts_controller.rb
+++ b/app/controllers/following_accounts_controller.rb
@@ -15,13 +15,13 @@ class FollowingAccountsController < ApplicationController
format.html do
expires_in 0, public: true unless user_signed_in?
- next if @account.user_hides_network?
+ next if @account.hide_collections?
follows
end
format.json do
- raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
+ raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
@@ -82,7 +82,7 @@ class FollowingAccountsController < ApplicationController
end
def restrict_fields_to
- if page_requested? || !@account.user_hides_network?
+ if page_requested? || !@account.hide_collections?
# Return all fields
else
%i(id type total_items)
diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb
index 32b5d7948..c7492700c 100644
--- a/app/controllers/settings/preferences_controller.rb
+++ b/app/controllers/settings/preferences_controller.rb
@@ -47,7 +47,6 @@ class Settings::PreferencesController < Settings::BaseController
:setting_system_font_ui,
:setting_noindex,
:setting_theme,
- :setting_hide_network,
:setting_aggregate_reblogs,
:setting_show_application,
:setting_advanced_layout,
diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb
index 0c15447a6..be5b4f302 100644
--- a/app/controllers/settings/profiles_controller.rb
+++ b/app/controllers/settings/profiles_controller.rb
@@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
- params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
+ params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value])
end
def set_account
diff --git a/app/helpers/admin/account_moderation_notes_helper.rb b/app/helpers/admin/account_moderation_notes_helper.rb
index 40b2a5289..2f08538ca 100644
--- a/app/helpers/admin/account_moderation_notes_helper.rb
+++ b/app/helpers/admin/account_moderation_notes_helper.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module Admin::AccountModerationNotesHelper
- def admin_account_link_to(account)
+ def admin_account_link_to(account, path: nil)
return if account.nil?
- link_to admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
+ link_to path || admin_account_path(account.id), class: name_tag_classes(account), title: account.acct do
safe_join([
image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'),
content_tag(:span, account.acct, class: 'username'),
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index f3aa4be4f..47eeeaac3 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -33,6 +33,8 @@ module Admin::ActionLogsHelper
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
when 'Instance'
record.domain
+ when 'Appeal'
+ link_to record.account.acct, disputes_strike_path(record.strike)
end
end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 907529b37..140fc73ed 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -5,9 +5,10 @@ module Admin::FilterHelper
AccountFilter::KEYS,
CustomEmojiFilter::KEYS,
ReportFilter::KEYS,
- TagFilter::KEYS,
- PreviewCardProviderFilter::KEYS,
- PreviewCardFilter::KEYS,
+ Trends::TagFilter::KEYS,
+ Trends::PreviewCardProviderFilter::KEYS,
+ Trends::PreviewCardFilter::KEYS,
+ Trends::StatusFilter::KEYS,
InstanceFilter::KEYS,
InviteFilter::KEYS,
RelationshipFilter::KEYS,
diff --git a/app/helpers/admin/trends/statuses_helper.rb b/app/helpers/admin/trends/statuses_helper.rb
new file mode 100644
index 000000000..d16e3dd12
--- /dev/null
+++ b/app/helpers/admin/trends/statuses_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Admin::Trends::StatusesHelper
+ def one_line_preview(status)
+ text = begin
+ if status.local?
+ status.text.split("\n").first
+ else
+ Nokogiri::HTML(status.text).css('html > body > *').first&.text
+ end
+ end
+
+ return '' if text.blank?
+
+ html = Formatter.instance.send(:encode, text)
+ html = Formatter.instance.send(:encode_custom_emojis, html, status.emojis, prefers_autoplay?)
+
+ html.html_safe # rubocop:disable Rails/OutputSafety
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 36c66b7d1..c5d9bbc19 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -224,4 +224,19 @@ module ApplicationHelper
content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json')
# rubocop:enable Rails/OutputSafety
end
+
+ def grouped_scopes(scopes)
+ scope_parser = ScopeParser.new
+ scope_transformer = ScopeTransformer.new
+
+ scopes.each_with_object({}) do |str, h|
+ scope = scope_transformer.apply(scope_parser.parse(str))
+
+ if h[scope.key]
+ h[scope.key].merge!(scope)
+ else
+ h[scope.key] = scope
+ end
+ end.values
+ end
end
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index c6557817d..102e4b132 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -15,6 +15,14 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value
end
+ def uri_from_bearcap(str)
+ if str&.start_with?('bear:')
+ Addressable::URI.parse(str).query_values['u']
+ else
+ str
+ end
+ end
+
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
@@ -54,7 +62,7 @@ module JsonLdHelper
end
def unsupported_uri_scheme?(uri)
- !uri.start_with?('http://', 'https://')
+ uri.nil? || !uri.start_with?('http://', 'https://')
end
def invalid_origin?(url)
diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb
index 730724208..9be35ad8e 100644
--- a/app/helpers/languages_helper.rb
+++ b/app/helpers/languages_helper.rb
@@ -1,94 +1,256 @@
# frozen_string_literal: true
module LanguagesHelper
- HUMAN_LOCALES = {
- af: 'Afrikaans',
- ar: 'العربية',
- ast: 'Asturianu',
- bg: 'Български',
- bn: 'বাংলা',
- br: 'Breton',
- ca: 'Català',
- co: 'Corsu',
- cs: 'Čeština',
- cy: 'Cymraeg',
- da: 'Dansk',
- de: 'Deutsch',
- el: 'Ελληνικά',
- en: 'English',
- eo: 'Esperanto',
+ ISO_639_1 = {
+ aa: ['Afar', 'Afaraf'].freeze,
+ ab: ['Abkhaz', 'аҧсуа бызшәа'].freeze,
+ ae: ['Avestan', 'avesta'].freeze,
+ af: ['Afrikaans', 'Afrikaans'].freeze,
+ ak: ['Akan', 'Akan'].freeze,
+ am: ['Amharic', 'አማርኛ'].freeze,
+ an: ['Aragonese', 'aragonés'].freeze,
+ ar: ['Arabic', 'اللغة العربية'].freeze,
+ as: ['Assamese', 'অসমীয়া'].freeze,
+ av: ['Avaric', 'авар мацӀ'].freeze,
+ ay: ['Aymara', 'aymar aru'].freeze,
+ az: ['Azerbaijani', 'azərbaycan dili'].freeze,
+ ba: ['Bashkir', 'башҡорт теле'].freeze,
+ be: ['Belarusian', 'беларуская мова'].freeze,
+ bg: ['Bulgarian', 'български език'].freeze,
+ bh: ['Bihari', 'भोजपुरी'].freeze,
+ bi: ['Bislama', 'Bislama'].freeze,
+ bm: ['Bambara', 'bamanankan'].freeze,
+ bn: ['Bengali', 'বাংলা'].freeze,
+ bo: ['Tibetan', 'བོད་ཡིག'].freeze,
+ br: ['Breton', 'brezhoneg'].freeze,
+ bs: ['Bosnian', 'bosanski jezik'].freeze,
+ ca: ['Catalan', 'Català'].freeze,
+ ce: ['Chechen', 'нохчийн мотт'].freeze,
+ ch: ['Chamorro', 'Chamoru'].freeze,
+ co: ['Corsican', 'corsu'].freeze,
+ cr: ['Cree', 'ᓀᐦᐃᔭᐍᐏᐣ'].freeze,
+ cs: ['Czech', 'čeština'].freeze,
+ cu: ['Old Church Slavonic', 'ѩзыкъ словѣньскъ'].freeze,
+ cv: ['Chuvash', 'чӑваш чӗлхи'].freeze,
+ cy: ['Welsh', 'Cymraeg'].freeze,
+ da: ['Danish', 'dansk'].freeze,
+ de: ['German', 'Deutsch'].freeze,
+ dv: ['Divehi', 'Dhivehi'].freeze,
+ dz: ['Dzongkha', 'རྫོང་ཁ'].freeze,
+ ee: ['Ewe', 'Eʋegbe'].freeze,
+ el: ['Greek', 'Ελληνικά'].freeze,
+ en: ['English', 'English'].freeze,
+ eo: ['Esperanto', 'Esperanto'].freeze,
+ es: ['Spanish', 'Español'].freeze,
+ et: ['Estonian', 'eesti'].freeze,
+ eu: ['Basque', 'euskara'].freeze,
+ fa: ['Persian', 'فارسی'].freeze,
+ ff: ['Fula', 'Fulfulde'].freeze,
+ fi: ['Finnish', 'suomi'].freeze,
+ fj: ['Fijian', 'Vakaviti'].freeze,
+ fo: ['Faroese', 'føroyskt'].freeze,
+ fr: ['French', 'Français'].freeze,
+ fy: ['Western Frisian', 'Frysk'].freeze,
+ ga: ['Irish', 'Gaeilge'].freeze,
+ gd: ['Scottish Gaelic', 'Gàidhlig'].freeze,
+ gl: ['Galician', 'galego'].freeze,
+ gu: ['Gujarati', 'ગુજરાતી'].freeze,
+ gv: ['Manx', 'Gaelg'].freeze,
+ ha: ['Hausa', 'هَوُسَ'].freeze,
+ he: ['Hebrew', 'עברית'].freeze,
+ hi: ['Hindi', 'हिन्दी'].freeze,
+ ho: ['Hiri Motu', 'Hiri Motu'].freeze,
+ hr: ['Croatian', 'Hrvatski'].freeze,
+ ht: ['Haitian', 'Kreyòl ayisyen'].freeze,
+ hu: ['Hungarian', 'magyar'].freeze,
+ hy: ['Armenian', 'Հայերեն'].freeze,
+ hz: ['Herero', 'Otjiherero'].freeze,
+ ia: ['Interlingua', 'Interlingua'].freeze,
+ id: ['Indonesian', 'Bahasa Indonesia'].freeze,
+ ie: ['Interlingue', 'Interlingue'].freeze,
+ ig: ['Igbo', 'Asụsụ Igbo'].freeze,
+ ii: ['Nuosu', 'ꆈꌠ꒿ Nuosuhxop'].freeze,
+ ik: ['Inupiaq', 'Iñupiaq'].freeze,
+ io: ['Ido', 'Ido'].freeze,
+ is: ['Icelandic', 'Íslenska'].freeze,
+ it: ['Italian', 'Italiano'].freeze,
+ iu: ['Inuktitut', 'ᐃᓄᒃᑎᑐᑦ'].freeze,
+ ja: ['Japanese', '日本語'].freeze,
+ jv: ['Javanese', 'basa Jawa'].freeze,
+ ka: ['Georgian', 'ქართული'].freeze,
+ kg: ['Kongo', 'Kikongo'].freeze,
+ ki: ['Kikuyu', 'Gĩkũyũ'].freeze,
+ kj: ['Kwanyama', 'Kuanyama'].freeze,
+ kk: ['Kazakh', 'қазақ тілі'].freeze,
+ kl: ['Kalaallisut', 'kalaallisut'].freeze,
+ km: ['Khmer', 'ខេមរភាសា'].freeze,
+ kn: ['Kannada', 'ಕನ್ನಡ'].freeze,
+ ko: ['Korean', '한국어'].freeze,
+ kr: ['Kanuri', 'Kanuri'].freeze,
+ ks: ['Kashmiri', 'कश्मीरी'].freeze,
+ ku: ['Kurdish', 'Kurdî'].freeze,
+ kv: ['Komi', 'коми кыв'].freeze,
+ kw: ['Cornish', 'Kernewek'].freeze,
+ ky: ['Kyrgyz', 'Кыргызча'].freeze,
+ la: ['Latin', 'latine'].freeze,
+ lb: ['Luxembourgish', 'Lëtzebuergesch'].freeze,
+ lg: ['Ganda', 'Luganda'].freeze,
+ li: ['Limburgish', 'Limburgs'].freeze,
+ ln: ['Lingala', 'Lingála'].freeze,
+ lo: ['Lao', 'ພາສາ'].freeze,
+ lt: ['Lithuanian', 'lietuvių kalba'].freeze,
+ lu: ['Luba-Katanga', 'Tshiluba'].freeze,
+ lv: ['Latvian', 'latviešu valoda'].freeze,
+ mg: ['Malagasy', 'fiteny malagasy'].freeze,
+ mh: ['Marshallese', 'Kajin M̧ajeļ'].freeze,
+ mi: ['Māori', 'te reo Māori'].freeze,
+ mk: ['Macedonian', 'македонски јазик'].freeze,
+ ml: ['Malayalam', 'മലയാളം'].freeze,
+ mn: ['Mongolian', 'Монгол хэл'].freeze,
+ mr: ['Marathi', 'मराठी'].freeze,
+ ms: ['Malay', 'Bahasa Malaysia'].freeze,
+ mt: ['Maltese', 'Malti'].freeze,
+ my: ['Burmese', 'ဗမာစာ'].freeze,
+ na: ['Nauru', 'Ekakairũ Naoero'].freeze,
+ nb: ['Norwegian Bokmål', 'Norsk bokmål'].freeze,
+ nd: ['Northern Ndebele', 'isiNdebele'].freeze,
+ ne: ['Nepali', 'नेपाली'].freeze,
+ ng: ['Ndonga', 'Owambo'].freeze,
+ nl: ['Dutch', 'Nederlands'].freeze,
+ nn: ['Norwegian Nynorsk', 'Norsk nynorsk'].freeze,
+ no: ['Norwegian', 'Norsk'].freeze,
+ nr: ['Southern Ndebele', 'isiNdebele'].freeze,
+ nv: ['Navajo', 'Diné bizaad'].freeze,
+ ny: ['Chichewa', 'chiCheŵa'].freeze,
+ oc: ['Occitan', 'occitan'].freeze,
+ oj: ['Ojibwe', 'ᐊᓂᔑᓈᐯᒧᐎᓐ'].freeze,
+ om: ['Oromo', 'Afaan Oromoo'].freeze,
+ or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
+ os: ['Ossetian', 'ирон æвзаг'].freeze,
+ pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
+ pi: ['Pāli', 'पाऴि'].freeze,
+ pl: ['Polish', 'Polski'].freeze,
+ ps: ['Pashto', 'پښتو'].freeze,
+ pt: ['Portuguese', 'Português'].freeze,
+ qu: ['Quechua', 'Runa Simi'].freeze,
+ rm: ['Romansh', 'rumantsch grischun'].freeze,
+ rn: ['Kirundi', 'Ikirundi'].freeze,
+ ro: ['Romanian', 'Română'].freeze,
+ ru: ['Russian', 'Русский'].freeze,
+ rw: ['Kinyarwanda', 'Ikinyarwanda'].freeze,
+ sa: ['Sanskrit', 'संस्कृतम्'].freeze,
+ sc: ['Sardinian', 'sardu'].freeze,
+ sd: ['Sindhi', 'सिन्धी'].freeze,
+ se: ['Northern Sami', 'Davvisámegiella'].freeze,
+ sg: ['Sango', 'yângâ tî sängö'].freeze,
+ si: ['Sinhala', 'සිංහල'].freeze,
+ sk: ['Slovak', 'slovenčina'].freeze,
+ sl: ['Slovenian', 'slovenščina'].freeze,
+ sn: ['Shona', 'chiShona'].freeze,
+ so: ['Somali', 'Soomaaliga'].freeze,
+ sq: ['Albanian', 'Shqip'].freeze,
+ sr: ['Serbian', 'српски језик'].freeze,
+ ss: ['Swati', 'SiSwati'].freeze,
+ st: ['Southern Sotho', 'Sesotho'].freeze,
+ su: ['Sundanese', 'Basa Sunda'].freeze,
+ sv: ['Swedish', 'Svenska'].freeze,
+ sw: ['Swahili', 'Kiswahili'].freeze,
+ ta: ['Tamil', 'தமிழ்'].freeze,
+ te: ['Telugu', 'తెలుగు'].freeze,
+ tg: ['Tajik', 'тоҷикӣ'].freeze,
+ th: ['Thai', 'ไทย'].freeze,
+ ti: ['Tigrinya', 'ትግርኛ'].freeze,
+ tk: ['Turkmen', 'Türkmen'].freeze,
+ tl: ['Tagalog', 'Wikang Tagalog'].freeze,
+ tn: ['Tswana', 'Setswana'].freeze,
+ to: ['Tonga', 'faka Tonga'].freeze,
+ tr: ['Turkish', 'Türkçe'].freeze,
+ ts: ['Tsonga', 'Xitsonga'].freeze,
+ tt: ['Tatar', 'татар теле'].freeze,
+ tw: ['Twi', 'Twi'].freeze,
+ ty: ['Tahitian', 'Reo Tahiti'].freeze,
+ ug: ['Uyghur', 'ئۇيغۇرچە'].freeze,
+ uk: ['Ukrainian', 'Українська'].freeze,
+ ur: ['Urdu', 'اردو'].freeze,
+ uz: ['Uzbek', 'Ўзбек'].freeze,
+ ve: ['Venda', 'Tshivenḓa'].freeze,
+ vi: ['Vietnamese', 'Tiếng Việt'].freeze,
+ vo: ['Volapük', 'Volapük'].freeze,
+ wa: ['Walloon', 'walon'].freeze,
+ wo: ['Wolof', 'Wollof'].freeze,
+ xh: ['Xhosa', 'isiXhosa'].freeze,
+ yi: ['Yiddish', 'ייִדיש'].freeze,
+ yo: ['Yoruba', 'Yorùbá'].freeze,
+ za: ['Zhuang', 'Saɯ cueŋƅ'].freeze,
+ zh: ['Chinese', '中文'].freeze,
+ zu: ['Zulu', 'isiZulu'].freeze,
+ }.freeze
+
+ ISO_639_3 = {
+ ast: ['Asturian', 'Asturianu'].freeze,
+ kab: ['Kabyle', 'Taqbaylit'].freeze,
+ kmr: ['Northern Kurdish', 'Kurmancî'].freeze,
+ zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
+ }.freeze
+
+ SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze
+
+ # For ISO-639-1 and ISO-639-3 language codes, we have their official
+ # names, but for some translations, we need the names of the
+ # regional variants specifically
+ REGIONAL_LOCALE_NAMES = {
'es-AR': 'Español (Argentina)',
'es-MX': 'Español (México)',
- es: 'Español',
- et: 'Eesti',
- eu: 'Euskara',
- fa: 'فارسی',
- fi: 'Suomi',
- fr: 'Français',
- ga: 'Gaeilge',
- gd: 'Gàidhlig',
- gl: 'Galego',
- he: 'עברית',
- hi: 'हिन्दी',
- hr: 'Hrvatski',
- hu: 'Magyar',
- hy: 'Հայերեն',
- id: 'Bahasa Indonesia',
- io: 'Ido',
- is: 'Íslenska',
- it: 'Italiano',
- ja: '日本語',
- ka: 'ქართული',
- kab: 'Taqbaylit',
- kk: 'Қазақша',
- kmr: 'Kurmancî',
- kn: 'ಕನ್ನಡ',
- ko: '한국어',
- ku: 'سۆرانی',
- lt: 'Lietuvių',
- lv: 'Latviešu',
- mk: 'Македонски',
- ml: 'മലയാളം',
- mr: 'मराठी',
- ms: 'Bahasa Melayu',
- nl: 'Nederlands',
- nn: 'Nynorsk',
- no: 'Norsk',
- oc: 'Occitan',
- pl: 'Polski',
'pt-BR': 'Português (Brasil)',
'pt-PT': 'Português (Portugal)',
- pt: 'Português',
- ro: 'Română',
- ru: 'Русский',
- sa: 'संस्कृतम्',
- sc: 'Sardu',
- si: 'සිංහල',
- sk: 'Slovenčina',
- sl: 'Slovenščina',
- sq: 'Shqip',
'sr-Latn': 'Srpski (latinica)',
- sr: 'Српски',
- sv: 'Svenska',
- ta: 'தமிழ்',
- te: 'తెలుగు',
- th: 'ไทย',
- tr: 'Türkçe',
- uk: 'Українська',
- ur: 'اُردُو',
- vi: 'Tiếng Việt',
- zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)',
- zh: '中文',
}.freeze
- def human_locale(locale)
- if locale == 'und'
+ def native_locale_name(locale)
+ if locale.blank? || locale == 'und'
I18n.t('generic.none')
+ elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
+ supported_locale[1]
+ elsif (regional_locale = REGIONAL_LOCALE_NAMES[locale.to_sym])
+ regional_locale
else
- HUMAN_LOCALES[locale.to_sym] || locale
+ locale
end
end
+
+ def standard_locale_name(locale)
+ if locale.blank?
+ I18n.t('generic.none')
+ elsif (supported_locale = SUPPORTED_LOCALES[locale.to_sym])
+ supported_locale[0]
+ else
+ locale
+ end
+ end
+
+ def valid_locale_or_nil(str)
+ return if str.blank?
+
+ code, = str.to_s.split(/[_-]/) # Strip out the region from e.g. en_US or ja-JP
+
+ return unless valid_locale?(code)
+
+ code
+ end
+
+ def valid_locale_cascade(*arr)
+ arr.each do |str|
+ locale = valid_locale_or_nil(str)
+ return locale if locale.present?
+ end
+
+ nil
+ end
+
+ def valid_locale?(locale)
+ locale.present? && SUPPORTED_LOCALES.key?(locale.to_sym)
+ end
end
diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb
index 23739d1cd..3d5592867 100644
--- a/app/helpers/settings_helper.rb
+++ b/app/helpers/settings_helper.rb
@@ -2,7 +2,7 @@
module SettingsHelper
def filterable_languages
- LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
+ LanguagesHelper::SUPPORTED_LOCALES.keys
end
def hash_to_object(hash)
diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index 25f079e9d..d328f89b7 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -132,7 +132,7 @@ module StatusesHelper
end
def render_video_component(status, **options)
- video = status.media_attachments.first
+ video = status.ordered_media_attachments.first
meta = video.file.meta || {}
@@ -150,12 +150,12 @@ module StatusesHelper
}.merge(**options)
react_component :video, component_params do
- render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
def render_audio_component(status, **options)
- audio = status.media_attachments.first
+ audio = status.ordered_media_attachments.first
meta = audio.file.meta || {}
@@ -170,7 +170,7 @@ module StatusesHelper
}.merge(**options)
react_component :audio, component_params do
- render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
@@ -178,11 +178,11 @@ module StatusesHelper
component_params = {
sensitive: sensitized?(status, current_account),
autoplay: prefers_autoplay?,
- media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
+ media: status.ordered_media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json },
}.merge(**options)
react_component :media_gallery, component_params do
- render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ render partial: 'statuses/attachment_list', locals: { attachments: status.ordered_media_attachments }
end
end
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 94a4df418..b0a217550 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -70,6 +70,8 @@ export const INIT_MEDIA_EDIT_MODAL = 'INIT_MEDIA_EDIT_MODAL';
export const COMPOSE_CHANGE_MEDIA_DESCRIPTION = 'COMPOSE_CHANGE_MEDIA_DESCRIPTION';
export const COMPOSE_CHANGE_MEDIA_FOCUS = 'COMPOSE_CHANGE_MEDIA_FOCUS';
+export const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS';
+
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
@@ -83,6 +85,15 @@ export const ensureComposeIsVisible = (getState, routerHistory) => {
}
};
+export function setComposeToStatus(status, text, spoiler_text) {
+ return{
+ type: COMPOSE_SET_STATUS,
+ status,
+ text,
+ spoiler_text,
+ };
+};
+
export function changeCompose(text) {
return {
type: COMPOSE_CHANGE,
@@ -137,8 +148,9 @@ export function directCompose(account, routerHistory) {
export function submitCompose(routerHistory) {
return function (dispatch, getState) {
- const status = getState().getIn(['compose', 'text'], '');
- const media = getState().getIn(['compose', 'media_attachments']);
+ const status = getState().getIn(['compose', 'text'], '');
+ const media = getState().getIn(['compose', 'media_attachments']);
+ const statusId = getState().getIn(['compose', 'id'], null);
if ((!status || !status.length) && media.size === 0) {
return;
@@ -146,15 +158,18 @@ export function submitCompose(routerHistory) {
dispatch(submitComposeRequest());
- api(getState).post('/api/v1/statuses', {
- status,
- in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
- media_ids: media.map(item => item.get('id')),
- sensitive: getState().getIn(['compose', 'sensitive']),
- spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
- visibility: getState().getIn(['compose', 'privacy']),
- poll: getState().getIn(['compose', 'poll'], null),
- }, {
+ api(getState).request({
+ url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`,
+ method: statusId === null ? 'post' : 'put',
+ data: {
+ status,
+ in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
+ media_ids: media.map(item => item.get('id')),
+ sensitive: getState().getIn(['compose', 'sensitive']),
+ spoiler_text: getState().getIn(['compose', 'spoiler']) ? getState().getIn(['compose', 'spoiler_text'], '') : '',
+ visibility: getState().getIn(['compose', 'privacy']),
+ poll: getState().getIn(['compose', 'poll'], null),
+ },
headers: {
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
},
@@ -176,11 +191,11 @@ export function submitCompose(routerHistory) {
}
};
- if (response.data.visibility !== 'direct') {
+ if (statusId === null && response.data.visibility !== 'direct') {
insertIfOnline('home');
}
- if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+ if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
insertIfOnline(`account:${response.data.account.id}`);
diff --git a/app/javascript/mastodon/actions/history.js b/app/javascript/mastodon/actions/history.js
new file mode 100644
index 000000000..c142aaf61
--- /dev/null
+++ b/app/javascript/mastodon/actions/history.js
@@ -0,0 +1,37 @@
+import api from '../api';
+import { importFetchedAccounts } from './importer';
+
+export const HISTORY_FETCH_REQUEST = 'HISTORY_FETCH_REQUEST';
+export const HISTORY_FETCH_SUCCESS = 'HISTORY_FETCH_SUCCESS';
+export const HISTORY_FETCH_FAIL = 'HISTORY_FETCH_FAIL';
+
+export const fetchHistory = statusId => (dispatch, getState) => {
+ const loading = getState().getIn(['history', statusId, 'loading']);
+
+ if (loading) {
+ return;
+ }
+
+ dispatch(fetchHistoryRequest(statusId));
+
+ api(getState).get(`/api/v1/statuses/${statusId}/history`).then(({ data }) => {
+ dispatch(importFetchedAccounts(data.map(x => x.account)));
+ dispatch(fetchHistorySuccess(statusId, data));
+ }).catch(error => dispatch(fetchHistoryFail(error)));
+};
+
+export const fetchHistoryRequest = statusId => ({
+ type: HISTORY_FETCH_REQUEST,
+ statusId,
+});
+
+export const fetchHistorySuccess = (statusId, history) => ({
+ type: HISTORY_FETCH_SUCCESS,
+ statusId,
+ history,
+});
+
+export const fetchHistoryFail = error => ({
+ type: HISTORY_FETCH_FAIL,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/modal.js b/app/javascript/mastodon/actions/modal.js
index 3d0299db5..3e576fab8 100644
--- a/app/javascript/mastodon/actions/modal.js
+++ b/app/javascript/mastodon/actions/modal.js
@@ -9,9 +9,10 @@ export function openModal(type, props) {
};
};
-export function closeModal(type) {
+export function closeModal(type, options = { ignoreFocus: false }) {
return {
type: MODAL_CLOSE,
modalType: type,
+ ignoreFocus: options.ignoreFocus,
};
};
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index 663cf21e3..00e8d74d7 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -34,7 +34,6 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
-
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
@@ -46,7 +45,7 @@ defineMessages({
});
const fetchRelatedRelationships = (dispatch, notifications) => {
- const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id);
+ const accountIds = notifications.filter(item => ['follow', 'follow_request', 'admin.sign_up'].indexOf(item.type) !== -1).map(item => item.account.id);
if (accountIds.length > 0) {
dispatch(fetchRelationships(accountIds));
@@ -124,7 +123,18 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
const excludeTypesFromFilter = filter => {
- const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll']);
+ const allTypes = ImmutableList([
+ 'follow',
+ 'follow_request',
+ 'favourite',
+ 'reblog',
+ 'mention',
+ 'poll',
+ 'status',
+ 'update',
+ 'admin.sign_up',
+ ]);
+
return allTypes.filterNot(item => item === filter).toJS();
};
diff --git a/app/javascript/mastodon/actions/reports.js b/app/javascript/mastodon/actions/reports.js
index afa0c3412..fbe5b3791 100644
--- a/app/javascript/mastodon/actions/reports.js
+++ b/app/javascript/mastodon/actions/reports.js
@@ -1,89 +1,38 @@
import api from '../api';
-import { openModal, closeModal } from './modal';
-
-export const REPORT_INIT = 'REPORT_INIT';
-export const REPORT_CANCEL = 'REPORT_CANCEL';
+import { openModal } from './modal';
export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST';
export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS';
export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL';
-export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE';
-export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
-export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
+export const initReport = (account, status) => dispatch =>
+ dispatch(openModal('REPORT', {
+ accountId: account.get('id'),
+ statusId: status?.get('id'),
+ }));
-export function initReport(account, status) {
- return dispatch => {
- dispatch({
- type: REPORT_INIT,
- account,
- status,
- });
+export const submitReport = (params, onSuccess, onFail) => (dispatch, getState) => {
+ dispatch(submitReportRequest());
- dispatch(openModal('REPORT'));
- };
+ api(getState).post('/api/v1/reports', params).then(response => {
+ dispatch(submitReportSuccess(response.data));
+ if (onSuccess) onSuccess();
+ }).catch(error => {
+ dispatch(submitReportFail(error));
+ if (onFail) onFail();
+ });
};
-export function cancelReport() {
- return {
- type: REPORT_CANCEL,
- };
-};
+export const submitReportRequest = () => ({
+ type: REPORT_SUBMIT_REQUEST,
+});
-export function toggleStatusReport(statusId, checked) {
- return {
- type: REPORT_STATUS_TOGGLE,
- statusId,
- checked,
- };
-};
+export const submitReportSuccess = report => ({
+ type: REPORT_SUBMIT_SUCCESS,
+ report,
+});
-export function submitReport() {
- return (dispatch, getState) => {
- dispatch(submitReportRequest());
-
- api(getState).post('/api/v1/reports', {
- account_id: getState().getIn(['reports', 'new', 'account_id']),
- status_ids: getState().getIn(['reports', 'new', 'status_ids']),
- comment: getState().getIn(['reports', 'new', 'comment']),
- forward: getState().getIn(['reports', 'new', 'forward']),
- }).then(response => {
- dispatch(closeModal());
- dispatch(submitReportSuccess(response.data));
- }).catch(error => dispatch(submitReportFail(error)));
- };
-};
-
-export function submitReportRequest() {
- return {
- type: REPORT_SUBMIT_REQUEST,
- };
-};
-
-export function submitReportSuccess(report) {
- return {
- type: REPORT_SUBMIT_SUCCESS,
- report,
- };
-};
-
-export function submitReportFail(error) {
- return {
- type: REPORT_SUBMIT_FAIL,
- error,
- };
-};
-
-export function changeReportComment(comment) {
- return {
- type: REPORT_COMMENT_CHANGE,
- comment,
- };
-};
-
-export function changeReportForward(forward) {
- return {
- type: REPORT_FORWARD_CHANGE,
- forward,
- };
-};
+export const submitReportFail = error => ({
+ type: REPORT_SUBMIT_FAIL,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/rules.js b/app/javascript/mastodon/actions/rules.js
new file mode 100644
index 000000000..34e60a121
--- /dev/null
+++ b/app/javascript/mastodon/actions/rules.js
@@ -0,0 +1,27 @@
+import api from '../api';
+
+export const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
+export const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
+export const RULES_FETCH_FAIL = 'RULES_FETCH_FAIL';
+
+export const fetchRules = () => (dispatch, getState) => {
+ dispatch(fetchRulesRequest());
+
+ api(getState)
+ .get('/api/v1/instance').then(({ data }) => dispatch(fetchRulesSuccess(data.rules)))
+ .catch(err => dispatch(fetchRulesFail(err)));
+};
+
+const fetchRulesRequest = () => ({
+ type: RULES_FETCH_REQUEST,
+});
+
+const fetchRulesSuccess = rules => ({
+ type: RULES_FETCH_SUCCESS,
+ rules,
+});
+
+const fetchRulesFail = error => ({
+ type: RULES_FETCH_FAIL,
+ error,
+});
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index 20d71362e..adc24eabf 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -2,7 +2,7 @@ import api from '../api';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
-import { ensureComposeIsVisible } from './compose';
+import { ensureComposeIsVisible, setComposeToStatus } from './compose';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS';
@@ -30,6 +30,10 @@ export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const REDRAFT = 'REDRAFT';
+export const STATUS_FETCH_SOURCE_REQUEST = 'STATUS_FETCH_SOURCE_REQUEST';
+export const STATUS_FETCH_SOURCE_SUCCESS = 'STATUS_FETCH_SOURCE_SUCCESS';
+export const STATUS_FETCH_SOURCE_FAIL = 'STATUS_FETCH_SOURCE_FAIL';
+
export function fetchStatusRequest(id, skipLoading) {
return {
type: STATUS_FETCH_REQUEST,
@@ -84,6 +88,37 @@ export function redraft(status, raw_text) {
};
};
+export const editStatus = (id, routerHistory) => (dispatch, getState) => {
+ let status = getState().getIn(['statuses', id]);
+
+ if (status.get('poll')) {
+ status = status.set('poll', getState().getIn(['polls', status.get('poll')]));
+ }
+
+ dispatch(fetchStatusSourceRequest());
+
+ api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
+ dispatch(fetchStatusSourceSuccess());
+ ensureComposeIsVisible(getState, routerHistory);
+ dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text));
+ }).catch(error => {
+ dispatch(fetchStatusSourceFail(error));
+ });
+};
+
+export const fetchStatusSourceRequest = () => ({
+ type: STATUS_FETCH_SOURCE_REQUEST,
+});
+
+export const fetchStatusSourceSuccess = () => ({
+ type: STATUS_FETCH_SOURCE_SUCCESS,
+});
+
+export const fetchStatusSourceFail = error => ({
+ type: STATUS_FETCH_SOURCE_FAIL,
+ error,
+});
+
export function deleteStatus(id, routerHistory, withRedraft = false) {
return (dispatch, getState) => {
let status = getState().getIn(['statuses', id]);
diff --git a/app/javascript/mastodon/actions/trends.js b/app/javascript/mastodon/actions/trends.js
index 853e4f60a..304bbebef 100644
--- a/app/javascript/mastodon/actions/trends.js
+++ b/app/javascript/mastodon/actions/trends.js
@@ -1,31 +1,94 @@
import api from '../api';
+import { importFetchedStatuses } from './importer';
-export const TRENDS_FETCH_REQUEST = 'TRENDS_FETCH_REQUEST';
-export const TRENDS_FETCH_SUCCESS = 'TRENDS_FETCH_SUCCESS';
-export const TRENDS_FETCH_FAIL = 'TRENDS_FETCH_FAIL';
+export const TRENDS_TAGS_FETCH_REQUEST = 'TRENDS_TAGS_FETCH_REQUEST';
+export const TRENDS_TAGS_FETCH_SUCCESS = 'TRENDS_TAGS_FETCH_SUCCESS';
+export const TRENDS_TAGS_FETCH_FAIL = 'TRENDS_TAGS_FETCH_FAIL';
-export const fetchTrends = () => (dispatch, getState) => {
- dispatch(fetchTrendsRequest());
+export const TRENDS_LINKS_FETCH_REQUEST = 'TRENDS_LINKS_FETCH_REQUEST';
+export const TRENDS_LINKS_FETCH_SUCCESS = 'TRENDS_LINKS_FETCH_SUCCESS';
+export const TRENDS_LINKS_FETCH_FAIL = 'TRENDS_LINKS_FETCH_FAIL';
+
+export const TRENDS_STATUSES_FETCH_REQUEST = 'TRENDS_STATUSES_FETCH_REQUEST';
+export const TRENDS_STATUSES_FETCH_SUCCESS = 'TRENDS_STATUSES_FETCH_SUCCESS';
+export const TRENDS_STATUSES_FETCH_FAIL = 'TRENDS_STATUSES_FETCH_FAIL';
+
+export const fetchTrendingHashtags = () => (dispatch, getState) => {
+ dispatch(fetchTrendingHashtagsRequest());
api(getState)
- .get('/api/v1/trends')
- .then(({ data }) => dispatch(fetchTrendsSuccess(data)))
- .catch(err => dispatch(fetchTrendsFail(err)));
+ .get('/api/v1/trends/tags')
+ .then(({ data }) => dispatch(fetchTrendingHashtagsSuccess(data)))
+ .catch(err => dispatch(fetchTrendingHashtagsFail(err)));
};
-export const fetchTrendsRequest = () => ({
- type: TRENDS_FETCH_REQUEST,
+export const fetchTrendingHashtagsRequest = () => ({
+ type: TRENDS_TAGS_FETCH_REQUEST,
skipLoading: true,
});
-export const fetchTrendsSuccess = trends => ({
- type: TRENDS_FETCH_SUCCESS,
+export const fetchTrendingHashtagsSuccess = trends => ({
+ type: TRENDS_TAGS_FETCH_SUCCESS,
trends,
skipLoading: true,
});
-export const fetchTrendsFail = error => ({
- type: TRENDS_FETCH_FAIL,
+export const fetchTrendingHashtagsFail = error => ({
+ type: TRENDS_TAGS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+});
+
+export const fetchTrendingLinks = () => (dispatch, getState) => {
+ dispatch(fetchTrendingLinksRequest());
+
+ api(getState)
+ .get('/api/v1/trends/links')
+ .then(({ data }) => dispatch(fetchTrendingLinksSuccess(data)))
+ .catch(err => dispatch(fetchTrendingLinksFail(err)));
+};
+
+export const fetchTrendingLinksRequest = () => ({
+ type: TRENDS_LINKS_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+export const fetchTrendingLinksSuccess = trends => ({
+ type: TRENDS_LINKS_FETCH_SUCCESS,
+ trends,
+ skipLoading: true,
+});
+
+export const fetchTrendingLinksFail = error => ({
+ type: TRENDS_LINKS_FETCH_FAIL,
+ error,
+ skipLoading: true,
+ skipAlert: true,
+});
+
+export const fetchTrendingStatuses = () => (dispatch, getState) => {
+ dispatch(fetchTrendingStatusesRequest());
+
+ api(getState).get('/api/v1/trends/statuses').then(({ data }) => {
+ dispatch(importFetchedStatuses(data));
+ dispatch(fetchTrendingStatusesSuccess(data));
+ }).catch(err => dispatch(fetchTrendingStatusesFail(err)));
+};
+
+export const fetchTrendingStatusesRequest = () => ({
+ type: TRENDS_STATUSES_FETCH_REQUEST,
+ skipLoading: true,
+});
+
+export const fetchTrendingStatusesSuccess = statuses => ({
+ type: TRENDS_STATUSES_FETCH_SUCCESS,
+ statuses,
+ skipLoading: true,
+});
+
+export const fetchTrendingStatusesFail = error => ({
+ type: TRENDS_STATUSES_FETCH_FAIL,
error,
skipLoading: true,
skipAlert: true,
diff --git a/app/javascript/mastodon/components/admin/Counter.js b/app/javascript/mastodon/components/admin/Counter.js
index 047e864b2..6edb7bcfc 100644
--- a/app/javascript/mastodon/components/admin/Counter.js
+++ b/app/javascript/mastodon/components/admin/Counter.js
@@ -68,12 +68,12 @@ export default class Counter extends React.PureComponent {
);
} else {
const measure = data[0];
- const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
+ const percentChange = measure.previous_total && percIncrease(measure.previous_total * 1, measure.total * 1);
content = (
-
- 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}
+ {measure.human_value || }
+ {measure.previous_total && ( 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'})}
);
}
diff --git a/app/javascript/mastodon/components/check.js b/app/javascript/mastodon/components/check.js
new file mode 100644
index 000000000..ee2ef1595
--- /dev/null
+++ b/app/javascript/mastodon/components/check.js
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const Check = () => (
+
+);
+
+export default Check;
diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js
index 7d0588901..4b4ad8355 100644
--- a/app/javascript/mastodon/components/dropdown_menu.js
+++ b/app/javascript/mastodon/components/dropdown_menu.js
@@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../features/ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import { supportsPassiveEvents } from 'detect-passive-events';
+import classNames from 'classnames';
+import { CircularProgress } from 'mastodon/components/loading_indicator';
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
let id = 0;
@@ -17,13 +19,18 @@ class DropdownMenu extends React.PureComponent {
};
static propTypes = {
- items: PropTypes.array.isRequired,
+ items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+ loading: PropTypes.bool,
+ scrollable: PropTypes.bool,
onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
+ renderItem: PropTypes.func,
+ renderHeader: PropTypes.func,
+ onItemClick: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -45,9 +52,11 @@ class DropdownMenu extends React.PureComponent {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
+
if (this.focusedItem && this.props.openedViaKeyboard) {
this.focusedItem.focus({ preventScroll: true });
}
+
this.setState({ mounted: true });
}
@@ -66,7 +75,7 @@ class DropdownMenu extends React.PureComponent {
}
handleKeyDown = e => {
- const items = Array.from(this.node.getElementsByTagName('a'));
+ const items = Array.from(this.node.querySelectorAll('a, button'));
const index = items.indexOf(document.activeElement);
let element = null;
@@ -109,21 +118,11 @@ class DropdownMenu extends React.PureComponent {
}
handleClick = e => {
- const i = Number(e.currentTarget.getAttribute('data-index'));
- const { action, to } = this.props.items[i];
-
- this.props.onClose();
-
- if (typeof action === 'function') {
- e.preventDefault();
- action(e);
- } else if (to) {
- e.preventDefault();
- this.context.router.history.push(to);
- }
+ const { onItemClick } = this.props;
+ onItemClick(e);
}
- renderItem (option, i) {
+ renderItem = (option, i) => {
if (option === null) {
return ;
}
@@ -140,9 +139,11 @@ class DropdownMenu extends React.PureComponent {
}
render () {
- const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props;
+ const { items, style, placement, arrowOffsetLeft, arrowOffsetTop, scrollable, renderHeader, loading } = this.props;
const { mounted } = this.state;
+ let renderItem = this.props.renderItem || this.renderItem;
+
return (
{({ opacity, scaleX, scaleY }) => (
@@ -152,9 +153,23 @@ class DropdownMenu extends React.PureComponent {
-
- {items.map((option, i) => this.renderItem(option, i))}
-
+
+ {loading && (
+
+ )}
+
+ {!loading && renderHeader && (
+
+ {renderHeader(items)}
+
+ )}
+
+ {!loading && (
+
+ {items.map((option, i) => renderItem(option, i, { onClick: this.handleClick, onKeyPress: this.handleItemKeyPress }))}
+
+ )}
+
)}
@@ -170,11 +185,14 @@ export default class Dropdown extends React.PureComponent {
};
static propTypes = {
- icon: PropTypes.string.isRequired,
- items: PropTypes.array.isRequired,
- size: PropTypes.number.isRequired,
+ children: PropTypes.node,
+ icon: PropTypes.string,
+ items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
+ loading: PropTypes.bool,
+ size: PropTypes.number,
title: PropTypes.string,
disabled: PropTypes.bool,
+ scrollable: PropTypes.bool,
status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func,
onOpen: PropTypes.func.isRequired,
@@ -182,6 +200,9 @@ export default class Dropdown extends React.PureComponent {
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
+ renderItem: PropTypes.func,
+ renderHeader: PropTypes.func,
+ onItemClick: PropTypes.func,
};
static defaultProps = {
@@ -237,17 +258,21 @@ export default class Dropdown extends React.PureComponent {
}
handleItemClick = e => {
+ const { onItemClick } = this.props;
const i = Number(e.currentTarget.getAttribute('data-index'));
- const { action, to } = this.props.items[i];
+ const item = this.props.items[i];
this.handleClose();
- if (typeof action === 'function') {
+ if (typeof onItemClick === 'function') {
e.preventDefault();
- action();
- } else if (to) {
+ onItemClick(item, i);
+ } else if (item && typeof item.action === 'function') {
e.preventDefault();
- this.context.router.history.push(to);
+ item.action();
+ } else if (item && item.to) {
+ e.preventDefault();
+ this.context.router.history.push(item.to);
}
}
@@ -265,29 +290,67 @@ export default class Dropdown extends React.PureComponent {
}
}
+ close = () => {
+ this.handleClose();
+ }
+
render () {
- const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
+ const {
+ icon,
+ items,
+ size,
+ title,
+ disabled,
+ loading,
+ scrollable,
+ dropdownPlacement,
+ openDropdownId,
+ openedViaKeyboard,
+ children,
+ renderItem,
+ renderHeader,
+ } = this.props;
+
const open = this.state.id === openDropdownId;
+ const button = children ? React.cloneElement(React.Children.only(children), {
+ ref: this.setTargetRef,
+ onClick: this.handleClick,
+ onMouseDown: this.handleMouseDown,
+ onKeyDown: this.handleButtonKeyDown,
+ onKeyPress: this.handleKeyPress,
+ }) : (
+
+ );
+
return (
-
-
+
+ {button}
-
+
-
+
);
}
diff --git a/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js
new file mode 100644
index 000000000..e30c18372
--- /dev/null
+++ b/app/javascript/mastodon/components/edited_timestamp/containers/dropdown_menu_container.js
@@ -0,0 +1,27 @@
+import { connect } from 'react-redux';
+import { openDropdownMenu, closeDropdownMenu } from 'mastodon/actions/dropdown_menu';
+import { fetchHistory } from 'mastodon/actions/history';
+import DropdownMenu from 'mastodon/components/dropdown_menu';
+
+const mapStateToProps = (state, { statusId }) => ({
+ dropdownPlacement: state.getIn(['dropdown_menu', 'placement']),
+ openDropdownId: state.getIn(['dropdown_menu', 'openId']),
+ openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
+ items: state.getIn(['history', statusId, 'items']),
+ loading: state.getIn(['history', statusId, 'loading']),
+});
+
+const mapDispatchToProps = (dispatch, { statusId }) => ({
+
+ onOpen (id, onItemClick, dropdownPlacement, keyboard) {
+ dispatch(fetchHistory(statusId));
+ dispatch(openDropdownMenu(id, dropdownPlacement, keyboard));
+ },
+
+ onClose (id) {
+ dispatch(closeDropdownMenu(id));
+ },
+
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu);
diff --git a/app/javascript/mastodon/components/edited_timestamp/index.js b/app/javascript/mastodon/components/edited_timestamp/index.js
new file mode 100644
index 000000000..bebf93886
--- /dev/null
+++ b/app/javascript/mastodon/components/edited_timestamp/index.js
@@ -0,0 +1,70 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { FormattedMessage, injectIntl } from 'react-intl';
+import Icon from 'mastodon/components/icon';
+import DropdownMenu from './containers/dropdown_menu_container';
+import { connect } from 'react-redux';
+import { openModal } from 'mastodon/actions/modal';
+import RelativeTimestamp from 'mastodon/components/relative_timestamp';
+import InlineAccount from 'mastodon/components/inline_account';
+
+const mapDispatchToProps = (dispatch, { statusId }) => ({
+
+ onItemClick (index) {
+ dispatch(openModal('COMPARE_HISTORY', { index, statusId }));
+ },
+
+});
+
+export default @connect(null, mapDispatchToProps)
+@injectIntl
+class EditedTimestamp extends React.PureComponent {
+
+ static propTypes = {
+ statusId: PropTypes.string.isRequired,
+ timestamp: PropTypes.string.isRequired,
+ intl: PropTypes.object.isRequired,
+ onItemClick: PropTypes.func.isRequired,
+ };
+
+ handleItemClick = (item, i) => {
+ const { onItemClick } = this.props;
+ onItemClick(i);
+ };
+
+ renderHeader = items => {
+ return (
+
+ );
+ }
+
+ renderItem = (item, index, { onClick, onKeyPress }) => {
+ const formattedDate = ;
+ const formattedName = ;
+
+ const label = item.get('original') ? (
+
+ ) : (
+
+ );
+
+ return (
+
+
+
+ );
+ }
+
+ render () {
+ const { timestamp, intl, statusId } = this.props;
+
+ return (
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/components/hashtag.js b/app/javascript/mastodon/components/hashtag.js
index a793a32f5..7f442d189 100644
--- a/app/javascript/mastodon/components/hashtag.js
+++ b/app/javascript/mastodon/components/hashtag.js
@@ -38,7 +38,7 @@ class SilentErrorBoundary extends React.Component {
*
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
*/
-const accountsCountRenderer = (displayNumber, pluralReady) => (
+export const accountsCountRenderer = (displayNumber, pluralReady) => (
+ {typeof counter !== 'undefined' && }
+
+ );
+
+ if (href) {
+ contents = (
+
+ {contents}
+
+ );
+ }
+
return (
);
}
diff --git a/app/javascript/mastodon/components/inline_account.js b/app/javascript/mastodon/components/inline_account.js
new file mode 100644
index 000000000..a1b495590
--- /dev/null
+++ b/app/javascript/mastodon/components/inline_account.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'mastodon/selectors';
+import Avatar from 'mastodon/components/avatar';
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { accountId }) => ({
+ account: getAccount(state, accountId),
+ });
+
+ return mapStateToProps;
+};
+
+export default @connect(makeMapStateToProps)
+class InlineAccount extends React.PureComponent {
+
+ static propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ };
+
+ render () {
+ const { account } = this.props;
+
+ return (
+
+ {account.get('username')}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/components/loading_indicator.js b/app/javascript/mastodon/components/loading_indicator.js
index d6a5adb6f..33c59d94c 100644
--- a/app/javascript/mastodon/components/loading_indicator.js
+++ b/app/javascript/mastodon/components/loading_indicator.js
@@ -1,10 +1,31 @@
import React from 'react';
-import { FormattedMessage } from 'react-intl';
+import PropTypes from 'prop-types';
+
+export const CircularProgress = ({ size, strokeWidth }) => {
+ const viewBox = `0 0 ${size} ${size}`;
+ const radius = (size - strokeWidth) / 2;
+
+ return (
+
+ );
+};
+
+CircularProgress.propTypes = {
+ size: PropTypes.number.isRequired,
+ strokeWidth: PropTypes.number.isRequired,
+};
const LoadingIndicator = () => (
);
diff --git a/app/javascript/mastodon/components/media_attachments.js b/app/javascript/mastodon/components/media_attachments.js
new file mode 100644
index 000000000..d27720de4
--- /dev/null
+++ b/app/javascript/mastodon/components/media_attachments.js
@@ -0,0 +1,116 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import { MediaGallery, Video, Audio } from 'mastodon/features/ui/util/async-components';
+import Bundle from 'mastodon/features/ui/components/bundle';
+import noop from 'lodash/noop';
+
+export default class MediaAttachments extends ImmutablePureComponent {
+
+ static propTypes = {
+ status: ImmutablePropTypes.map.isRequired,
+ height: PropTypes.number,
+ width: PropTypes.number,
+ };
+
+ static defaultProps = {
+ height: 110,
+ width: 239,
+ };
+
+ updateOnProps = [
+ 'status',
+ ];
+
+ renderLoadingMediaGallery = () => {
+ const { height, width } = this.props;
+
+ return (
+
+ );
+ }
+
+ renderLoadingVideoPlayer = () => {
+ const { height, width } = this.props;
+
+ return (
+
+ );
+ }
+
+ renderLoadingAudioPlayer = () => {
+ const { height, width } = this.props;
+
+ return (
+
+ );
+ }
+
+ render () {
+ const { status, width, height } = this.props;
+ const mediaAttachments = status.get('media_attachments');
+
+ if (mediaAttachments.size === 0) {
+ return null;
+ }
+
+ if (mediaAttachments.getIn([0, 'type']) === 'audio') {
+ const audio = mediaAttachments.get(0);
+
+ return (
+
+ {Component => (
+
+ )}
+
+ );
+ } else if (mediaAttachments.getIn([0, 'type']) === 'video') {
+ const video = mediaAttachments.get(0);
+
+ return (
+
+ {Component => (
+
+ )}
+
+ );
+ } else {
+ return (
+
+ {Component => (
+
+ )}
+
+ );
+ }
+ }
+
+}
diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js
index 755c46fd6..b894aeaf9 100644
--- a/app/javascript/mastodon/components/modal_root.js
+++ b/app/javascript/mastodon/components/modal_root.js
@@ -18,6 +18,7 @@ export default class ModalRoot extends React.PureComponent {
g: PropTypes.number,
b: PropTypes.number,
}),
+ ignoreFocus: PropTypes.bool,
};
activeElement = this.props.children ? document.activeElement : null;
@@ -72,7 +73,9 @@ export default class ModalRoot extends React.PureComponent {
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
- this.activeElement.focus({ preventScroll: true });
+ if (!this.props.ignoreFocus) {
+ this.activeElement.focus({ preventScroll: true });
+ }
this.activeElement = null;
}).catch(console.error);
diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js
index 711181dcd..512480339 100644
--- a/app/javascript/mastodon/components/relative_timestamp.js
+++ b/app/javascript/mastodon/components/relative_timestamp.js
@@ -5,10 +5,15 @@ import PropTypes from 'prop-types';
const messages = defineMessages({
today: { id: 'relative_time.today', defaultMessage: 'today' },
just_now: { id: 'relative_time.just_now', defaultMessage: 'now' },
+ just_now_full: { id: 'relative_time.full.just_now', defaultMessage: 'just now' },
seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' },
+ seconds_full: { id: 'relative_time.full.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} ago' },
minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' },
+ minutes_full: { id: 'relative_time.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} ago' },
hours: { id: 'relative_time.hours', defaultMessage: '{number}h' },
+ hours_full: { id: 'relative_time.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} ago' },
days: { id: 'relative_time.days', defaultMessage: '{number}d' },
+ days_full: { id: 'relative_time.full.days', defaultMessage: '{number, plural, one {# day} other {# days}} ago' },
moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' },
seconds_remaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left' },
minutes_remaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left' },
@@ -66,7 +71,7 @@ const getUnitDelay = units => {
}
};
-export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
+export const timeAgoString = (intl, date, now, year, timeGiven, short) => {
const delta = now - date.getTime();
let relativeTime;
@@ -74,16 +79,16 @@ export const timeAgoString = (intl, date, now, year, timeGiven = true) => {
if (delta < DAY && !timeGiven) {
relativeTime = intl.formatMessage(messages.today);
} else if (delta < 10 * SECOND) {
- relativeTime = intl.formatMessage(messages.just_now);
+ relativeTime = intl.formatMessage(short ? messages.just_now : messages.just_now_full);
} else if (delta < 7 * DAY) {
if (delta < MINUTE) {
- relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) });
+ relativeTime = intl.formatMessage(short ? messages.seconds : messages.seconds_full, { number: Math.floor(delta / SECOND) });
} else if (delta < HOUR) {
- relativeTime = intl.formatMessage(messages.minutes, { number: Math.floor(delta / MINUTE) });
+ relativeTime = intl.formatMessage(short ? messages.minutes : messages.minutes_full, { number: Math.floor(delta / MINUTE) });
} else if (delta < DAY) {
- relativeTime = intl.formatMessage(messages.hours, { number: Math.floor(delta / HOUR) });
+ relativeTime = intl.formatMessage(short ? messages.hours : messages.hours_full, { number: Math.floor(delta / HOUR) });
} else {
- relativeTime = intl.formatMessage(messages.days, { number: Math.floor(delta / DAY) });
+ relativeTime = intl.formatMessage(short ? messages.days : messages.days_full, { number: Math.floor(delta / DAY) });
}
} else if (date.getFullYear() === year) {
relativeTime = intl.formatDate(date, shortDateFormatOptions);
@@ -124,6 +129,7 @@ class RelativeTimestamp extends React.Component {
timestamp: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
futureDate: PropTypes.bool,
+ short: PropTypes.bool,
};
state = {
@@ -132,6 +138,7 @@ class RelativeTimestamp extends React.Component {
static defaultProps = {
year: (new Date()).getFullYear(),
+ short: true,
};
shouldComponentUpdate (nextProps, nextState) {
@@ -176,11 +183,11 @@ class RelativeTimestamp extends React.Component {
}
render () {
- const { timestamp, intl, year, futureDate } = this.props;
+ const { timestamp, intl, year, futureDate, short } = this.props;
const timeGiven = timestamp.includes('T');
const date = new Date(timestamp);
- const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven);
+ const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven, short);
return (