diff --git a/.circleci/config.yml b/.circleci/config.yml index 751ca95b1..4fcc8c618 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,8 @@ version: 2.1 orbs: - ruby: circleci/ruby@1.2.0 - node: circleci/node@4.7.0 + ruby: circleci/ruby@1.4.0 + node: circleci/node@5.0.1 executors: default: @@ -23,7 +23,7 @@ executors: environment: POSTGRES_USER: root POSTGRES_HOST_AUTH_METHOD: trust - - image: circleci/redis:6-alpine + - image: cimg/redis:6.2 commands: install-system-dependencies: @@ -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/.codeclimate.yml b/.codeclimate.yml index c253bd95a..ee9022cda 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,4 +1,4 @@ -version: "2" +version: '2' checks: argument-count: enabled: false @@ -34,8 +34,8 @@ plugins: sass-lint: enabled: true exclude_patterns: -- spec/ -- vendor/asset/ + - spec/ + - vendor/asset/ -- app/javascript/mastodon/locales/**/*.json -- config/locales/**/*.yml + - app/javascript/mastodon/locales/**/*.json + - config/locales/**/*.yml 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..628efc8ec --- /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..538f6cccd --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,83 @@ +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 7de5e00f4..0df0a8778 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -107,7 +107,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..880fdfac9 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -3,32 +3,41 @@ on: workflow_dispatch: push: branches: - - "main" + - '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 }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + if: github.event_name != 'pull_request' - uses: docker/metadata-action@v3 id: meta with: - images: tootsuite/mastodon + images: ghcr.io/${{ github.repository_owner }}/mastodon flavor: | - latest=auto + latest=true tags: | type=edge,branch=main - type=semver,pattern={{ raw }} + type=match,pattern=v(.*),group=0 + 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-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/mastodon:latest cache-to: type=inline diff --git a/.github/workflows/check-i18n.yml b/.github/workflows/check-i18n.yml index 2e8f230f3..be38a096d 100644 --- a/.github/workflows/check-i18n.yml +++ b/.github/workflows/check-i18n.yml @@ -2,9 +2,9 @@ name: Check i18n on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] env: RAILS_ENV: test @@ -14,21 +14,21 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.0' - bundler-cache: true - - name: Check locale file normalization - run: bundle exec i18n-tasks check-normalized - - name: Check for unused strings - run: bundle exec i18n-tasks unused -l en - - name: Check for wrong string interpolations - run: bundle exec i18n-tasks check-consistent-interpolations - - name: Check that all required locale files exist - run: bundle exec rake repo:check_locales_files + - uses: actions/checkout@v2 + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libicu-dev libidn11-dev + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.0' + bundler-cache: true + - name: Check locale file normalization + run: bundle exec i18n-tasks check-normalized + - name: Check for unused strings + run: bundle exec i18n-tasks unused -l en + - name: Check for wrong string interpolations + run: bundle exec i18n-tasks check-consistent-interpolations + - name: Check that all required locale files exist + run: bundle exec rake repo:check_locales_files diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..de7673eb6 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,78 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config and downloaded libraries. +/.bundle +/vendor/bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-journal + +# Ignore all logfiles and tempfiles. +.eslintcache +/log/* +!/log/.keep +/tmp +/coverage +/public/system +/public/assets +/public/packs +/public/packs-test +.env +.env.production +.env.development +/node_modules/ +/build/ + +# Ignore Vagrant files +.vagrant/ + +# Ignore Capistrano customizations +/config/deploy/* + +# Ignore IDE files +.vscode/ +.idea/ + +# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose +/postgres +/postgres14 +/redis +/elasticsearch + +# ignore Helm dependency charts +/chart/charts/*.tgz + +# Ignore Apple files +.DS_Store + +# Ignore vim files +*~ +*.swp + +# Ignore npm debug log +npm-debug.log + +# Ignore yarn log files +yarn-error.log +yarn-debug.log + +# Ignore vagrant log files +*-cloudimg-console.log + +# Ignore Docker option files +docker-compose.override.yml + +# Ignore Helm files +/chart + +# Ignore emoji map file +/app/javascript/mastodon/features/emoji/emoji_map.json + +# Ignore locale files +/app/javascript/mastodon/locales +/config/locales diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..1d70813d5 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + singleQuote: true +} diff --git a/.rubocop.yml b/.rubocop.yml index cb7c73ec7..a76937426 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -14,8 +14,8 @@ AllCops: - 'node_modules/**/*' - 'Vagrantfile' - 'vendor/**/*' - - 'lib/json_ld/*' - - 'lib/templates/**/*' + - 'lib/json_ld/*' + - 'lib/templates/**/*' Bundler/OrderedGems: Enabled: false @@ -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 c2eff7fa3..52a62a213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,202 @@ Changelog All notable changes to this project will be documented in this file. +## Unreleased +### Added + +- **Add support for incoming edited posts** ([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: + - `status.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 notification type: + - `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 + - Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338)) - Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425)) ### Security + - Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828)) - Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307) - Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430)) @@ -17,10 +207,12 @@ All notable changes to this project will be documented in this file. ## [3.4.5] - 2022-01-31 ### Added + - Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393)) - Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000)) ### Fixed + - Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394)) - Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398)) - Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188)) @@ -83,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)) @@ -416,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)) @@ -503,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)) @@ -526,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)) @@ -552,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)) @@ -568,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)) @@ -587,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)) @@ -702,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)) @@ -1191,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)) @@ -1201,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)) @@ -1492,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)) @@ -1669,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..2073cbebf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ SHELL ["/bin/bash", "-c"] RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections # Install Node v16 (LTS) -ENV NODE_VER="16.13.2" +ENV NODE_VER="16.14.2" RUN ARCH= && \ dpkgArch="$(dpkg --print-architecture)" && \ case "${dpkgArch##*-}" in \ @@ -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 1ddb6f383..8bb361f49 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' @@ -90,7 +89,7 @@ gem 'stoplight', '~> 2.2.1' gem 'strong_migrations', '~> 0.7' gem 'tty-prompt', '~> 0.23', require: false gem 'twitter-text', '~> 3.1.0' -gem 'tzinfo-data', '~> 1.2021' +gem 'tzinfo-data', '~> 1.2022' gem 'webpacker', '~> 5.4' gem 'webpush', '~> 0.3' gem 'webauthn', '~> 3.0.0.alpha1' @@ -102,12 +101,12 @@ gem 'rdf-normalize', '~> 0.5' gem 'redcarpet', '~> 3.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 @@ -117,7 +116,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' @@ -128,15 +127,15 @@ 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' - gem 'letter_opener', '~> 1.7' + gem 'letter_opener', '~> 1.8' gem 'letter_opener_web', '~> 2.0' gem 'memory_profiler' - gem 'rubocop', '~> 1.25', require: false - gem 'rubocop-rails', '~> 2.13', require: false + gem 'rubocop', '~> 1.26', require: false + gem 'rubocop-rails', '~> 2.14', require: false gem 'brakeman', '~> 5.2', require: false gem 'bundler-audit', '~> 0.9', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 550a79c87..5ee2b9c26 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) @@ -286,10 +294,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) @@ -303,10 +312,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) @@ -331,7 +343,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) @@ -339,8 +351,8 @@ GEM terrapin (~> 0.6.0) launchy (2.5.0) addressable (~> 2.7) - letter_opener (1.7.0) - launchy (~> 2.2) + letter_opener (1.8.1) + launchy (>= 2.2, < 3) letter_opener_web (2.0.0) actionmailer (>= 5.2) letter_opener (~> 1.7) @@ -355,14 +367,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) @@ -373,9 +385,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) @@ -385,8 +397,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) @@ -407,17 +419,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) - parallel (1.21.0) - parser (3.1.0.0) + ox (2.14.10) + parallel (1.22.0) + parser (3.1.1.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) @@ -439,35 +461,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) @@ -483,11 +511,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) @@ -497,9 +525,9 @@ GEM rdf (~> 3.2) redcarpet (3.5.1) redis (4.5.1) - redis-namespace (1.8.1) + redis-namespace (1.8.2) redis (>= 3.0.4) - regexp_parser (2.2.0) + regexp_parser (2.2.1) request_store (1.5.0) rack (>= 1.4) responders (3.0.1) @@ -508,19 +536,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) @@ -531,21 +559,21 @@ 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.26.0) parallel (~> 1.10) parser (>= 3.1.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml - rubocop-ast (>= 1.15.1, < 2.0) + rubocop-ast (>= 1.16.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.1) - parser (>= 3.0.1.1) - rubocop-rails (2.13.2) + rubocop-ast (1.16.0) + parser (>= 3.1.1.0) + rubocop-rails (2.14.2) activesupport (>= 4.2.0) rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) @@ -554,32 +582,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) @@ -605,11 +633,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) @@ -637,13 +669,19 @@ GEM unf (~> 0.1.0) tzinfo (2.0.4) concurrent-ruby (~> 1.0) - tzinfo-data (1.2021.5) + tzinfo-data (1.2022.1) tzinfo (>= 1.0.0) unf (0.1.4) unf_ext 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) @@ -656,6 +694,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) @@ -675,7 +716,7 @@ GEM xorcist (1.1.2) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.5.3) + zeitwerk (2.5.4) PLATFORMS ruby @@ -684,12 +725,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) @@ -701,7 +742,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 @@ -713,13 +753,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) hcaptcha (~> 7.1) hiredis (~> 0.6) @@ -729,12 +770,11 @@ 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) - letter_opener (~> 1.7) + kt-paperclip (~> 7.1) + letter_opener (~> 1.8) letter_opener_web (~> 2.0) link_header (~> 0.0) lograge (~> 0.11) @@ -761,12 +801,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) @@ -776,14 +816,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) + rubocop (~> 1.26) + rubocop-rails (~> 2.14) 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) @@ -799,7 +839,7 @@ DEPENDENCIES thor (~> 1.2) tty-prompt (~> 0.23) twitter-text (~> 3.1.0) - tzinfo-data (~> 1.2021) + tzinfo-data (~> 1.2022) webauthn (~> 3.0.0.alpha1) webmock (~> 3.14) webpacker (~> 5.4) 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 aeff2f233..0d44b4d23 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.json b/app.json index 6b4365383..c694908c5 100644 --- a/app.json +++ b/app.json @@ -95,8 +95,5 @@ "scripts": { "postdeploy": "bundle exec rails db:migrate && bundle exec rails db:seed" }, - "addons": [ - "heroku-postgresql", - "heroku-redis" - ] + "addons": ["heroku-postgresql", "heroku-redis"] } diff --git a/app/chewy/statuses_index.rb b/app/chewy/statuses_index.rb index 1903c2ea3..65cbb6fcd 100644 --- a/app/chewy/statuses_index.rb +++ b/app/chewy/statuses_index.rb @@ -57,7 +57,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/base_controller.rb b/app/controllers/activitypub/base_controller.rb index 4cbc3ab8f..196d85a32 100644 --- a/app/controllers/activitypub/base_controller.rb +++ b/app/controllers/activitypub/base_controller.rb @@ -2,6 +2,7 @@ class ActivityPub::BaseController < Api::BaseController skip_before_action :require_authenticated_user! + skip_around_action :set_locale private 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/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 71efb543e..47138bf6c 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -35,6 +35,9 @@ module Admin flash[:alert] = I18n.t('admin.accounts.no_account_selected') rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') + rescue ActiveRecord::RecordInvalid => e + error_message = action_from_button == 'copy' ? 'admin.custom_emojis.batch_copy_error' : 'admin.custom_emojis.batch_error' + flash[:alert] = I18n.t(error_message, message: e.message) ensure redirect_to admin_custom_emojis_path(filter_params) end 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..d96285b44 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? @@ -14,8 +15,6 @@ class Api::BaseController < ApplicationController protect_from_forgery with: :null_session - skip_around_action :set_locale - rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e| render json: { error: e.to_s }, status: 422 end 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 eefd28d45..c47d6ccfd 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -44,13 +44,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 @@ -81,17 +86,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 b1390ae48..7de446ac4 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,25 +36,46 @@ 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], - content_type: status_params[:content_type], - 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], + content_type: status_params[:content_type], + 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], + content_type: status_params[:content_type] + ) + + 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 @@ -73,8 +95,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 @@ -85,6 +108,7 @@ class Api::V1::StatusesController < Api::BaseController :sensitive, :spoiler_text, :visibility, + :language, :scheduled_at, :content_type, media_ids: [], 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/application_controller.rb b/app/controllers/application_controller.rb index 08cca0734..0f948ff5f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -136,16 +136,6 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end - def current_flavour - return Setting.flavour unless Themes.instance.flavours.include? current_user&.setting_flavour - current_user.setting_flavour - end - - def current_skin - return Setting.skin unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin - current_user.setting_skin - end - def respond_with_error(code) respond_to do |format| format.any do 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 6b1f3fa82..1c0f360a9 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -10,6 +10,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :set_pack 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] @@ -116,8 +117,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 @@ -128,6 +131,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/theming_concern.rb b/app/controllers/concerns/theming_concern.rb index 1ee3256c0..f993a81d7 100644 --- a/app/controllers/concerns/theming_concern.rb +++ b/app/controllers/concerns/theming_concern.rb @@ -10,8 +10,17 @@ module ThemingConcern private + def current_flavour + [current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) } + end + + def current_skin + skins = Themes.instance.skins_for(current_flavour) + [current_user&.setting_skin, Setting.skin, 'default'].find { |skin| skins.include?(skin) } + end + def valid_pack_data?(data, pack_name) - data['pack'].is_a?(Hash) && [String, Hash].any? { |c| data['pack'][pack_name].is_a?(c) } + data['pack'].is_a?(Hash) && data['pack'][pack_name].present? end def nil_pack(data) 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..7830c5524 --- /dev/null +++ b/app/controllers/disputes/base_controller.rb @@ -0,0 +1,23 @@ +# 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! + before_action :set_pack + + private + + def set_pack + use_pack 'admin' + end + + 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 d519138cd..f898994ac 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -16,13 +16,13 @@ class FollowerAccountsController < ApplicationController use_pack 'public' 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?) @@ -83,7 +83,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 4b4978fb9..bc291c962 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -16,13 +16,13 @@ class FollowingAccountsController < ApplicationController use_pack 'public' 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?) @@ -83,7 +83,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/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index b3b5476e2..0853897f2 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -3,6 +3,7 @@ class InstanceActorsController < ApplicationController include AccountControllerConcern + skip_before_action :check_account_confirmation skip_around_action :set_locale def show diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index d05ceb53f..1fddd087b 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -48,7 +48,6 @@ class Settings::PreferencesController < Settings::BaseController :setting_system_font_ui, :setting_system_emoji_font, :setting_noindex, - :setting_hide_network, :setting_hide_followers_count, :setting_aggregate_reblogs, :setting_show_application, @@ -58,7 +57,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_use_pending_items, :setting_trends, :setting_crop_images, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag), + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end 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 8b41033a5..eace78af6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -9,9 +9,9 @@ module ApplicationHelper RTL_LOCALES = %i( ar + ckb fa he - ku ).freeze def friendly_number_to_human(number, **options) @@ -225,4 +225,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..d39bb6c93 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -1,94 +1,257 @@ # 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: ['Kurmanji (Kurdish)', 'Kurmancî'].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 Melayu'].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, + ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, + kab: ['Kabyle', 'Taqbaylit'].freeze, + kmr: ['Kurmanji (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/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 261c72b2a..baa98e98f 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -75,6 +75,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.' }, @@ -88,6 +90,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, @@ -150,8 +161,9 @@ export function directCompose(account, routerHistory) { export function submitCompose(routerHistory) { return function (dispatch, getState) { - let status = getState().getIn(['compose', 'text'], ''); - let media = getState().getIn(['compose', 'media_attachments']); + let status = getState().getIn(['compose', 'text'], ''); + const media = getState().getIn(['compose', 'media_attachments']); + const statusId = getState().getIn(['compose', 'id'], null); const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; @@ -159,20 +171,25 @@ export function submitCompose(routerHistory) { return; } - dispatch(submitComposeRequest()); if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { status = status + ' 👁️'; } - api(getState).post('/api/v1/statuses', { - status, - content_type: getState().getIn(['compose', 'content_type']), - in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), - media_ids: media.map(item => item.get('id')), - sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), - spoiler_text: spoilerText, - visibility: getState().getIn(['compose', 'privacy']), - poll: getState().getIn(['compose', 'poll'], null), - }, { + + dispatch(submitComposeRequest()); + + api(getState).request({ + url: statusId === null ? '/api/v1/statuses' : `/api/v1/statuses/${statusId}`, + method: statusId === null ? 'post' : 'put', + data: { + status, + content_type: getState().getIn(['compose', 'content_type']), + in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + media_ids: media.map(item => item.get('id')), + sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), + spoiler_text: spoilerText, + visibility: getState().getIn(['compose', 'privacy']), + poll: getState().getIn(['compose', 'poll'], null), + }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), }, @@ -202,14 +219,16 @@ export function submitCompose(routerHistory) { } }; - insertIfOnline('home'); + if (statusId === null) { + 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'); if (!response.data.local_only) { insertIfOnline('public'); } - } else if (response.data.visibility === 'direct') { + } else if (statusId === null && response.data.visibility === 'direct') { insertIfOnline('direct'); } }).catch(function (error) { diff --git a/app/javascript/flavours/glitch/actions/history.js b/app/javascript/flavours/glitch/actions/history.js new file mode 100644 index 000000000..c47057261 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/history.js @@ -0,0 +1,37 @@ +import api from 'flavours/glitch/util/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/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js index 3d0299db5..3e576fab8 100644 --- a/app/javascript/flavours/glitch/actions/modal.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 4b00ea632..42ad39efa 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -47,7 +47,6 @@ export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; - export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; @@ -58,7 +57,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 > 0) { dispatch(fetchRelationships(accountIds)); @@ -136,7 +135,18 @@ const excludeTypesFromSettings = state => state.getIn(['settings', 'notification 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/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js index 80c3b3280..333bc71f4 100644 --- a/app/javascript/flavours/glitch/actions/reports.js +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -1,89 +1,38 @@ import api from 'flavours/glitch/util/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/flavours/glitch/actions/rules.js b/app/javascript/flavours/glitch/actions/rules.js new file mode 100644 index 000000000..b95045e81 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/rules.js @@ -0,0 +1,27 @@ +import api from 'flavours/glitch/util/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/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 7db357df1..6ffcf181d 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -2,7 +2,7 @@ import api from 'flavours/glitch/util/api'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses } 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'; @@ -26,6 +26,10 @@ export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; 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, @@ -81,6 +85,37 @@ export function redraft(status, raw_text, content_type) { }; }; +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/flavours/glitch/components/admin/Counter.js b/app/javascript/flavours/glitch/components/admin/Counter.js index 2bc9ce482..ecb242950 100644 --- a/app/javascript/flavours/glitch/components/admin/Counter.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/check.js b/app/javascript/flavours/glitch/components/check.js new file mode 100644 index 000000000..ee2ef1595 --- /dev/null +++ b/app/javascript/flavours/glitch/components/check.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const Check = () => ( + + + +); + +export default Check; diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 023fecb9a..bd6bc4ffd 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -6,6 +6,8 @@ import Overlay from 'react-overlays/lib/Overlay'; import Motion from 'flavours/glitch/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,30 +118,20 @@ 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(); - } 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

  • ; } - const { text, href = '#' } = option; + const { text, href = '#', target = '_blank', method } = option; return (
  • - + {text}
  • @@ -140,21 +139,37 @@ 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 }) => ( // It should not be transformed when mounting because the resulting // size will be used to determine the coordinate of the menu by // react-overlays -
    +
    -
      - {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 = { @@ -236,17 +257,22 @@ export default class Dropdown extends React.PureComponent { } } - handleItemClick = (i, e) => { - const { action, to } = this.props.items[i]; + handleItemClick = e => { + const { onItemClick } = this.props; + const i = Number(e.currentTarget.getAttribute('data-index')); + 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); } } @@ -264,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/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js new file mode 100644 index 000000000..8b73663d4 --- /dev/null +++ b/app/javascript/flavours/glitch/components/edited_timestamp/containers/dropdown_menu_container.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; +import { fetchHistory } from 'flavours/glitch/actions/history'; +import DropdownMenu from 'flavours/glitch/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/flavours/glitch/components/edited_timestamp/index.js b/app/javascript/flavours/glitch/components/edited_timestamp/index.js new file mode 100644 index 000000000..9648133af --- /dev/null +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/components/icon'; +import DropdownMenu from './containers/dropdown_menu_container'; +import { connect } from 'react-redux'; +import { openModal } from 'flavours/glitch/actions/modal'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import InlineAccount from 'flavours/glitch/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/flavours/glitch/components/icon_button.js b/app/javascript/flavours/glitch/components/icon_button.js index 58d3568dd..3999409cd 100644 --- a/app/javascript/flavours/glitch/components/icon_button.js +++ b/app/javascript/flavours/glitch/components/icon_button.js @@ -30,6 +30,7 @@ export default class IconButton extends React.PureComponent { label: PropTypes.string, counter: PropTypes.number, obfuscateCount: PropTypes.bool, + href: PropTypes.string, }; static defaultProps = { @@ -109,6 +110,7 @@ export default class IconButton extends React.PureComponent { title, counter, obfuscateCount, + href, } = this.props; const { @@ -130,6 +132,21 @@ export default class IconButton extends React.PureComponent { style.width = 'auto'; } + let contents = ( + + + ); + + if (href) { + contents = ( + + {contents} + + ); + } + return ( ); } diff --git a/app/javascript/flavours/glitch/components/inline_account.js b/app/javascript/flavours/glitch/components/inline_account.js new file mode 100644 index 000000000..2ef1f52cc --- /dev/null +++ b/app/javascript/flavours/glitch/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 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/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/flavours/glitch/components/loading_indicator.js b/app/javascript/flavours/glitch/components/loading_indicator.js index d6a5adb6f..59f721c50 100644 --- a/app/javascript/flavours/glitch/components/loading_indicator.js +++ b/app/javascript/flavours/glitch/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/flavours/glitch/components/media_attachments.js b/app/javascript/flavours/glitch/components/media_attachments.js new file mode 100644 index 000000000..c8d133f09 --- /dev/null +++ b/app/javascript/flavours/glitch/components/media_attachments.js @@ -0,0 +1,119 @@ +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 'flavours/glitch/util/async-components'; +import Bundle from 'flavours/glitch/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, + revealed: PropTypes.bool, + }; + + 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, revealed } = 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/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index 7b5a630e5..0595f6a0e 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -18,6 +18,7 @@ export default class ModalRoot extends React.PureComponent { b: PropTypes.number, }), noEsc: PropTypes.bool, + 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/flavours/glitch/components/relative_timestamp.js b/app/javascript/flavours/glitch/components/relative_timestamp.js index 711181dcd..512480339 100644 --- a/app/javascript/flavours/glitch/components/relative_timestamp.js +++ b/app/javascript/flavours/glitch/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 (
    diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index bee06e64c..0649fe1ca 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import spring from 'react-motion/lib/spring'; -import Toggle from 'react-toggle'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; @@ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent icon: PropTypes.string, meta: PropTypes.node, name: PropTypes.string.isRequired, - on: PropTypes.bool, text: PropTypes.node, })), onChange: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, style: PropTypes.object, value: PropTypes.string, + renderItemContents: PropTypes.func, openedViaKeyboard: PropTypes.bool, + closeOnChange: PropTypes.bool, }; static defaultProps = { style: {}, + closeOnChange: true, }; state = { @@ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent document.removeEventListener('touchend', this.handleDocumentClick, withPassive); } - handleClick = (name, e) => { + handleClick = (e) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + const { onChange, onClose, + closeOnChange, items, } = this.props; - const { on } = this.props.items.find(item => item.name === name); + const { name } = this.props.items[i]; e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined')) { + if (closeOnChange) { onClose(); } onChange(name); @@ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } } - handleKeyDown = (name, e) => { + handleKeyDown = (e) => { + const index = Number(e.currentTarget.getAttribute('data-index')); const { items } = this.props; - const index = items.findIndex(item => { - return (item.name === name); - }); let element = null; switch(e.key) { @@ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent if (element) { element.focus(); - this.handleChange(element.getAttribute('data-index')); + this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); e.preventDefault(); e.stopPropagation(); } @@ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent this.focusedItem = c; } - renderItem = (item) => { - const { name, icon, meta, on, text } = item; + renderItem = (item, i) => { + const { name, icon, meta, text } = item; const active = (name === (this.props.value || this.state.value)); - const computedClass = classNames('composer--options--dropdown--content--item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); + const computedClass = classNames('composer--options--dropdown--content--item', { active }); - let prefix = null; + let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); - if (on !== null && typeof on !== 'undefined') { - prefix = ; - } else if (icon) { - prefix = + if (!contents) { + contents = ( + + {icon && } + +
    + {text} + {meta} +
    +
    + ); } return (
    - {prefix} - -
    - {text} - {meta} -
    + {contents}
    ); } @@ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, }} > - {!!items && items.map(item => this.renderItem(item))} + {!!items && items.map((item, i) => this.renderItem(item, i))}
    )} diff --git a/app/javascript/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js index c6e4cc36b..95add2027 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ b/app/javascript/flavours/glitch/features/compose/components/header.js @@ -119,7 +119,7 @@ class Header extends ImmutablePureComponent { ({ checked: state.getIn(['compose', 'advanced_options', name]) })) +class ToggleOption extends ImmutablePureComponent { + + static propTypes = { + name: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChangeAdvancedOption: PropTypes.func.isRequired, + }; + + handleChange = () => { + this.props.onChangeAdvancedOption(this.props.name); + }; + + render() { + const { meta, text, checked } = this.props; + + return ( + + + +
    + {text} + {meta} +
    +
    + ); + } + +} + export default @injectIntl class ComposerOptions extends ImmutablePureComponent { @@ -106,6 +138,7 @@ class ComposerOptions extends ImmutablePureComponent { resetFileKey: PropTypes.number, spoiler: PropTypes.bool, showContentTypeChoice: PropTypes.bool, + isEditing: PropTypes.bool, }; // Handles file selection. @@ -141,6 +174,13 @@ class ComposerOptions extends ImmutablePureComponent { this.fileElement = fileElement; } + renderToggleItemContents = (item) => { + const { onChangeAdvancedOption } = this.props; + const { name, meta, text } = item; + + return ; + }; + // Rendering. render () { const { @@ -152,7 +192,6 @@ class ComposerOptions extends ImmutablePureComponent { hasMedia, allowPoll, hasPoll, - intl, onChangeAdvancedOption, onChangeContentType, onChangeVisibility, @@ -164,23 +203,25 @@ class ComposerOptions extends ImmutablePureComponent { resetFileKey, spoiler, showContentTypeChoice, + isEditing, + intl: { formatMessage }, } = this.props; const contentTypeItems = { plain: { icon: 'file-text', name: 'text/plain', - text: , + text: formatMessage(messages.plain), }, html: { icon: 'code', name: 'text/html', - text: , + text: formatMessage(messages.html), }, markdown: { icon: 'arrow-circle-down', name: 'text/markdown', - text: , + text: formatMessage(messages.markdown), }, }; @@ -204,18 +245,18 @@ class ComposerOptions extends ImmutablePureComponent { { icon: 'cloud-upload', name: 'upload', - text: , + text: formatMessage(messages.upload), }, { icon: 'paint-brush', name: 'doodle', - text: , + text: formatMessage(messages.doodle), }, ]} onChange={this.handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.attach)} + title={formatMessage(messages.attach)} /> {!!pollLimits && ( )}
    )} @@ -262,31 +303,31 @@ class ComposerOptions extends ImmutablePureComponent { ariaControls='glitch.composer.spoiler.input' label='CW' onClick={onToggleSpoiler} - title={intl.formatMessage(messages.spoiler)} + title={formatMessage(messages.spoiler)} /> )} !!value)} - disabled={disabled} + disabled={disabled || isEditing} icon='ellipsis-h' items={advancedOptions ? [ { - meta: , + meta: formatMessage(messages.local_only_long), name: 'do_not_federate', - on: advancedOptions.get('do_not_federate'), - text: , + text: formatMessage(messages.local_only_short), }, { - meta: , + meta: formatMessage(messages.threaded_mode_long), name: 'threaded_mode', - on: advancedOptions.get('threaded_mode'), - text: , + text: formatMessage(messages.threaded_mode_short), }, ] : null} onChange={onChangeAdvancedOption} + renderItemContents={this.renderToggleItemContents} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.advanced_options_icon_title)} + title={formatMessage(messages.advanced_options_icon_title)} + closeOnChange={false} />
    ); diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js index 39f7c7bd1..c8e783d22 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js @@ -1,46 +1,19 @@ import PropTypes from 'prop-types'; import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import Dropdown from './dropdown'; const messages = defineMessages({ - change_privacy: { - defaultMessage: 'Adjust status privacy', - id: 'privacy.change', - }, - direct_long: { - defaultMessage: 'Visible for mentioned users only', - id: 'privacy.direct.long', - }, - direct_short: { - defaultMessage: 'Direct', - id: 'privacy.direct.short', - }, - private_long: { - defaultMessage: 'Visible for followers only', - id: 'privacy.private.long', - }, - private_short: { - defaultMessage: 'Followers-only', - id: 'privacy.private.short', - }, - public_long: { - defaultMessage: 'Visible for all, shown in public timelines', - id: 'privacy.public.long', - }, - public_short: { - defaultMessage: 'Public', - id: 'privacy.public.short', - }, - unlisted_long: { - defaultMessage: 'Visible for all, but not in public timelines', - id: 'privacy.unlisted.long', - }, - unlisted_short: { - defaultMessage: 'Unlisted', - id: 'privacy.unlisted.short', - }, + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); export default @injectIntl @@ -53,40 +26,40 @@ class PrivacyDropdown extends React.PureComponent { value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, noDirect: PropTypes.bool, - noModal: PropTypes.bool, container: PropTypes.func, + disabled: PropTypes.bool, intl: PropTypes.object.isRequired, }; render () { - const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props; + const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props; // We predefine our privacy items so that we can easily pick the // dropdown icon later. const privacyItems = { direct: { icon: 'envelope', - meta: , + meta: formatMessage(messages.direct_long), name: 'direct', - text: , + text: formatMessage(messages.direct_short), }, private: { icon: 'lock', - meta: , + meta: formatMessage(messages.private_long), name: 'private', - text: , + text: formatMessage(messages.private_short), }, public: { icon: 'globe', - meta: , + meta: formatMessage(messages.public_long), name: 'public', - text: , + text: formatMessage(messages.public_short), }, unlisted: { icon: 'unlock', - meta: , + meta: formatMessage(messages.unlisted_long), name: 'unlisted', - text: , + text: formatMessage(messages.unlisted_short), }, }; @@ -104,9 +77,8 @@ class PrivacyDropdown extends React.PureComponent { onChange={onChange} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={intl.formatMessage(messages.change_privacy)} + title={formatMessage(messages.change_privacy)} container={container} - noModal={noModal} value={value} /> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index 1531dcaa9..9a8c0f510 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -2,7 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import { length } from 'stringz'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -23,6 +23,7 @@ const messages = defineMessages({ defaultMessage: '{publish}!', id: 'compose_form.publish_loud', }, + saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, }); export default @injectIntl @@ -36,6 +37,7 @@ class Publisher extends ImmutablePureComponent { onSubmit: PropTypes.func, privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), + isEditing: PropTypes.bool, }; handleSubmit = () => { @@ -43,7 +45,7 @@ class Publisher extends ImmutablePureComponent { }; render () { - const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props; + const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; const diff = maxChars - length(countText || ''); const computedClass = classNames('composer--publisher', { @@ -51,63 +53,37 @@ class Publisher extends ImmutablePureComponent { over: diff < 0, }); + const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' }; + + let publishText; + if (isEditing) { + publishText = intl.formatMessage(messages.saveChanges); + } else if (privacy === 'private' || privacy === 'direct') { + const iconId = privacyIcons[privacy]; + publishText = ( + + {intl.formatMessage(messages.publish)} + + ); + } else { + publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + } + return (
    - {sideArm && sideArm !== 'none' ? ( + {sideArm && !isEditing && sideArm !== 'none' ? ( - + {!isEditingStatus && ()}
    + + {(media.get('description') || '').length === 0 && ( +
    + +
    + )}
    )} diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index 8eff8a36b..a037bbbcc 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -51,6 +51,7 @@ function mapStateToProps (state) { focusDate: state.getIn(['compose', 'focusDate']), caretPosition: state.getIn(['compose', 'caretPosition']), isSubmitting: state.getIn(['compose', 'is_submitting']), + isEditing: state.getIn(['compose', 'id']) !== null, isChangingUpload: state.getIn(['compose', 'is_changing_upload']), isUploading: state.getIn(['compose', 'is_uploading']), layout: state.getIn(['local_settings', 'layout']), diff --git a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js index 395a9aa5b..dd6899be4 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js @@ -1,14 +1,24 @@ import { connect } from 'react-redux'; import { cancelReplyCompose } from 'flavours/glitch/actions/compose'; -import { makeGetStatus } from 'flavours/glitch/selectors'; import ReplyIndicator from '../components/reply_indicator'; -function makeMapStateToProps (state) { - const inReplyTo = state.getIn(['compose', 'in_reply_to']); +const makeMapStateToProps = () => { + const mapStateToProps = state => { + let statusId = state.getIn(['compose', 'id'], null); + let editing = true; - return { - status: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null, + if (statusId === null) { + statusId = state.getIn(['compose', 'in_reply_to']); + editing = false; + } + + return { + status: state.getIn(['statuses', statusId]), + editing, + }; }; + + return mapStateToProps; }; const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js index f3ca4ce7b..d6256fe96 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -5,6 +5,7 @@ import { submitCompose } from 'flavours/glitch/actions/compose'; const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), + isEditingStatus: state.getIn(['compose', 'id']) !== null, }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js index 2a3fd1ecf..c9ef5850c 100644 --- a/app/javascript/flavours/glitch/features/directory/components/account_card.js +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -7,31 +7,28 @@ import { makeGetAccount } from 'flavours/glitch/selectors'; import Avatar from 'flavours/glitch/components/avatar'; import DisplayName from 'flavours/glitch/components/display_name'; import Permalink from 'flavours/glitch/components/permalink'; -import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; -import IconButton from 'flavours/glitch/components/icon_button'; +import Button from 'flavours/glitch/components/button'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state'; import ShortNumber from 'flavours/glitch/components/short_number'; import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, } from 'flavours/glitch/actions/accounts'; import { openModal } from 'flavours/glitch/actions/modal'; -import { initMuteModal } from 'flavours/glitch/actions/mutes'; +import classNames from 'classnames'; const messages = defineMessages({ - follow: { id: 'account.follow', defaultMessage: 'Follow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - unfollowConfirm: { - id: 'confirmations.unfollow.confirm', - defaultMessage: 'Unfollow', - }, + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, + unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' }, + unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' }, + unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, }); const makeMapStateToProps = () => { @@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onBlock(account) { if (account.getIn(['relationship', 'blocking'])) { dispatch(unblockAccount(account.get('id'))); - } else { - dispatch(blockAccount(account.get('id'))); } }, onMute(account) { if (account.getIn(['relationship', 'muting'])) { dispatch(unmuteAccount(account.get('id'))); - } else { - dispatch(initMuteModal(account)); } }, + }); export default @@ -138,130 +132,92 @@ class AccountCard extends ImmutablePureComponent { handleMute = () => { this.props.onMute(this.props.account); - }; + } + + handleEditProfile = () => { + window.open('/settings/profile', '_blank'); + } render() { const { account, intl } = this.props; - let buttons; + let actionBtn; - if ( - account.get('id') !== me && - account.get('relationship', null) !== null - ) { - const following = account.getIn(['relationship', 'following']); - const requested = account.getIn(['relationship', 'requested']); - const blocking = account.getIn(['relationship', 'blocking']); - const muting = account.getIn(['relationship', 'muting']); - - if (requested) { - buttons = ( - - ); - } else if (blocking) { - buttons = ( - - ); - } else if (muting) { - buttons = ( - - ); - } else if (!account.get('moved') || following) { - buttons = ( - - ); + if (me !== account.get('id')) { + if (!account.get('relationship')) { // Wait until the relationship is loaded + actionBtn = ''; + } else if (account.getIn(['relationship', 'requested'])) { + actionBtn = +
    + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/report/comment.js b/app/javascript/flavours/glitch/features/report/comment.js new file mode 100644 index 000000000..ec261afcb --- /dev/null +++ b/app/javascript/flavours/glitch/features/report/comment.js @@ -0,0 +1,83 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; +import Button from 'flavours/glitch/components/button'; +import Toggle from 'react-toggle'; + +const messages = defineMessages({ + placeholder: { id: 'report.placeholder', defaultMessage: 'Type or paste additional comments' }, +}); + +export default @injectIntl +class Comment extends React.PureComponent { + + static propTypes = { + onSubmit: PropTypes.func.isRequired, + comment: PropTypes.string.isRequired, + onChangeComment: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + isSubmitting: PropTypes.bool, + forward: PropTypes.bool, + isRemote: PropTypes.bool, + domain: PropTypes.string, + onChangeForward: PropTypes.func.isRequired, + }; + + handleClick = () => { + const { onSubmit } = this.props; + onSubmit(); + }; + + handleChange = e => { + const { onChangeComment } = this.props; + onChangeComment(e.target.value); + }; + + handleKeyDown = e => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.handleClick(); + } + }; + + handleForwardChange = e => { + const { onChangeForward } = this.props; + onChangeForward(e.target.checked); + }; + + render () { + const { comment, isRemote, forward, domain, isSubmitting, intl } = this.props; + + return ( + +

    + +