mirror of https://framagit.org/tykayn/mastodon.git
Merge branch 'merged-master' into 'master'
Merged master after 3.2 See merge request tykayn/mastodon!1
This commit is contained in:
commit
4a48b6e172
|
@ -5,12 +5,13 @@ aliases:
|
|||
docker:
|
||||
- image: circleci/ruby:2.7-buster-node
|
||||
environment: &ruby_environment
|
||||
BUNDLE_JOBS: 3
|
||||
BUNDLE_RETRY: 3
|
||||
BUNDLE_APP_CONFIG: ./.bundle/
|
||||
BUNDLE_PATH: ./vendor/bundle/
|
||||
DB_HOST: localhost
|
||||
DB_USER: root
|
||||
RAILS_ENV: test
|
||||
PARALLEL_TEST_PROCESSORS: 4
|
||||
ALLOW_NOPAM: true
|
||||
CONTINUOUS_INTEGRATION: true
|
||||
DISABLE_SIMPLECOV: true
|
||||
|
@ -32,9 +33,9 @@ aliases:
|
|||
- &restore_ruby_dependencies
|
||||
restore_cache:
|
||||
keys:
|
||||
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
- v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
||||
- v2-ruby-dependencies-
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
||||
- v3-ruby-dependencies-
|
||||
|
||||
- &install_steps
|
||||
steps:
|
||||
|
@ -42,11 +43,13 @@ aliases:
|
|||
- *attach_workspace
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v1-node-dependencies-
|
||||
- run: yarn install --frozen-lockfile
|
||||
- v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
- v2-node-dependencies-
|
||||
- run:
|
||||
name: Install yarn dependencies
|
||||
command: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
key: v1-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ./node_modules/
|
||||
- *persist_to_workspace
|
||||
|
@ -57,27 +60,28 @@ aliases:
|
|||
command: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||
|
||||
## TODO: FIX THESE BUSTER DEPENDANCES
|
||||
sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
|
||||
sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
|
||||
sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
|
||||
sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb
|
||||
|
||||
- &install_ruby_dependencies
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
||||
- run:
|
||||
name: Set Ruby version
|
||||
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
||||
- *restore_ruby_dependencies
|
||||
- run: bundle config set clean 'true'
|
||||
- run: bundle config set deployment 'true'
|
||||
- run: bundle config set with 'pam_authentication'
|
||||
- run: bundle config set without 'development production'
|
||||
- run: bundle config set frozen 'true'
|
||||
- run: bundle install --jobs 16 --retry 3 && bundle clean
|
||||
- run:
|
||||
name: Set bundler settings
|
||||
command: |
|
||||
bundle config clean 'true'
|
||||
bundle config deployment 'true'
|
||||
bundle config with 'pam_authentication'
|
||||
bundle config without 'development production'
|
||||
bundle config frozen 'true'
|
||||
- run:
|
||||
name: Install bundler dependencies
|
||||
command: bundle check || (bundle install && bundle clean)
|
||||
- save_cache:
|
||||
key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
||||
paths:
|
||||
- ./.bundle/
|
||||
- ./vendor/bundle/
|
||||
|
@ -88,17 +92,26 @@ aliases:
|
|||
- ./mastodon/vendor/bundle/
|
||||
|
||||
- &test_steps
|
||||
parallelism: 4
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: sudo apt-get install -y ffmpeg
|
||||
- run:
|
||||
name: Prepare Tests
|
||||
command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
|
||||
name: Install FFMPEG
|
||||
command: sudo apt-get install -y ffmpeg
|
||||
- run:
|
||||
name: Run Tests
|
||||
command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec
|
||||
|
||||
name: Load database schema
|
||||
command: ./bin/rails db:create db:schema:load db:seed
|
||||
- run:
|
||||
name: Run rspec in parallel
|
||||
command: |
|
||||
bundle exec rspec --profile 10 \
|
||||
--format RspecJunitFormatter \
|
||||
--out test_results/rspec.xml \
|
||||
--format progress \
|
||||
$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
|
||||
- store_test_results:
|
||||
path: test_results
|
||||
jobs:
|
||||
install:
|
||||
<<: *defaults
|
||||
|
@ -115,19 +128,14 @@ jobs:
|
|||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
install-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
<<: *install_ruby_dependencies
|
||||
|
||||
build:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: ./bin/rails assets:precompile
|
||||
- run:
|
||||
name: Precompile assets
|
||||
command: ./bin/rails assets:precompile
|
||||
- persist_to_workspace:
|
||||
root: ~/projects/
|
||||
paths:
|
||||
|
@ -149,10 +157,10 @@ jobs:
|
|||
- *install_system_dependencies
|
||||
- run:
|
||||
name: Create database
|
||||
command: ./bin/rails parallel:create
|
||||
command: ./bin/rails db:create
|
||||
- run:
|
||||
name: Run migrations
|
||||
command: ./bin/rails parallel:migrate
|
||||
command: ./bin/rails db:migrate
|
||||
|
||||
test-ruby2.7:
|
||||
<<: *defaults
|
||||
|
@ -178,35 +186,33 @@ jobs:
|
|||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-ruby2.5:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/ruby:2.5-buster-node
|
||||
environment: *ruby_environment
|
||||
- image: circleci/postgres:12.2
|
||||
environment:
|
||||
POSTGRES_USER: root
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
- image: circleci/redis:5-alpine
|
||||
<<: *test_steps
|
||||
|
||||
test-webui:
|
||||
<<: *defaults
|
||||
docker:
|
||||
- image: circleci/node:12-buster
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- run: ./bin/retry yarn test:jest
|
||||
- run:
|
||||
name: Run jest
|
||||
command: yarn test:jest
|
||||
|
||||
check-i18n:
|
||||
<<: *defaults
|
||||
steps:
|
||||
- *attach_workspace
|
||||
- *install_system_dependencies
|
||||
- run: bundle exec i18n-tasks check-normalized
|
||||
- run: bundle exec i18n-tasks unused -l en
|
||||
- run: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- run: bundle exec rake repo:check_locales_files
|
||||
- run:
|
||||
name: Check locale file normalization
|
||||
command: bundle exec i18n-tasks check-normalized
|
||||
- run:
|
||||
name: Check for unused strings
|
||||
command: bundle exec i18n-tasks unused -l en
|
||||
- run:
|
||||
name: Check for wrong string interpolations
|
||||
command: bundle exec i18n-tasks check-consistent-interpolations
|
||||
- run:
|
||||
name: Check that all required locale files exist
|
||||
command: bundle exec rake repo:check_locales_files
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -220,10 +226,6 @@ workflows:
|
|||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- install-ruby2.5:
|
||||
requires:
|
||||
- install
|
||||
- install-ruby2.7
|
||||
- build:
|
||||
requires:
|
||||
- install-ruby2.7
|
||||
|
@ -238,10 +240,6 @@ workflows:
|
|||
requires:
|
||||
- install-ruby2.6
|
||||
- build
|
||||
- test-ruby2.5:
|
||||
requires:
|
||||
- install-ruby2.5
|
||||
- build
|
||||
- test-webui:
|
||||
requires:
|
||||
- install
|
||||
|
|
|
@ -30,7 +30,7 @@ plugins:
|
|||
channel: eslint-6
|
||||
rubocop:
|
||||
enabled: true
|
||||
channel: rubocop-0-76
|
||||
channel: rubocop-0-82
|
||||
sass-lint:
|
||||
enabled: true
|
||||
exclude_patterns:
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
version: 1
|
||||
|
||||
update_configs:
|
||||
- package_manager: "ruby:bundler"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
||||
|
||||
- package_manager: "javascript"
|
||||
directory: "/"
|
||||
update_schedule: "weekly"
|
||||
# Supported update schedule: live daily weekly monthly
|
||||
version_requirement_updates: "auto"
|
||||
# Supported version requirements: auto widen_ranges increase_versions increase_versions_if_necessary
|
||||
allowed_updates:
|
||||
- match:
|
||||
dependency_type: "all"
|
||||
# Supported dependency types: all indirect direct production development
|
||||
update_type: "all"
|
||||
# Supported update types: all security
|
|
@ -1,262 +1,60 @@
|
|||
# Service dependencies
|
||||
# You may set REDIS_URL instead for more advanced options
|
||||
# You may also set REDIS_NAMESPACE to share Redis between multiple Mastodon servers
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
# You may set DATABASE_URL instead for more advanced options
|
||||
DB_HOST=db
|
||||
DB_USER=postgres
|
||||
DB_NAME=postgres
|
||||
DB_PASS=
|
||||
DB_PORT=5432
|
||||
# Optional ElasticSearch configuration
|
||||
# You may also set ES_PREFIX to share the same cluster between multiple Mastodon servers (falls back to REDIS_NAMESPACE if not set)
|
||||
# ES_ENABLED=true
|
||||
# ES_HOST=es
|
||||
# ES_PORT=9200
|
||||
# This is a sample configuration file. You can generate your configuration
|
||||
# with the `rake mastodon:setup` interactive setup wizard, but to customize
|
||||
# your setup even further, you'll need to edit it manually. This sample does
|
||||
# not demonstrate all available configuration options. Please look at
|
||||
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
||||
|
||||
# Federation
|
||||
# Note: Changing LOCAL_DOMAIN at a later time will cause unwanted side effects, including breaking all existing federation.
|
||||
# LOCAL_DOMAIN should *NOT* contain the protocol part of the domain e.g https://example.com.
|
||||
# ----------
|
||||
# This identifies your server and cannot be changed safely later
|
||||
# ----------
|
||||
LOCAL_DOMAIN=example.com
|
||||
|
||||
# Changing LOCAL_HTTPS in production is no longer supported. (Mastodon will always serve https:// links)
|
||||
# Redis
|
||||
# -----
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Use this only if you need to run mastodon on a different domain than the one used for federation.
|
||||
# You can read more about this option on https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Serving_a_different_domain.md
|
||||
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
|
||||
# WEB_DOMAIN=mastodon.example.com
|
||||
# PostgreSQL
|
||||
# ----------
|
||||
DB_HOST=/var/run/postgresql
|
||||
DB_USER=mastodon
|
||||
DB_NAME=mastodon_production
|
||||
DB_PASS=
|
||||
DB_PORT=5432
|
||||
|
||||
# Use this if you want to have several aliases handler@example1.com
|
||||
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
|
||||
# be added. Comma separated values
|
||||
# ALTERNATE_DOMAINS=example1.com,example2.com
|
||||
# ElasticSearch (optional)
|
||||
# ------------------------
|
||||
ES_ENABLED=true
|
||||
ES_HOST=localhost
|
||||
ES_PORT=9200
|
||||
|
||||
# Application secrets
|
||||
# Generate each with the `RAILS_ENV=production bundle exec rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)
|
||||
# Secrets
|
||||
# -------
|
||||
# Make sure to use `rake secret` to generate secrets
|
||||
# -------
|
||||
SECRET_KEY_BASE=
|
||||
OTP_SECRET=
|
||||
|
||||
# VAPID keys (used for push notifications
|
||||
# You can generate the keys using the following command (first is the private key, second is the public one)
|
||||
# You should only generate this once per instance. If you later decide to change it, all push subscription will
|
||||
# be invalidated, requiring the users to access the website again to resubscribe.
|
||||
#
|
||||
# Generate with `RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key` task (`docker-compose run --rm web rake mastodon:webpush:generate_vapid_key` if you use docker compose)
|
||||
#
|
||||
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
|
||||
# Web Push
|
||||
# --------
|
||||
# Generate with `rake mastodon:webpush:generate_vapid_key`
|
||||
# --------
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_PUBLIC_KEY=
|
||||
|
||||
# Registrations
|
||||
# Single user mode will disable registrations and redirect frontpage to the first profile
|
||||
# SINGLE_USER_MODE=true
|
||||
# Prevent registrations with following e-mail domains
|
||||
# EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc
|
||||
# Only allow registrations with the following e-mail domains
|
||||
# EMAIL_DOMAIN_WHITELIST=example1.com|example2.de|etc
|
||||
|
||||
# Optionally change default language
|
||||
# DEFAULT_LOCALE=de
|
||||
|
||||
# E-mail configuration
|
||||
# Note: Mailgun and SparkPost (https://sparkpo.st/smtp) each have good free tiers
|
||||
# If you want to use an SMTP server without authentication (e.g local Postfix relay)
|
||||
# then set SMTP_AUTH_METHOD and SMTP_OPENSSL_VERIFY_MODE to 'none' and
|
||||
# *comment* SMTP_LOGIN and SMTP_PASSWORD (leaving them blank is not enough).
|
||||
# Sending mail
|
||||
# ------------
|
||||
SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
#SMTP_REPLY_TO=
|
||||
#SMTP_DOMAIN= # defaults to LOCAL_DOMAIN
|
||||
#SMTP_DELIVERY_METHOD=smtp # delivery method can also be sendmail
|
||||
#SMTP_AUTH_METHOD=plain
|
||||
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
#SMTP_OPENSSL_VERIFY_MODE=peer
|
||||
#SMTP_ENABLE_STARTTLS_AUTO=true
|
||||
#SMTP_TLS=true
|
||||
SMTP_FROM_ADDRESS=notificatons@example.com
|
||||
|
||||
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
|
||||
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
|
||||
# PAPERCLIP_ROOT_URL=/system
|
||||
|
||||
# Optional asset host for multi-server setups
|
||||
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
|
||||
# if WEB_DOMAIN is not set. For example, the server may have the
|
||||
# following header field:
|
||||
# Access-Control-Allow-Origin: https://example.com/
|
||||
# CDN_HOST=https://assets.example.com
|
||||
|
||||
# S3 (optional)
|
||||
# The attachment host must allow cross origin request from WEB_DOMAIN or
|
||||
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
|
||||
# following header field:
|
||||
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
|
||||
# S3_ENABLED=true
|
||||
# S3_BUCKET=
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_PROTOCOL=http
|
||||
# S3_HOSTNAME=192.168.1.123:9000
|
||||
|
||||
# S3 (Minio Config (optional) Please check Minio instance for details)
|
||||
# The attachment host must allow cross origin request - see the description
|
||||
# above.
|
||||
# S3_ENABLED=true
|
||||
# S3_BUCKET=
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_PROTOCOL=https
|
||||
# S3_HOSTNAME=
|
||||
# S3_ENDPOINT=
|
||||
# S3_SIGNATURE_VERSION=
|
||||
|
||||
# Google Cloud Storage (optional)
|
||||
# Use S3 compatible API. Since GCS does not support Multipart Upload,
|
||||
# increase the value of S3_MULTIPART_THRESHOLD to disable Multipart Upload.
|
||||
# The attachment host must allow cross origin request - see the description
|
||||
# above.
|
||||
# S3_ENABLED=true
|
||||
# AWS_ACCESS_KEY_ID=
|
||||
# AWS_SECRET_ACCESS_KEY=
|
||||
# S3_REGION=
|
||||
# S3_PROTOCOL=https
|
||||
# S3_HOSTNAME=storage.googleapis.com
|
||||
# S3_ENDPOINT=https://storage.googleapis.com
|
||||
# S3_MULTIPART_THRESHOLD=52428801 # 50.megabytes
|
||||
|
||||
# Swift (optional)
|
||||
# The attachment host must allow cross origin request - see the description
|
||||
# above.
|
||||
# SWIFT_ENABLED=true
|
||||
# SWIFT_USERNAME=
|
||||
# For Keystone V3, the value for SWIFT_TENANT should be the project name
|
||||
# SWIFT_TENANT=
|
||||
# SWIFT_PASSWORD=
|
||||
# Some OpenStack V3 providers require PROJECT_ID (optional)
|
||||
# SWIFT_PROJECT_ID=
|
||||
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
|
||||
# issues with token rate-limiting during high load.
|
||||
# SWIFT_AUTH_URL=
|
||||
# SWIFT_CONTAINER=
|
||||
# SWIFT_OBJECT_URL=
|
||||
# SWIFT_REGION=
|
||||
# Defaults to 'default'
|
||||
# SWIFT_DOMAIN_NAME=
|
||||
# Defaults to 60 seconds. Set to 0 to disable
|
||||
# SWIFT_CACHE_TTL=
|
||||
|
||||
# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
|
||||
# S3_ALIAS_HOST=
|
||||
|
||||
# Streaming API integration
|
||||
# STREAMING_API_BASE_URL=
|
||||
|
||||
# Advanced settings
|
||||
# If you need to use pgBouncer, you need to disable prepared statements:
|
||||
# PREPARED_STATEMENTS=false
|
||||
|
||||
# Cluster number setting for streaming API server.
|
||||
# If you comment out following line, cluster number will be `numOfCpuCores - 1`.
|
||||
STREAMING_CLUSTER_NUM=1
|
||||
|
||||
# Docker mastodon user
|
||||
# If you use Docker, you may want to assign UID/GID manually.
|
||||
# UID=1000
|
||||
# GID=1000
|
||||
|
||||
# LDAP authentication (optional)
|
||||
# LDAP_ENABLED=true
|
||||
# LDAP_HOST=localhost
|
||||
# LDAP_PORT=389
|
||||
# LDAP_METHOD=simple_tls
|
||||
# LDAP_BASE=
|
||||
# LDAP_BIND_DN=
|
||||
# LDAP_PASSWORD=
|
||||
# LDAP_UID=cn
|
||||
# LDAP_MAIL=mail
|
||||
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
|
||||
# LDAP_UID_CONVERSION_ENABLED=true
|
||||
# LDAP_UID_CONVERSION_SEARCH=., -
|
||||
# LDAP_UID_CONVERSION_REPLACE=_
|
||||
|
||||
# PAM authentication (optional)
|
||||
# PAM authentication uses for the email generation the "email" pam variable
|
||||
# and optional as fallback PAM_DEFAULT_SUFFIX
|
||||
# The pam environment variable "email" is provided by:
|
||||
# https://github.com/devkral/pam_email_extractor
|
||||
# PAM_ENABLED=true
|
||||
# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
|
||||
# PAM_EMAIL_DOMAIN=example.com
|
||||
# Name of the pam service (pam "auth" section is evaluated)
|
||||
# PAM_DEFAULT_SERVICE=rpam
|
||||
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
|
||||
# PAM_CONTROLLED_SERVICE=rpam
|
||||
|
||||
# Global OAuth settings (optional) :
|
||||
# If you have only one strategy, you may want to enable this
|
||||
# OAUTH_REDIRECT_AT_SIGN_IN=true
|
||||
|
||||
# Optional CAS authentication (cf. omniauth-cas) :
|
||||
# CAS_ENABLED=true
|
||||
# CAS_URL=https://sso.myserver.com/
|
||||
# CAS_HOST=sso.myserver.com/
|
||||
# CAS_PORT=443
|
||||
# CAS_SSL=true
|
||||
# CAS_VALIDATE_URL=
|
||||
# CAS_CALLBACK_URL=
|
||||
# CAS_LOGOUT_URL=
|
||||
# CAS_LOGIN_URL=
|
||||
# CAS_UID_FIELD='user'
|
||||
# CAS_CA_PATH=
|
||||
# CAS_DISABLE_SSL_VERIFICATION=false
|
||||
# CAS_UID_KEY='user'
|
||||
# CAS_NAME_KEY='name'
|
||||
# CAS_EMAIL_KEY='email'
|
||||
# CAS_NICKNAME_KEY='nickname'
|
||||
# CAS_FIRST_NAME_KEY='firstname'
|
||||
# CAS_LAST_NAME_KEY='lastname'
|
||||
# CAS_LOCATION_KEY='location'
|
||||
# CAS_IMAGE_KEY='image'
|
||||
# CAS_PHONE_KEY='phone'
|
||||
|
||||
# Optional SAML authentication (cf. omniauth-saml)
|
||||
# SAML_ENABLED=true
|
||||
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
|
||||
# SAML_ISSUER=https://example.com
|
||||
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
|
||||
# SAML_IDP_CERT=
|
||||
# SAML_IDP_CERT_FINGERPRINT=
|
||||
# SAML_NAME_IDENTIFIER_FORMAT=
|
||||
# SAML_CERT=
|
||||
# SAML_PRIVATE_KEY=
|
||||
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
|
||||
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
|
||||
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
|
||||
# SAML_ATTRIBUTES_STATEMENTS_UID="urn:oid:0.9.2342.19200300.100.1.1"
|
||||
# SAML_ATTRIBUTES_STATEMENTS_EMAIL="urn:oid:1.3.6.1.4.1.5923.1.1.1.6"
|
||||
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME="urn:oid:2.16.840.1.113730.3.1.241"
|
||||
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME="urn:oid:2.5.4.42"
|
||||
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME="urn:oid:2.5.4.4"
|
||||
# SAML_UID_ATTRIBUTE="urn:oid:0.9.2342.19200300.100.1.1"
|
||||
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
|
||||
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=
|
||||
|
||||
# Use HTTP proxy for outgoing request (optional)
|
||||
# http_proxy=http://gateway.local:8118
|
||||
# Access control for hidden service.
|
||||
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true
|
||||
|
||||
# Authorized fetch mode (optional)
|
||||
# Require remote servers to authentify when fetching toots, see
|
||||
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
|
||||
# AUTHORIZED_FETCH=true
|
||||
|
||||
# Whitelist mode (optional)
|
||||
# Only allow federation with whitelisted domains, see
|
||||
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
|
||||
# WHITELIST_MODE=true
|
||||
# File storage (optional)
|
||||
# -----------------------
|
||||
S3_ENABLED=true
|
||||
S3_BUCKET=files.example.com
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
S3_ALIAS_HOST=files.example.com
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
VAGRANT=true
|
||||
LOCAL_DOMAIN=mastodon.local
|
||||
BIND=0.0.0.0
|
||||
DB_HOST=/var/run/postgresql/
|
||||
|
|
|
@ -199,6 +199,11 @@ module.exports = {
|
|||
'import/no-unresolved': 'error',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
|
||||
'promise/catch-or-return': 'error',
|
||||
'promise/catch-or-return': [
|
||||
'error',
|
||||
{
|
||||
allowFinally: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
patreon: mastodon
|
||||
open_collective: mastodon
|
||||
github: [Gargron]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: all
|
||||
|
||||
- package-ecosystem: bundler
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 99
|
||||
allow:
|
||||
- dependency-type: all
|
|
@ -17,31 +17,36 @@
|
|||
/log/*
|
||||
!/log/.keep
|
||||
/tmp
|
||||
coverage
|
||||
public/system
|
||||
public/assets
|
||||
public/packs
|
||||
public/packs-test
|
||||
/coverage
|
||||
/public/system
|
||||
/public/assets
|
||||
/public/packs
|
||||
/public/packs-test
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
node_modules/
|
||||
build/
|
||||
/node_modules/
|
||||
/build/
|
||||
|
||||
# Ignore Vagrant files
|
||||
.vagrant/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
config/deploy/*
|
||||
/config/deploy/*
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
||||
postgres
|
||||
redis
|
||||
elasticsearch
|
||||
/postgres
|
||||
/redis
|
||||
/elasticsearch
|
||||
|
||||
# ignore Helm lockfile, dependency charts, and local values file
|
||||
/chart/Chart.lock
|
||||
/chart/charts/*.tgz
|
||||
/chart/values.yaml
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
@ -58,7 +63,7 @@ yarn-error.log
|
|||
yarn-debug.log
|
||||
|
||||
# Ignore vagrant log files
|
||||
ubuntu-xenial-16.04-cloudimg-console.log
|
||||
*-cloudimg-console.log
|
||||
|
||||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
||||
|
|
|
@ -2,7 +2,7 @@ require:
|
|||
- rubocop-rails
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
TargetRubyVersion: 2.4
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
- 'db/**/*'
|
||||
|
@ -28,6 +28,10 @@ Layout/EmptyLineAfterMagicComment:
|
|||
Layout/SpaceInsideHashLiteralBraces:
|
||||
EnforcedStyle: space
|
||||
|
||||
Lint/UselessAccessModifier:
|
||||
ContextCreatingMethods:
|
||||
- class_methods
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 100
|
||||
|
||||
|
@ -46,7 +50,7 @@ Metrics/ClassLength:
|
|||
Metrics/CyclomaticComplexity:
|
||||
Max: 25
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
AllowURI: true
|
||||
Enabled: false
|
||||
|
||||
|
|
207
AUTHORS.md
207
AUTHORS.md
|
@ -5,62 +5,66 @@ Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
|
|||
and provided thanks to the work of the following contributors:
|
||||
|
||||
* [Gargron](https://github.com/Gargron)
|
||||
* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
|
||||
* [ThibG](https://github.com/ThibG)
|
||||
* [ykzts](https://github.com/ykzts)
|
||||
* [dependabot[bot]](https://github.com/apps/dependabot)
|
||||
* [akihikodaki](https://github.com/akihikodaki)
|
||||
* [dependabot-preview[bot]](https://github.com/apps/dependabot-preview)
|
||||
* [mjankowski](https://github.com/mjankowski)
|
||||
* [unarist](https://github.com/unarist)
|
||||
* [yiskah](https://github.com/yiskah)
|
||||
* [nolanlawson](https://github.com/nolanlawson)
|
||||
* [ysksn](https://github.com/ysksn)
|
||||
* [abcang](https://github.com/abcang)
|
||||
* [ysksn](https://github.com/ysksn)
|
||||
* [mayaeh](https://github.com/mayaeh)
|
||||
* [sorin-davidoi](https://github.com/sorin-davidoi)
|
||||
* [lynlynlynx](https://github.com/lynlynlynx)
|
||||
* [mayaeh](https://github.com/mayaeh)
|
||||
* [m4sk1n](mailto:me@m4sk.in)
|
||||
* [Marcin Mikołajczak](mailto:me@m4sk.in)
|
||||
* [Kjwon15](https://github.com/Kjwon15)
|
||||
* [noellabo](https://github.com/noellabo)
|
||||
* [renatolond](https://github.com/renatolond)
|
||||
* [alpaca-tc](https://github.com/alpaca-tc)
|
||||
* [jeroenpraat](https://github.com/jeroenpraat)
|
||||
* [nclm](https://github.com/nclm)
|
||||
* [ineffyble](https://github.com/ineffyble)
|
||||
* [mabkenar](https://github.com/mabkenar)
|
||||
* [shleeable](https://github.com/shleeable)
|
||||
* [zunda](https://github.com/zunda)
|
||||
* [Masoud Abkenar](mailto:ampbox@gmail.com)
|
||||
* [blackle](https://github.com/blackle)
|
||||
* [Quent-in](https://github.com/Quent-in)
|
||||
* [JantsoP](https://github.com/JantsoP)
|
||||
* [zunda](https://github.com/zunda)
|
||||
* [nullkal](https://github.com/nullkal)
|
||||
* [yookoala](https://github.com/yookoala)
|
||||
* [Sasha-Sorokin](https://github.com/Sasha-Sorokin)
|
||||
* [Aditoo17](https://github.com/Aditoo17)
|
||||
* [Quenty31](https://github.com/Quenty31)
|
||||
* [marek-lach](https://github.com/marek-lach)
|
||||
* [shuheiktgw](https://github.com/shuheiktgw)
|
||||
* [ashfurrow](https://github.com/ashfurrow)
|
||||
* [eramdam](https://github.com/eramdam)
|
||||
* [noellabo](https://github.com/noellabo)
|
||||
* [takayamaki](https://github.com/takayamaki)
|
||||
* [danhunsaker](https://github.com/danhunsaker)
|
||||
* [eramdam](https://github.com/eramdam)
|
||||
* [takayamaki](https://github.com/takayamaki)
|
||||
* [ariasuni](https://github.com/ariasuni)
|
||||
* [masarakki](https://github.com/masarakki)
|
||||
* [ticky](https://github.com/ticky)
|
||||
* [ThisIsMissEm](https://github.com/ThisIsMissEm)
|
||||
* [hinaloe](https://github.com/hinaloe)
|
||||
* [hcmiya](https://github.com/hcmiya)
|
||||
* [stephenburgess8](https://github.com/stephenburgess8)
|
||||
* [Wonderfall](https://github.com/Wonderfall)
|
||||
* [Wonderfall](mailto:wonderfall@targaryen.house)
|
||||
* [matteoaquila](https://github.com/matteoaquila)
|
||||
* [yukimochi](https://github.com/yukimochi)
|
||||
* [palindromordnilap](https://github.com/palindromordnilap)
|
||||
* [rkarabut](https://github.com/rkarabut)
|
||||
* [Artoria2e5](https://github.com/Artoria2e5)
|
||||
* [trwnh](https://github.com/trwnh)
|
||||
* [nightpool](https://github.com/nightpool)
|
||||
* [Artoria2e5](https://github.com/Artoria2e5)
|
||||
* [marrus-sh](https://github.com/marrus-sh)
|
||||
* [hinaloe](https://github.com/hinaloe)
|
||||
* [krainboltgreene](https://github.com/krainboltgreene)
|
||||
* [pfigel](https://github.com/pfigel)
|
||||
* [Aldarone](https://github.com/Aldarone)
|
||||
* [BoFFire](https://github.com/BoFFire)
|
||||
* [Aldarone](https://github.com/Aldarone)
|
||||
* [clworld](https://github.com/clworld)
|
||||
* [MasterGroosha](https://github.com/MasterGroosha)
|
||||
* [dracos](https://github.com/dracos)
|
||||
|
@ -68,52 +72,50 @@ and provided thanks to the work of the following contributors:
|
|||
* [SerCom_KC](mailto:sercom-kc@users.noreply.github.com)
|
||||
* [Sylvhem](https://github.com/Sylvhem)
|
||||
* [MitarashiDango](https://github.com/MitarashiDango)
|
||||
* [angristan](https://github.com/angristan)
|
||||
* [JeanGauthier](https://github.com/JeanGauthier)
|
||||
* [kschaper](https://github.com/kschaper)
|
||||
* [beatrix-bitrot](https://github.com/beatrix-bitrot)
|
||||
* [angristan](https://github.com/angristan)
|
||||
* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
|
||||
* [BenLubar](https://github.com/BenLubar)
|
||||
* [adbelle](https://github.com/adbelle)
|
||||
* [evanminto](https://github.com/evanminto)
|
||||
* [MightyPork](https://github.com/MightyPork)
|
||||
* [ashleyhull-versent](mailto:ashley.hull@versent.com.au)
|
||||
* [ashleyhull-versent](https://github.com/ashleyhull-versent)
|
||||
* [yhirano55](https://github.com/yhirano55)
|
||||
* [rinsuki](https://github.com/rinsuki)
|
||||
* [dunn](https://github.com/dunn)
|
||||
* [devkral](https://github.com/devkral)
|
||||
* [camponez](https://github.com/camponez)
|
||||
* [hugogameiro](https://github.com/hugogameiro)
|
||||
* [SerCom_KC](mailto:szescxz@gmail.com)
|
||||
* [aschmitz](https://github.com/aschmitz)
|
||||
* [trwnh](https://github.com/trwnh)
|
||||
* [devkral](https://github.com/devkral)
|
||||
* [fpiesche](https://github.com/fpiesche)
|
||||
* [hugogameiro](https://github.com/hugogameiro)
|
||||
* [gandaro](https://github.com/gandaro)
|
||||
* [johnsudaar](https://github.com/johnsudaar)
|
||||
* [ariasuni](https://github.com/ariasuni)
|
||||
* [trebmuh](https://github.com/trebmuh)
|
||||
* [rmhasan](https://github.com/rmhasan)
|
||||
* [kedamaDQ](https://github.com/kedamaDQ)
|
||||
* [lindwurm](https://github.com/lindwurm)
|
||||
* [victorhck](mailto:victorhck@geeko.site)
|
||||
* [voidsatisfaction](https://github.com/voidsatisfaction)
|
||||
* [BenLubar](https://github.com/BenLubar)
|
||||
* [hikari-no-yume](https://github.com/hikari-no-yume)
|
||||
* [seefood](https://github.com/seefood)
|
||||
* [jackjennings](https://github.com/jackjennings)
|
||||
* [koyuawsmbrtn](https://github.com/koyuawsmbrtn)
|
||||
* [mfmfuyu](https://github.com/mfmfuyu)
|
||||
* [puckipedia](https://github.com/puckipedia)
|
||||
* [spla](mailto:spla@mastodont.cat)
|
||||
* [expenses](https://github.com/expenses)
|
||||
* [walf443](https://github.com/walf443)
|
||||
* [JoelQ](https://github.com/JoelQ)
|
||||
* [mistydemeo](https://github.com/mistydemeo)
|
||||
* [dunn](https://github.com/dunn)
|
||||
* [Ashley](mailto:expenses@airmail.cc)
|
||||
* [xqus](https://github.com/xqus)
|
||||
* [pfm-eyesightjp](https://github.com/pfm-eyesightjp)
|
||||
* [fakenine](https://github.com/fakenine)
|
||||
* [Shleeble](https://github.com/Shleeble)
|
||||
* [Samy KACIMI](mailto:samy.kacimi@gmail.com)
|
||||
* [tsuwatch](https://github.com/tsuwatch)
|
||||
* [victorhck](https://github.com/victorhck)
|
||||
* [mkljczk](https://github.com/mkljczk)
|
||||
* [manuelviens](https://github.com/manuelviens)
|
||||
* [puckipedia](https://github.com/puckipedia)
|
||||
* [fvh-P](https://github.com/fvh-P)
|
||||
* [rtucker](https://github.com/rtucker)
|
||||
* [Anna e só](mailto:contraexemplos@gmail.com)
|
||||
|
@ -123,6 +125,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [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)
|
||||
|
@ -136,44 +139,53 @@ and provided thanks to the work of the following contributors:
|
|||
* [TheKinrar](https://github.com/TheKinrar)
|
||||
* [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)
|
||||
* [fhemberger](https://github.com/fhemberger)
|
||||
* [Gomasy](https://github.com/Gomasy)
|
||||
* [greysteil](https://github.com/greysteil)
|
||||
* [hencatsmith](https://github.com/hencatsmith)
|
||||
* [d6rkaiz](https://github.com/d6rkaiz)
|
||||
* [Reverite](https://github.com/Reverite)
|
||||
* [JohnD28](https://github.com/JohnD28)
|
||||
* [znz](https://github.com/znz)
|
||||
* [saper](https://github.com/saper)
|
||||
* [Naouak](https://github.com/Naouak)
|
||||
* [pawelngei](https://github.com/pawelngei)
|
||||
* [reneklacan](https://github.com/reneklacan)
|
||||
* [ekiru](https://github.com/ekiru)
|
||||
* [tcitworld](https://github.com/tcitworld)
|
||||
* [geta6](https://github.com/geta6)
|
||||
* [happycoloredbanana](https://github.com/happycoloredbanana)
|
||||
* [leopku](https://github.com/leopku)
|
||||
* [SansPseudoFix](https://github.com/SansPseudoFix)
|
||||
* [salvadorpla](https://github.com/salvadorpla)
|
||||
* [spla](mailto:sp@mastodont.cat)
|
||||
* [tateisu](https://github.com/tateisu)
|
||||
* [tomfhowe](https://github.com/tomfhowe)
|
||||
* [noraworld](https://github.com/noraworld)
|
||||
* [lfuelling](https://github.com/lfuelling)
|
||||
* [theboss](https://github.com/theboss)
|
||||
* [nzws](https://github.com/nzws)
|
||||
* [duxovni](https://github.com/duxovni)
|
||||
* [smorimoto](https://github.com/smorimoto)
|
||||
* [178inaba](https://github.com/178inaba)
|
||||
* [acid-chicken](https://github.com/acid-chicken)
|
||||
* [xgess](https://github.com/xgess)
|
||||
* [alyssais](https://github.com/alyssais)
|
||||
* [aablinov](https://github.com/aablinov)
|
||||
* [stalker314314](https://github.com/stalker314314)
|
||||
* [cutls](https://github.com/cutls)
|
||||
* [dariusk](https://github.com/dariusk)
|
||||
* [huertanix](https://github.com/huertanix)
|
||||
* [genesixx](https://github.com/genesixx)
|
||||
* [eleboucher](https://github.com/eleboucher)
|
||||
* [halkeye](https://github.com/halkeye)
|
||||
* [Hanage999](https://github.com/Hanage999)
|
||||
* [treby](https://github.com/treby)
|
||||
* [jpdevries](https://github.com/jpdevries)
|
||||
* [gdpelican](https://github.com/gdpelican)
|
||||
* [kmichl](https://github.com/kmichl)
|
||||
* [Kurtis Rainbolt-Greene](mailto:me@kurtisrainboltgreene.name)
|
||||
* [saper](https://github.com/saper)
|
||||
* [panarom](https://github.com/panarom)
|
||||
* [Dar13](https://github.com/Dar13)
|
||||
* [nevillepark](https://github.com/nevillepark)
|
||||
* [ornithocoder](https://github.com/ornithocoder)
|
||||
|
@ -181,7 +193,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [pierreozoux](https://github.com/pierreozoux)
|
||||
* [qguv](https://github.com/qguv)
|
||||
* [Ram Lmn](mailto:ramlmn@users.noreply.github.com)
|
||||
* [aurelia-sl](https://github.com/aurelia-sl)
|
||||
* [Sascha](mailto:sascha@serenitylabs.cloud)
|
||||
* [harukasan](https://github.com/harukasan)
|
||||
* [stamak](https://github.com/stamak)
|
||||
* [Technowix](https://github.com/Technowix)
|
||||
|
@ -196,9 +208,9 @@ and provided thanks to the work of the following contributors:
|
|||
* [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)
|
||||
* [luzi82](https://github.com/luzi82)
|
||||
* [duxovni](https://github.com/duxovni)
|
||||
* [slice](https://github.com/slice)
|
||||
* [tmm576](https://github.com/tmm576)
|
||||
* [unsmell](mailto:unsmell@users.noreply.github.com)
|
||||
|
@ -209,13 +221,12 @@ and provided thanks to the work of the following contributors:
|
|||
* [AndreLewin](https://github.com/AndreLewin)
|
||||
* [0xflotus](https://github.com/0xflotus)
|
||||
* [redtachyons](https://github.com/redtachyons)
|
||||
* [acid-chicken](https://github.com/acid-chicken)
|
||||
* [thurloat](https://github.com/thurloat)
|
||||
* [aaribaud](https://github.com/aaribaud)
|
||||
* [pointlessone](https://github.com/pointlessone)
|
||||
* [Andrew](mailto:andrewlchronister@gmail.com)
|
||||
* [aurelien-reeves](https://github.com/aurelien-reeves)
|
||||
* [AnaGelez](https://github.com/AnaGelez)
|
||||
* [elegaanz](https://github.com/elegaanz)
|
||||
* [estuans](https://github.com/estuans)
|
||||
* [dissolve](https://github.com/dissolve)
|
||||
* [PurpleBooth](https://github.com/PurpleBooth)
|
||||
|
@ -227,16 +238,14 @@ and provided thanks to the work of the following contributors:
|
|||
* [muffinista](https://github.com/muffinista)
|
||||
* [cdutson](https://github.com/cdutson)
|
||||
* [farlistener](https://github.com/farlistener)
|
||||
* [dariusk](https://github.com/dariusk)
|
||||
* [DavidLibeau](https://github.com/DavidLibeau)
|
||||
* [dmerejkowsky](https://github.com/dmerejkowsky)
|
||||
* [ddevault](https://github.com/ddevault)
|
||||
* [Fjoerfoks](https://github.com/Fjoerfoks)
|
||||
* [fmauNeko](https://github.com/fmauNeko)
|
||||
* [gloaec](https://github.com/gloaec)
|
||||
* [Gomasy](https://github.com/Gomasy)
|
||||
* [unstabler](https://github.com/unstabler)
|
||||
* [potato4d](https://github.com/potato4d)
|
||||
* [Hanage999](https://github.com/Hanage999)
|
||||
* [h-izumi](https://github.com/h-izumi)
|
||||
* [ErikXXon](https://github.com/ErikXXon)
|
||||
* [ian-kelling](https://github.com/ian-kelling)
|
||||
|
@ -251,13 +260,17 @@ and provided thanks to the work of the following contributors:
|
|||
* [tkbky](https://github.com/tkbky)
|
||||
* [Kaylee](mailto:kaylee@codethat.sucks)
|
||||
* [Kazhnuz](https://github.com/Kazhnuz)
|
||||
* [mkody](https://github.com/mkody)
|
||||
* [connyduck](https://github.com/connyduck)
|
||||
* [LindseyB](https://github.com/LindseyB)
|
||||
* [Lorenz Diener](mailto:halcyon@icosahedron.website)
|
||||
* [alimony](https://github.com/alimony)
|
||||
* [Markus Amalthea Magnuson](mailto:markus.magnuson@gmail.com)
|
||||
* [madmath03](https://github.com/madmath03)
|
||||
* [mig5](https://github.com/mig5)
|
||||
* [moritzheiber](https://github.com/moritzheiber)
|
||||
* [Nathaniel Suchy](mailto:me@lunorian.is)
|
||||
* [ndarville](https://github.com/ndarville)
|
||||
* [NimaBoscarino](https://github.com/NimaBoscarino)
|
||||
* [Abzol](https://github.com/Abzol)
|
||||
* [PatOnTheBack](https://github.com/PatOnTheBack)
|
||||
* [xPaw](https://github.com/xPaw)
|
||||
|
@ -287,16 +300,15 @@ and provided thanks to the work of the following contributors:
|
|||
* [amazedkoumei](https://github.com/amazedkoumei)
|
||||
* [anon5r](https://github.com/anon5r)
|
||||
* [aus-social](https://github.com/aus-social)
|
||||
* [imbsky](https://github.com/imbsky)
|
||||
* [bsky](mailto:me@imbsky.net)
|
||||
* [codl](https://github.com/codl)
|
||||
* [cpsdqs](https://github.com/cpsdqs)
|
||||
* [barzamin](https://github.com/barzamin)
|
||||
* [fhalna](https://github.com/fhalna)
|
||||
* [highemerly](https://github.com/highemerly)
|
||||
* [haoyayoi](https://github.com/haoyayoi)
|
||||
* [ik11235](https://github.com/ik11235)
|
||||
* [kawax](https://github.com/kawax)
|
||||
* [shrft](https://github.com/shrft)
|
||||
* [007lva](https://github.com/007lva)
|
||||
* [mbajur](https://github.com/mbajur)
|
||||
* [matsurai25](https://github.com/matsurai25)
|
||||
|
@ -307,15 +319,18 @@ and provided thanks to the work of the following contributors:
|
|||
* [pinfort](https://github.com/pinfort)
|
||||
* [rbaumert](https://github.com/rbaumert)
|
||||
* [rhoio](https://github.com/rhoio)
|
||||
* [sclaire-1](https://github.com/sclaire-1)
|
||||
* [umonaca](https://github.com/umonaca)
|
||||
* [usagi-f](https://github.com/usagi-f)
|
||||
* [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)
|
||||
* [clarfon](https://github.com/clarfon)
|
||||
* [cygnan](https://github.com/cygnan)
|
||||
* [Awea](https://github.com/Awea)
|
||||
* [halcy](https://github.com/halcy)
|
||||
* [eai04191](https://github.com/eai04191)
|
||||
* [8398a7](https://github.com/8398a7)
|
||||
* [857b](https://github.com/857b)
|
||||
* [insom](https://github.com/insom)
|
||||
|
@ -332,6 +347,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [a2](https://github.com/a2)
|
||||
* [alfiedotwtf](https://github.com/alfiedotwtf)
|
||||
* [0xa](https://github.com/0xa)
|
||||
* [ArisuOngaku](https://github.com/ArisuOngaku)
|
||||
* [virtualpain](https://github.com/virtualpain)
|
||||
* [sapphirus](https://github.com/sapphirus)
|
||||
* [amandavisconti](https://github.com/amandavisconti)
|
||||
|
@ -342,15 +358,22 @@ and provided thanks to the work of the following contributors:
|
|||
* [schas002](https://github.com/schas002)
|
||||
* [contraexemplo](https://github.com/contraexemplo)
|
||||
* [abackstrom](https://github.com/abackstrom)
|
||||
* [arielrodrigues](https://github.com/arielrodrigues)
|
||||
* [orlea](https://github.com/orlea)
|
||||
* [armandfardeau](https://github.com/armandfardeau)
|
||||
* [raboof](https://github.com/raboof)
|
||||
* [jumbosushi](https://github.com/jumbosushi)
|
||||
* [ayumin](https://github.com/ayumin)
|
||||
* [bzg](https://github.com/bzg)
|
||||
* [benediktg](https://github.com/benediktg)
|
||||
* [BastienDurel](https://github.com/BastienDurel)
|
||||
* [li-bei](https://github.com/li-bei)
|
||||
* [Benedikt Geißler](mailto:benedikt@g5r.eu)
|
||||
* [BenisonSebastian](https://github.com/BenisonSebastian)
|
||||
* [blakebarnett](https://github.com/blakebarnett)
|
||||
* [bradj](https://github.com/bradj)
|
||||
* [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)
|
||||
|
@ -358,41 +381,48 @@ and provided thanks to the work of the following contributors:
|
|||
* [chris-martin](https://github.com/chris-martin)
|
||||
* [DoubleMalt](https://github.com/DoubleMalt)
|
||||
* [Moosh-be](https://github.com/Moosh-be)
|
||||
* [cchoi12](https://github.com/cchoi12)
|
||||
* [Motoma](https://github.com/Motoma)
|
||||
* [Christopher Kolstad](mailto:christopher.kolstad@finn.no)
|
||||
* [csu](https://github.com/csu)
|
||||
* [kklleemm](https://github.com/kklleemm)
|
||||
* [colindean](https://github.com/colindean)
|
||||
* [DeeUnderscore](https://github.com/DeeUnderscore)
|
||||
* [dachinat](https://github.com/dachinat)
|
||||
* [multiple-creatures](https://github.com/multiple-creatures)
|
||||
* [shapeshifter-system](https://github.com/shapeshifter-system)
|
||||
* [watilde](https://github.com/watilde)
|
||||
* [daprice](https://github.com/daprice)
|
||||
* [da2x](https://github.com/da2x)
|
||||
* [codesections](https://github.com/codesections)
|
||||
* [dar5hak](https://github.com/dar5hak)
|
||||
* [kant](https://github.com/kant)
|
||||
* [maxolasersquad](https://github.com/maxolasersquad)
|
||||
* [singingwolfboy](https://github.com/singingwolfboy)
|
||||
* [caldwell](https://github.com/caldwell)
|
||||
* [davidcelis](https://github.com/davidcelis)
|
||||
* [divergentdave](https://github.com/divergentdave)
|
||||
* [davefp](https://github.com/davefp)
|
||||
* [yipdw](https://github.com/yipdw)
|
||||
* [debanshuk](https://github.com/debanshuk)
|
||||
* [mascali33](https://github.com/mascali33)
|
||||
* [DerekNonGeneric](https://github.com/DerekNonGeneric)
|
||||
* [dblandin](https://github.com/dblandin)
|
||||
* [Drew Gates](mailto:aranaur@users.noreply.github.com)
|
||||
* [dtschust](https://github.com/dtschust)
|
||||
* [Dryusdan](https://github.com/Dryusdan)
|
||||
* [eai04191](https://github.com/eai04191)
|
||||
* [d3vgru](https://github.com/d3vgru)
|
||||
* [Elizafox](https://github.com/Elizafox)
|
||||
* [enewhuis](https://github.com/enewhuis)
|
||||
* [ericblade](https://github.com/ericblade)
|
||||
* [mikoim](https://github.com/mikoim)
|
||||
* [espenronnevik](https://github.com/espenronnevik)
|
||||
* [Expenses](mailto:expenses@airmail.cc)
|
||||
* [fabianonline](https://github.com/fabianonline)
|
||||
* [Finariel](https://github.com/Finariel)
|
||||
* [siuying](https://github.com/siuying)
|
||||
* [zoc](https://github.com/zoc)
|
||||
* [fwenzel](https://github.com/fwenzel)
|
||||
* [gabrielrumiranda](https://github.com/gabrielrumiranda)
|
||||
* [GenbuHase](https://github.com/GenbuHase)
|
||||
* [nilsding](https://github.com/nilsding)
|
||||
* [hattori6789](https://github.com/hattori6789)
|
||||
|
@ -401,6 +431,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [myfreeweb](https://github.com/myfreeweb)
|
||||
* [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)
|
||||
|
@ -421,46 +452,49 @@ and provided thanks to the work of the following contributors:
|
|||
* [jack-michaud](https://github.com/jack-michaud)
|
||||
* [Floppy](https://github.com/Floppy)
|
||||
* [loomchild](https://github.com/loomchild)
|
||||
* [jglauche](https://github.com/jglauche)
|
||||
* [jenkr55](https://github.com/jenkr55)
|
||||
* [hyenagirl64](https://github.com/hyenagirl64)
|
||||
* [press5](https://github.com/press5)
|
||||
* [TrollDecker](https://github.com/TrollDecker)
|
||||
* [jmontane](https://github.com/jmontane)
|
||||
* [jonathanklee](https://github.com/jonathanklee)
|
||||
* [jguerder](https://github.com/jguerder)
|
||||
* [Jehops](https://github.com/Jehops)
|
||||
* [joshuap](https://github.com/joshuap)
|
||||
* [Tiwy57](https://github.com/Tiwy57)
|
||||
* [xuv](https://github.com/xuv)
|
||||
* [Jnsll](https://github.com/Jnsll)
|
||||
* [j0k3r](https://github.com/j0k3r)
|
||||
* [KEINOS](https://github.com/KEINOS)
|
||||
* [futoase](https://github.com/futoase)
|
||||
* [pot8to](https://github.com/pot8to)
|
||||
* [Jonathan Klee](mailto:klee.jonathan@gmail.com)
|
||||
* [Jordan Guerder](mailto:jguerder@fr.pulseheberg.net)
|
||||
* [Joseph Mingrone](mailto:jehops@users.noreply.github.com)
|
||||
* [Joshua Wood](mailto:josh@joshuawood.net)
|
||||
* [Julien](mailto:tiwy57@users.noreply.github.com)
|
||||
* [Julien Deswaef](mailto:juego@requiem4tv.com)
|
||||
* [June Sallou](mailto:jnsll@users.noreply.github.com)
|
||||
* [Jérémy Benoist](mailto:j0k3r@users.noreply.github.com)
|
||||
* [KEINOS](mailto:github@keinos.com)
|
||||
* [Keiji Matsuzaki](mailto:futoase@gmail.com)
|
||||
* [Kevin Liu](mailto:kevin@potatofrom.space)
|
||||
* [Kit Redgrave](mailto:qwertyitis@gmail.com)
|
||||
* [Knut Erik](mailto:abjectio@users.noreply.github.com)
|
||||
* [mkody](https://github.com/mkody)
|
||||
* [k0ta0uchi](https://github.com/k0ta0uchi)
|
||||
* [KrzysiekJ](https://github.com/KrzysiekJ)
|
||||
* [Kota Ouchi](mailto:k0ta0uchi@gmail.com)
|
||||
* [Krzysztof Jurewicz](mailto:krzysztof.jurewicz@gmail.com)
|
||||
* [Leo Wzukw](mailto:leowzukw@users.noreply.github.com)
|
||||
* [Tak](https://github.com/Tak)
|
||||
* [cacheflow](https://github.com/cacheflow)
|
||||
* [ldidry](https://github.com/ldidry)
|
||||
* [jemus42](https://github.com/jemus42)
|
||||
* [lfuelling](https://github.com/lfuelling)
|
||||
* [Grabacr07](https://github.com/Grabacr07)
|
||||
* [mistermantas](https://github.com/mistermantas)
|
||||
* [MareenaKunjachan](https://github.com/MareenaKunjachan)
|
||||
* [mareklach](https://github.com/mareklach)
|
||||
* [wirehack7](https://github.com/wirehack7)
|
||||
* [martymcguire](https://github.com/martymcguire)
|
||||
* [marvinkopf](https://github.com/marvinkopf)
|
||||
* [otsune](https://github.com/otsune)
|
||||
* [mbugowski](https://github.com/mbugowski)
|
||||
* [Leonie](mailto:62470640+bubblineyuri@users.noreply.github.com)
|
||||
* [Levi Bard](mailto:taktaktaktaktaktaktaktaktaktak@gmail.com)
|
||||
* [Lex Alexander](mailto:l.alexander10@gmail.com)
|
||||
* [Lorenz Diener](mailto:lorenzd@gmail.com)
|
||||
* [Luc Didry](mailto:ldidry@users.noreply.github.com)
|
||||
* [Lukas Burk](mailto:jemus42@users.noreply.github.com)
|
||||
* [Manato Kameya](mailto:grabacr07+github@gmail.com)
|
||||
* [Mantas](mailto:mistermantas@users.noreply.github.com)
|
||||
* [Marcin Mikołajczak](mailto:me@mkljczk.pl)
|
||||
* [Mareena Kunjachan](mailto:mareenakunjachan@gmail.com)
|
||||
* [Marek Lach](mailto:marek.brohatwack.lach@gmail.com)
|
||||
* [Markus R](mailto:wirehack7@users.noreply.github.com)
|
||||
* [Marty McGuire](mailto:schmartissimo@gmail.com)
|
||||
* [Marvin Kopf](mailto:marvinkopf@posteo.de)
|
||||
* [Masafumi Otsune](mailto:info@otsune.com)
|
||||
* [Matej Ľach](mailto:matejlach@users.noreply.github.com)
|
||||
* [Mateusz Bugowski](mailto:23140767+mbugowski@users.noreply.github.com)
|
||||
* [Mathias B](mailto:10813340+mathias-b@users.noreply.github.com)
|
||||
* [madmath03](https://github.com/madmath03)
|
||||
* [matt-auckland](https://github.com/matt-auckland)
|
||||
* [webroo](https://github.com/webroo)
|
||||
* [Mathieu Brunot](mailto:mb.mathieu.brunot@gmail.com)
|
||||
* [Matt](mailto:matt-auckland@users.noreply.github.com)
|
||||
* [Matt Sweetman](mailto:webroo@gmail.com)
|
||||
* [Matthias Beyer](mailto:mail@beyermatthias.de)
|
||||
* [Matthias Jouan](mailto:matthias.jouan@gmail.com)
|
||||
* [Matthieu Paret](mailto:matthieuparet69@gmail.com)
|
||||
|
@ -512,10 +546,11 @@ and provided thanks to the work of the following contributors:
|
|||
* [S.H](mailto:gamelinks007@gmail.com)
|
||||
* [Sadiq Saif](mailto:staticsafe@users.noreply.github.com)
|
||||
* [Sam Hewitt](mailto:hewittsamuel@gmail.com)
|
||||
* [Sasha Sorokin](mailto:dafri.nochiterov8@gmail.com)
|
||||
* [Sara Aimée Smiseth](mailto:51710585+sarasmiseth@users.noreply.github.com)
|
||||
* [Satoshi KOJIMA](mailto:skoji@mac.com)
|
||||
* [ScienJus](mailto:i@scienjus.com)
|
||||
* [Scott Larkin](mailto:scott@codeclimate.com)
|
||||
* [Scott Sweeny](mailto:scott@ssweeny.net)
|
||||
* [Sebastian Hübner](mailto:imolein@users.noreply.github.com)
|
||||
* [Sebastian Morr](mailto:sebastian@morr.cc)
|
||||
* [Sergei Č](mailto:noiwex1911@gmail.com)
|
||||
|
@ -525,10 +560,12 @@ and provided thanks to the work of the following contributors:
|
|||
* [Shin Kojima](mailto:shin@kojima.org)
|
||||
* [Shouko Yu](mailto:imshouko@gmail.com)
|
||||
* [Sina Mashek](mailto:sina@mashek.xyz)
|
||||
* [Soft. Dev](mailto:24978+nileshkumar@users.noreply.github.com)
|
||||
* [Soshi Kato](mailto:mail@sossii.com)
|
||||
* [Spanky](mailto:2788886+spankyworks@users.noreply.github.com)
|
||||
* [StefOfficiel](mailto:pichard.stephane@free.fr)
|
||||
* [Steven Tappert](mailto:admin@dark-it.net)
|
||||
* [Stéphane Guillou](mailto:stephane.guillou@member.fsf.org)
|
||||
* [Svetlozar Todorov](mailto:svetlik@users.noreply.github.com)
|
||||
* [Sébastien Santoro](mailto:dereckson@espace-win.org)
|
||||
* [Tad Thorley](mailto:phaedryx@users.noreply.github.com)
|
||||
|
@ -536,7 +573,10 @@ and provided thanks to the work of the following contributors:
|
|||
* [Takayuki KUSANO](mailto:github@tkusano.jp)
|
||||
* [TakesxiSximada](mailto:takesxi.sximada@gmail.com)
|
||||
* [Tao Bror Bojlén](mailto:brortao@users.noreply.github.com)
|
||||
* [Taras Gogol](mailto:taras2358@gmail.com)
|
||||
* [Tdxdxoz](mailto:tdxdxoz@gmail.com)
|
||||
* [TheInventrix](mailto:theinventrix@users.noreply.github.com)
|
||||
* [TheMainOne](mailto:50847364+theevilskeleton@users.noreply.github.com)
|
||||
* [Thomas Alberola](mailto:thomas@needacoffee.fr)
|
||||
* [Toby Deshane](mailto:fortyseven@users.noreply.github.com)
|
||||
* [Toby Pinder](mailto:gigitrix@gmail.com)
|
||||
|
@ -563,6 +603,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [Yann Klis](mailto:yann.klis@gmail.com)
|
||||
* [Yağızhan](mailto:35808275+yagizhan49@users.noreply.github.com)
|
||||
* [Yeechan Lu](mailto:wz.bluesnow@gmail.com)
|
||||
* [Your Name](mailto:lorenzd@gmail.com)
|
||||
* [Yusuke Abe](mailto:moonset20@gmail.com)
|
||||
* [Zachary Spector](mailto:logicaldash@gmail.com)
|
||||
* [ZiiX](mailto:ziix@users.noreply.github.com)
|
||||
|
@ -572,6 +613,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [bsky](mailto:git@imbsky.net)
|
||||
* [caesarologia](mailto:lopesgemelli.1@gmail.com)
|
||||
* [cbayerlein](mailto:c.bayerlein@gmail.com)
|
||||
* [chr v1.x](mailto:chr@cybre.space)
|
||||
* [chrolis](mailto:chrolis@users.noreply.github.com)
|
||||
* [cormo](mailto:cormorant2+github@gmail.com)
|
||||
* [d0p1](mailto:dopi-sama@hush.com)
|
||||
|
@ -582,6 +624,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [fusshi-](mailto:dikky1218@users.noreply.github.com)
|
||||
* [gentaro](mailto:gentaroooo@gmail.com)
|
||||
* [gol-cha](mailto:info@mevo.xyz)
|
||||
* [guigeekz](mailto:pattusg@gmail.com)
|
||||
* [hakoai](mailto:hk--76@qa2.so-net.ne.jp)
|
||||
* [haosbvnker](mailto:github@chaosbunker.com)
|
||||
* [ichi_i](mailto:51489410+ichi-i@users.noreply.github.com)
|
||||
|
@ -593,10 +636,11 @@ and provided thanks to the work of the following contributors:
|
|||
* [jooops](mailto:joops@autistici.org)
|
||||
* [jukper](mailto:jukkaperanto@gmail.com)
|
||||
* [jumoru](mailto:jumoru@mailbox.org)
|
||||
* [kaiyou](mailto:pierre@jaury.eu)
|
||||
* [karlyeurl](mailto:karl.yeurl@gmail.com)
|
||||
* [kedama](mailto:32974885+kedamadq@users.noreply.github.com)
|
||||
* [kodai](mailto:shirafuta.kodai@gmail.com)
|
||||
* [kuro5hin](mailto:rusty@kuro5hin.org)
|
||||
* [leo60228](mailto:leo@60228.dev)
|
||||
* [luzpaz](mailto:luzpaz@users.noreply.github.com)
|
||||
* [maxypy](mailto:maxime@mpigou.fr)
|
||||
* [mhe](mailto:mail@marcus-herrmann.com)
|
||||
|
@ -607,21 +651,25 @@ 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)
|
||||
* [rch850](mailto:rich850@gmail.com)
|
||||
* [roikale](mailto:roikale@users.noreply.github.com)
|
||||
* [rysiekpl](mailto:rysiek@hackerspace.pl)
|
||||
* [saturday06](mailto:dyob@lunaport.net)
|
||||
* [scd31](mailto:57571338+scd31@users.noreply.github.com)
|
||||
* [scriptjunkie](mailto:scriptjunkie@scriptjunkie.us)
|
||||
* [seekr](mailto:mario.drs@gmail.com)
|
||||
* [sternenseemann](mailto:git@lukasepple.de)
|
||||
* [sundevour](mailto:31990469+sundevour@users.noreply.github.com)
|
||||
* [syui](mailto:syui@users.noreply.github.com)
|
||||
* [tackeyy](mailto:mailto.takita.yusuke@gmail.com)
|
||||
* [tateisu](mailto:tateisu@gmail.com)
|
||||
* [taicv](mailto:chuvantai@gmail.com)
|
||||
* [tmyt](mailto:shigure@refy.net)
|
||||
* [trevDev()](mailto:trev@trevdev.ca)
|
||||
* [tsia](mailto:github@tsia.de)
|
||||
* [umonaca](mailto:53662960+umonaca@users.noreply.github.com)
|
||||
* [utam0k](mailto:k0ma@utam0k.jp)
|
||||
* [vpzomtrrfrt](mailto:vpzomtrrfrt@gmail.com)
|
||||
* [walfie](mailto:walfington@gmail.com)
|
||||
|
@ -634,6 +682,7 @@ and provided thanks to the work of the following contributors:
|
|||
* [りんすき](mailto:6533808+rinsuki@users.noreply.github.com)
|
||||
* [ヨイツの賢狼ホロ | 3rd style](mailto:horo@yoitsu.moe)
|
||||
* [唐宗勛](mailto:tangzongxun@hotmail.com)
|
||||
* [夕日](mailto:xirikm@gmail.com)
|
||||
* [猫吸血鬼ディフリス / 猫ロキP](mailto:deflis@gmail.com)
|
||||
* [艮 鮟鱇](mailto:ushitora_anqou@yahoo.co.jp)
|
||||
* [西小倉宏信](mailto:nishiko@mindia.jp)
|
||||
|
|
224
CHANGELOG.md
224
CHANGELOG.md
|
@ -3,6 +3,230 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
|
||||
- Add `SMTP_SSL` environment variable ([OmmyZhang](https://github.com/tootsuite/mastodon/pull/14309))
|
||||
- Add hotkey for toggling content warning input in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13987))
|
||||
- **Add e-mail-based sign in challenge for users with disabled 2FA** ([Gargron](https://github.com/tootsuite/mastodon/pull/14013))
|
||||
- If user tries signing in after:
|
||||
- Being inactive for a while
|
||||
- With a previously unknown IP
|
||||
- Without 2FA being enabled
|
||||
- Require to enter a token sent via e-mail before sigining in
|
||||
- Add `limit` param to RSS feeds ([noellabo](https://github.com/tootsuite/mastodon/pull/13743))
|
||||
- Add `visibility` param to share page ([noellabo](https://github.com/tootsuite/mastodon/pull/13023))
|
||||
- Add blurhash to link previews ([ThibG](https://github.com/tootsuite/mastodon/pull/13984), [ThibG](https://github.com/tootsuite/mastodon/pull/14143), [ThibG](https://github.com/tootsuite/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14278), [ThibG](https://github.com/tootsuite/mastodon/pull/14126), [ThibG](https://github.com/tootsuite/mastodon/pull/14261), [ThibG](https://github.com/tootsuite/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
|
||||
- The Card entity in REST API has a new `blurhash` attribute
|
||||
- Add support for `summary` field for media description in ActivityPub ([ThibG](https://github.com/tootsuite/mastodon/pull/13763))
|
||||
- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14031), [noellabo](https://github.com/tootsuite/mastodon/pull/14195))
|
||||
- **Add personal notes for accounts** ([ThibG](https://github.com/tootsuite/mastodon/pull/14148), [Gargron](https://github.com/tootsuite/mastodon/pull/14208), [Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14251))
|
||||
- To clarify, these are notes only you can see, to help you remember details
|
||||
- Notes can be viewed and edited from profiles in web UI
|
||||
- New REST API: `POST /api/v1/accounts/:id/note` with `comment` param
|
||||
- The Relationship entity in REST API has a new `note` attribute
|
||||
- Add Helm chart ([dunn](https://github.com/tootsuite/mastodon/pull/14090), [dunn](https://github.com/tootsuite/mastodon/pull/14256), [dunn](https://github.com/tootsuite/mastodon/pull/14245))
|
||||
- **Add customizable thumbnails for audio and video attachments** ([Gargron](https://github.com/tootsuite/mastodon/pull/14145), [Gargron](https://github.com/tootsuite/mastodon/pull/14244), [Gargron](https://github.com/tootsuite/mastodon/pull/14273), [Gargron](https://github.com/tootsuite/mastodon/pull/14203), [ThibG](https://github.com/tootsuite/mastodon/pull/14255), [ThibG](https://github.com/tootsuite/mastodon/pull/14306))
|
||||
- Metadata (album, artist, etc) is no longer stripped from audio files
|
||||
- Album art is automatically extracted from audio files
|
||||
- Thumbnail can be manually uploaded for both audio and video attachments
|
||||
- Media upload APIs now support `thumbnail` param
|
||||
- On `POST /api/v1/media` and `POST /api/v2/media`
|
||||
- And on `PUT /api/v1/media/:id`
|
||||
- ActivityPub representation of media attachments represents custom thumbnails with an `icon` attribute
|
||||
- **Add color extraction for thumbnails** ([Gargron](https://github.com/tootsuite/mastodon/pull/14209), [ThibG](https://github.com/tootsuite/mastodon/pull/14264))
|
||||
- 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 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/tootsuite/mastodon/pull/14123), [highemerly](https://github.com/tootsuite/mastodon/pull/14292))
|
||||
- Add `tootctl email_domain_blocks` ([tateisu](https://github.com/tootsuite/mastodon/pull/13589), [Gargron](https://github.com/tootsuite/mastodon/pull/14147))
|
||||
- Add "Add new domain block" to header of federation page in admin UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13934))
|
||||
- Add ability to keep emoji picker open with ctrl+click in web UI ([bclindner](https://github.com/tootsuite/mastodon/pull/13896), [noellabo](https://github.com/tootsuite/mastodon/pull/14096))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change `.env.production.sample` to be leaner and cleaner ([Gargron](https://github.com/tootsuite/mastodon/pull/14206))
|
||||
- It was overloaded as de-facto documentation and getting quite crowded
|
||||
- Defer to the actual documentation while still giving a minimal example
|
||||
- Change `tootctl search deploy` to work faster and display progress ([Gargron](https://github.com/tootsuite/mastodon/pull/14300))
|
||||
- Change User-Agent of link preview fetching service to include "Bot" ([Gargron](https://github.com/tootsuite/mastodon/pull/14248))
|
||||
- Some websites may not render OpenGraph tags into HTML if that's not the case
|
||||
- Change behaviour to carry blocks over when someone migrates their followers ([ThibG](https://github.com/tootsuite/mastodon/pull/14144))
|
||||
- Change volume control and download buttons in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14122))
|
||||
- **Change design of audio players in web UI** ([Gargron](https://github.com/tootsuite/mastodon/pull/14095), [ThibG](https://github.com/tootsuite/mastodon/pull/14281), [Gargron](https://github.com/tootsuite/mastodon/pull/14282), [ThibG](https://github.com/tootsuite/mastodon/pull/14118), [Gargron](https://github.com/tootsuite/mastodon/pull/14199))
|
||||
- Change reply filter to never filter own toots in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14128))
|
||||
- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/14132))
|
||||
- Change contrast of flash messages ([cchoi12](https://github.com/tootsuite/mastodon/pull/13892))
|
||||
- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13834))
|
||||
- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/tootsuite/mastodon/pull/13938))
|
||||
- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13954))
|
||||
- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/tootsuite/mastodon/pull/13773), [ThibG](https://github.com/tootsuite/mastodon/pull/13772), [mfmfuyu](https://github.com/tootsuite/mastodon/pull/14020), [ThibG](https://github.com/tootsuite/mastodon/pull/14015))
|
||||
- Change structure of unavailable content section on about page ([ariasuni](https://github.com/tootsuite/mastodon/pull/13930))
|
||||
- Change behaviour to accept ActivityPub activities relayed through group actor ([noellabo](https://github.com/tootsuite/mastodon/pull/14279))
|
||||
|
||||
### Removed
|
||||
|
||||
- Remove the terms "blacklist" and "whitelist" from UX ([Gargron](https://github.com/tootsuite/mastodon/pull/14149), [mayaeh](https://github.com/tootsuite/mastodon/pull/14192))
|
||||
- Environment variables changed (old versions continue to work):
|
||||
- `WHITELIST_MODE` → `LIMITED_FEDERATION_MODE`
|
||||
- `EMAIL_DOMAIN_BLACKLIST` → `EMAIL_DOMAIN_DENYLIST`
|
||||
- `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/tootsuite/mastodon/pull/13695), [noellabo](https://github.com/tootsuite/mastodon/pull/14259))
|
||||
- Remove unnecessary Node.js version upper bound ([ykzts](https://github.com/tootsuite/mastodon/pull/14139))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/14061))
|
||||
- Fix streaming server trying to use empty password to connect to Redis when `REDIS_PASSWORD` is given but blank ([ThibG](https://github.com/tootsuite/mastodon/pull/14135))
|
||||
- Fix being unable to unboost posts when blocked by their author ([ThibG](https://github.com/tootsuite/mastodon/pull/14308))
|
||||
- Fix account domain block not properly unfollowing accounts from domain ([Gargron](https://github.com/tootsuite/mastodon/pull/14304))
|
||||
- Fix removing a domain allow wiping known accounts in open federation mode ([ThibG](https://github.com/tootsuite/mastodon/pull/14298))
|
||||
- Fix blocks and mutes pagination in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14275))
|
||||
- Fix new posts pushing down origin of opened dropdown in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14271))
|
||||
- Fix timeline markers not being saved sometimes ([ThibG](https://github.com/tootsuite/mastodon/pull/13887), [ThibG](https://github.com/tootsuite/mastodon/pull/13889), [ThibG](https://github.com/tootsuite/mastodon/pull/14155))
|
||||
- Fix CSV uploads being rejected ([noellabo](https://github.com/tootsuite/mastodon/pull/13835))
|
||||
- Fix incompatibility with ElasticSearch 7.x ([noellabo](https://github.com/tootsuite/mastodon/pull/13828))
|
||||
- Fix being able to search posts where you're in the target audience but not actively mentioned ([noellabo](https://github.com/tootsuite/mastodon/pull/13829))
|
||||
- Fix non-local posts appearing on local-only hashtag timelines in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/13827))
|
||||
- Fix `tootctl media remove-orphans` choking on unknown files in storage ([Gargron](https://github.com/tootsuite/mastodon/pull/13765))
|
||||
- Fix `tootctl upgrade storage-schema` misbehaving ([Gargron](https://github.com/tootsuite/mastodon/pull/13761), [angristan](https://github.com/tootsuite/mastodon/pull/13768))
|
||||
- Fix it marking records as upgraded even though no files were moved
|
||||
- Fix it not working with S3 storage
|
||||
- Fix it not working with custom emojis
|
||||
- Fix GIF reader raising incorrect exceptions ([ThibG](https://github.com/tootsuite/mastodon/pull/13760))
|
||||
- Fix hashtag search performing account search as well ([ThibG](https://github.com/tootsuite/mastodon/pull/13758))
|
||||
- Fix Webfinger returning wrong status code on malformed or missing param ([ThibG](https://github.com/tootsuite/mastodon/pull/13759))
|
||||
- Fix `rake mastodon:setup` error when some environment variables are set ([ThibG](https://github.com/tootsuite/mastodon/pull/13928))
|
||||
- Fix admin page crashing when trying to block an invalid domain name in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13884))
|
||||
- Fix unsent toot confirmation dialog not popping up in single column mode in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13888))
|
||||
- Fix performance of follow import ([noellabo](https://github.com/tootsuite/mastodon/pull/13836))
|
||||
- Reduce timeout of Webfinger requests to that of other requests
|
||||
- Use circuit breakers to stop hitting unresponsive servers
|
||||
- Avoid hitting servers that are already known to be generally unavailable
|
||||
- Fix filters ignoring media descriptions ([BenLubar](https://github.com/tootsuite/mastodon/pull/13837))
|
||||
- Fix soem actions on custom emojis leading to cryptic errors in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13951))
|
||||
- Fix ActivityPub serialization of replies when some of them are URIs ([ThibG](https://github.com/tootsuite/mastodon/pull/13957))
|
||||
- Fix `rake mastodon:setup` choking on environment variables containing `%` ([ThibG](https://github.com/tootsuite/mastodon/pull/13940))
|
||||
- Fix account redirect confirmation message talking about moved followers ([ThibG](https://github.com/tootsuite/mastodon/pull/13950))
|
||||
- Fix avatars having the wrong size on public detailed status pages ([ThibG](https://github.com/tootsuite/mastodon/pull/14140))
|
||||
- Fix various issues around OpenGraph representation of media ([Gargron](https://github.com/tootsuite/mastodon/pull/14133))
|
||||
- Pages containing audio no longer say "Attached: 1 image" in description
|
||||
- Audio attachments now represented as OpenGraph `og:audio`
|
||||
- The `twitter:player` page now uses Mastodon's proper audio/video player
|
||||
- Audio/video buffered bars now display correctly in audio/video player
|
||||
- Volume and progress bars now respond to movement/move smoother
|
||||
- Fix audio/video/images/cards not reacting to window resizes in web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/14130))
|
||||
- Fix very wide media attachments resulting in too thin a thumbnail in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14127))
|
||||
- Fix crash when merging posts into home feed after following someone ([ThibG](https://github.com/tootsuite/mastodon/pull/14129))
|
||||
- Fix unique username constraint for local users not being enforced in database ([ThibG](https://github.com/tootsuite/mastodon/pull/14099))
|
||||
- Fix unnecessary gap under video modal in web UI ([mfmfuyu](https://github.com/tootsuite/mastodon/pull/14098))
|
||||
- Fix 2FA and sign in token pages not respecting user locale ([mfmfuyu](https://github.com/tootsuite/mastodon/pull/14087))
|
||||
- Fix unapproved users being able to view profiles when in limited-federation mode *and* requiring approval for sign-ups ([ThibG](https://github.com/tootsuite/mastodon/pull/14093))
|
||||
- Fix initial audio volume not corresponding to what's displayed in audio player in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14057))
|
||||
- Fix timelines sometimes jumping when closing modals in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14019))
|
||||
- Fix memory usage of downloading remote files ([Gargron](https://github.com/tootsuite/mastodon/pull/14184), [Gargron](https://github.com/tootsuite/mastodon/pull/14181))
|
||||
- Don't read entire file (up to 40 MB) into memory
|
||||
- Read and write it to temp file in small chunks
|
||||
- Fix inconsistent account header padding in web UI ([trwnh](https://github.com/tootsuite/mastodon/pull/14179))
|
||||
- Fix Thai being skipped from language detection ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/13989))
|
||||
- Since Thai has its own alphabet, it can be detected more reliably
|
||||
- Fix broken hashtag column options styling in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/14247))
|
||||
- Fix pointer cursor being shown on toots that are not clickable in web UI ([arielrodrigues](https://github.com/tootsuite/mastodon/pull/14185))
|
||||
- Fix lock icon not being shown when locking account in profile settings ([ThibG](https://github.com/tootsuite/mastodon/pull/14190))
|
||||
- Fix domain blocks doing work the wrong way around ([ThibG](https://github.com/tootsuite/mastodon/pull/13424))
|
||||
- Instead of suspending accounts one by one, mark all as suspended first (quick)
|
||||
- Only then proceed to start removing their data (slow)
|
||||
- Clear out media attachments in a separate worker (slow)
|
||||
|
||||
## [v3.1.5] - 2020-07-07
|
||||
### Security
|
||||
|
||||
- Fix media attachment enumeration ([ThibG](https://github.com/tootsuite/mastodon/pull/14254))
|
||||
- Change rate limits for various paths ([Gargron](https://github.com/tootsuite/mastodon/pull/14253))
|
||||
- Fix other sessions not being logged out on password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14252))
|
||||
|
||||
## [v3.1.5] - 2020-07-07
|
||||
### Security
|
||||
|
||||
- Fix media attachment enumeration ([ThibG](https://github.com/tootsuite/mastodon/pull/14254))
|
||||
- Change rate limits for various paths ([Gargron](https://github.com/tootsuite/mastodon/pull/14253))
|
||||
- Fix other sessions not being logged out on password change ([Gargron](https://github.com/tootsuite/mastodon/pull/14252))
|
||||
|
||||
## [v3.1.4] - 2020-05-14
|
||||
### Added
|
||||
|
||||
- Add `vi` to available locales ([taicv](https://github.com/tootsuite/mastodon/pull/13542))
|
||||
- Add ability to remove identity proofs from account ([Gargron](https://github.com/tootsuite/mastodon/pull/13682))
|
||||
- Add ability to exclude local content from federated timeline ([noellabo](https://github.com/tootsuite/mastodon/pull/13504), [noellabo](https://github.com/tootsuite/mastodon/pull/13745))
|
||||
- Add `remote` param to `GET /api/v1/timelines/public` REST API
|
||||
- Add `public/remote` / `public:remote` variants to streaming API
|
||||
- "Remote only" option in federated timeline column settings in web UI
|
||||
- Add ability to exclude remote content from hashtag timelines in web UI ([noellabo](https://github.com/tootsuite/mastodon/pull/13502))
|
||||
- No changes to REST API
|
||||
- "Local only" option in hashtag column settings in web UI
|
||||
- Add Capistrano tasks that reload the services after deploying ([berkes](https://github.com/tootsuite/mastodon/pull/12642))
|
||||
- Add `invites_enabled` attribute to `GET /api/v1/instance` in REST API ([ThibG](https://github.com/tootsuite/mastodon/pull/13501))
|
||||
- Add `tootctl emoji export` command ([lfuelling](https://github.com/tootsuite/mastodon/pull/13534))
|
||||
- Add separate cache directory for non-local uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/12821), [Hanage999](https://github.com/tootsuite/mastodon/pull/13593), [mayaeh](https://github.com/tootsuite/mastodon/pull/13551))
|
||||
- Add `tootctl upgrade storage-schema` command to move old non-local uploads to the cache directory
|
||||
- Add buttons to delete header and avatar from profile settings ([sternenseemann](https://github.com/tootsuite/mastodon/pull/13234))
|
||||
- Add emoji graphics and shortcodes from Twemoji 12.1.5 ([DeeUnderscore](https://github.com/tootsuite/mastodon/pull/13021))
|
||||
|
||||
### Changed
|
||||
|
||||
- Change error message when trying to migrate to an account that does not have current account set as an alias to be more clear ([TheEvilSkeleton](https://github.com/tootsuite/mastodon/pull/13746))
|
||||
- Change delivery failure tracking to work with hostnames instead of URLs ([Gargron](https://github.com/tootsuite/mastodon/pull/13437), [noellabo](https://github.com/tootsuite/mastodon/pull/13481), [noellabo](https://github.com/tootsuite/mastodon/pull/13482), [noellabo](https://github.com/tootsuite/mastodon/pull/13535))
|
||||
- Change Content-Security-Policy to not need unsafe-inline style-src ([ThibG](https://github.com/tootsuite/mastodon/pull/13679), [ThibG](https://github.com/tootsuite/mastodon/pull/13692), [ThibG](https://github.com/tootsuite/mastodon/pull/13576), [ThibG](https://github.com/tootsuite/mastodon/pull/13575), [ThibG](https://github.com/tootsuite/mastodon/pull/13438))
|
||||
- Change how RSS items are titled and formatted ([ThibG](https://github.com/tootsuite/mastodon/pull/13592), [ykzts](https://github.com/tootsuite/mastodon/pull/13591))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix dropdown of muted and followed accounts offering option to hide boosts in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13748))
|
||||
- Fix "You are already signed in" alert being shown at wrong times ([ThibG](https://github.com/tootsuite/mastodon/pull/13547))
|
||||
- Fix retrying of failed-to-download media files not actually working ([noellabo](https://github.com/tootsuite/mastodon/pull/13741))
|
||||
- Fix first poll option not being focused when adding a poll in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13740))
|
||||
- Fix `sr` locale being selected over `sr-Latn` ([ThibG](https://github.com/tootsuite/mastodon/pull/13693))
|
||||
- Fix error within error when limiting backtrace to 3 lines ([Gargron](https://github.com/tootsuite/mastodon/pull/13120))
|
||||
- Fix `tootctl media remove-orphans` crashing on "Import" files ([ThibG](https://github.com/tootsuite/mastodon/pull/13685))
|
||||
- Fix regression in `tootctl media remove-orphans` ([Gargron](https://github.com/tootsuite/mastodon/pull/13405))
|
||||
- Fix old unique jobs digests not having been cleaned up ([Gargron](https://github.com/tootsuite/mastodon/pull/13683))
|
||||
- Fix own following/followers not showing muted users ([ThibG](https://github.com/tootsuite/mastodon/pull/13614))
|
||||
- Fix list of followed people ignoring sorting on Follows & Followers page ([taras2358](https://github.com/tootsuite/mastodon/pull/13676))
|
||||
- Fix wrong pgHero Content-Security-Policy when `CDN_HOST` is set ([ThibG](https://github.com/tootsuite/mastodon/pull/13595))
|
||||
- Fix needlessly deduplicating usernames on collisions with remote accounts when signing-up through SAML/CAS ([kaiyou](https://github.com/tootsuite/mastodon/pull/13581))
|
||||
- Fix page incorrectly scrolling when bringing up dropdown menus in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13574))
|
||||
- Fix messed up z-index when NoScript blocks media/previews in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13449))
|
||||
- Fix "See what's happening" page showing public instead of local timeline for logged-in users ([ThibG](https://github.com/tootsuite/mastodon/pull/13499))
|
||||
- Fix not being able to resolve public resources in development environment ([Gargron](https://github.com/tootsuite/mastodon/pull/13505))
|
||||
- Fix uninformative error message when uploading unsupported image files ([ThibG](https://github.com/tootsuite/mastodon/pull/13540))
|
||||
- Fix expanded video player issues in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13541), [eai04191](https://github.com/tootsuite/mastodon/pull/13533))
|
||||
- Fix and refactor keyboard navigation in dropdown menus in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13528))
|
||||
- Fix uploaded image orientation being messed up in some browsers in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13493))
|
||||
- Fix actions log crash when displaying updates of deleted announcements in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13489))
|
||||
- Fix search not working due to proxy settings when using hidden services ([Gargron](https://github.com/tootsuite/mastodon/pull/13488))
|
||||
- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/tootsuite/mastodon/pull/13485), [ThibG](https://github.com/tootsuite/mastodon/pull/13490))
|
||||
- Fix confusing error when failing to add an alias to an unknown account ([ThibG](https://github.com/tootsuite/mastodon/pull/13480))
|
||||
- Fix "Email changed" notification sometimes having wrong e-mail ([ThibG](https://github.com/tootsuite/mastodon/pull/13475))
|
||||
- Fix varioues issues on the account aliases page ([ThibG](https://github.com/tootsuite/mastodon/pull/13452))
|
||||
- Fix API footer link in web UI ([bubblineyuri](https://github.com/tootsuite/mastodon/pull/13441))
|
||||
- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13445))
|
||||
- Fix styling of polls in JS-less fallback on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/13436))
|
||||
- Fix trying to delete already deleted file when post-processing ([Gargron](https://github.com/tootsuite/mastodon/pull/13406))
|
||||
|
||||
### Security
|
||||
|
||||
- Fix Doorkeeper vulnerability that exposed app secret to users who authorized the app and reset secret of the web UI that could have been exposed ([dependabot-preview[bot]](https://github.com/tootsuite/mastodon/pull/13613), [Gargron](https://github.com/tootsuite/mastodon/pull/13688))
|
||||
- For apps that self-register on behalf of every individual user (such as most mobile apps), this is a non-issue
|
||||
- The issue only affects developers of apps who are shared between multiple users, such as server-side apps like cross-posters
|
||||
|
||||
## [v3.1.3] - 2020-04-05
|
||||
### Added
|
||||
|
||||
|
|
12
Dockerfile
12
Dockerfile
|
@ -1,11 +1,11 @@
|
|||
FROM ubuntu:18.04 as build-dep
|
||||
FROM ubuntu:20.04 as build-dep
|
||||
|
||||
# Use bash for the shell
|
||||
SHELL ["bash", "-c"]
|
||||
|
||||
# Install Node v12 (LTS)
|
||||
ENV NODE_VER="12.16.1"
|
||||
RUN ARCH= && \
|
||||
ENV NODE_VER="12.16.3"
|
||||
RUN ARCH= && \
|
||||
dpkgArch="$(dpkg --print-architecture)" && \
|
||||
case "${dpkgArch##*-}" in \
|
||||
amd64) ARCH='x64';; \
|
||||
|
@ -74,7 +74,7 @@ RUN cd /opt/mastodon && \
|
|||
bundle install -j$(nproc) && \
|
||||
yarn install --pure-lockfile
|
||||
|
||||
FROM ubuntu:18.04
|
||||
FROM ubuntu:20.04
|
||||
|
||||
# Copy over all the langs needed for runtime
|
||||
COPY --from=build-dep /opt/node /opt/node
|
||||
|
@ -98,8 +98,8 @@ RUN apt update && \
|
|||
# Install mastodon runtime deps
|
||||
RUN apt -y --no-install-recommends install \
|
||||
libssl1.1 libpq5 imagemagick ffmpeg \
|
||||
libicu60 libprotobuf10 libidn11 libyaml-0-2 \
|
||||
file ca-certificates tzdata libreadline7 && \
|
||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
||||
file ca-certificates tzdata libreadline8 && \
|
||||
apt -y install gcc && \
|
||||
ln -s /opt/mastodon /mastodon && \
|
||||
gem install bundler && \
|
||||
|
|
54
Gemfile
54
Gemfile
|
@ -6,10 +6,10 @@ ruby '>= 2.5.0', '< 3.0.0'
|
|||
gem 'pkg-config', '~> 1.4'
|
||||
|
||||
gem 'puma', '~> 4.3'
|
||||
gem 'rails', '~> 5.2.4.2'
|
||||
gem 'rails', '~> 5.2.4.3'
|
||||
gem 'sprockets', '~> 3.7.2'
|
||||
gem 'thor', '~> 0.20'
|
||||
gem 'rack', '~> 2.2.2'
|
||||
gem 'rack', '~> 2.2.3'
|
||||
|
||||
gem 'thwait', '~> 0.1.0'
|
||||
gem 'e2mmap', '~> 0.1.0'
|
||||
|
@ -17,10 +17,10 @@ gem 'e2mmap', '~> 0.1.0'
|
|||
gem 'hamlit-rails', '~> 0.2'
|
||||
gem 'pg', '~> 1.2'
|
||||
gem 'makara', '~> 0.4'
|
||||
gem 'pghero', '~> 2.4'
|
||||
gem 'pghero', '~> 2.5'
|
||||
gem 'dotenv-rails', '~> 2.7'
|
||||
|
||||
gem 'aws-sdk-s3', '~> 1.61', require: false
|
||||
gem 'aws-sdk-s3', '~> 1.73', require: false
|
||||
gem 'fog-core', '<= 2.1.0'
|
||||
gem 'fog-openstack', '~> 0.3', require: false
|
||||
gem 'paperclip', '~> 6.0'
|
||||
|
@ -48,8 +48,10 @@ gem 'omniauth-cas', '~> 1.1'
|
|||
gem 'omniauth-saml', '~> 1.10'
|
||||
gem 'omniauth', '~> 1.9'
|
||||
|
||||
gem 'color_diff', '~> 0.1'
|
||||
gem 'discard', '~> 1.2'
|
||||
gem 'doorkeeper', '~> 5.3'
|
||||
gem 'doorkeeper', '~> 5.4'
|
||||
gem 'ed25519', '~> 1.2'
|
||||
gem 'fast_blank', '~> 1.0'
|
||||
gem 'fastimage'
|
||||
gem 'goldfinger', '~> 2.1'
|
||||
|
@ -57,12 +59,12 @@ gem 'hiredis', '~> 0.6'
|
|||
gem 'redis-namespace', '~> 1.7'
|
||||
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
|
||||
gem 'htmlentities', '~> 4.3'
|
||||
gem 'http', '~> 4.3'
|
||||
gem 'http', '~> 4.4'
|
||||
gem 'http_accept_language', '~> 2.1'
|
||||
gem 'http_parser.rb', '~> 0.6', git: 'https://github.com/tmm1/http_parser.rb', ref: '54b17ba8c7d8d20a16dfc65d1775241833219cf2', submodules: true
|
||||
gem 'httplog', '~> 1.4.2'
|
||||
gem 'httplog', '~> 1.4.3'
|
||||
gem 'idn-ruby', require: 'idn'
|
||||
gem 'kaminari', '~> 1.1'
|
||||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
||||
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
|
||||
|
@ -75,15 +77,15 @@ gem 'parallel', '~> 1.19'
|
|||
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
|
||||
gem 'pundit', '~> 2.1'
|
||||
gem 'premailer-rails'
|
||||
gem 'rack-attack', '~> 6.2'
|
||||
gem 'rack-attack', '~> 6.3'
|
||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||
gem 'rails-i18n', '~> 5.1'
|
||||
gem 'rails-settings-cached', '~> 0.6'
|
||||
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'redis', '~> 4.2', require: ['redis', 'redis/connection/hiredis']
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'rqrcode', '~> 1.1'
|
||||
gem 'ruby-progressbar', '~> 1.10'
|
||||
gem 'sanitize', '~> 5.1'
|
||||
gem 'sanitize', '~> 5.2'
|
||||
gem 'sidekiq', '~> 6.0'
|
||||
gem 'sidekiq-scheduler', '~> 3.0'
|
||||
gem 'sidekiq-unique-jobs', '~> 6.0'
|
||||
|
@ -93,11 +95,10 @@ gem 'simple_form', '~> 5.0'
|
|||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
||||
gem 'stoplight', '~> 2.2.0'
|
||||
gem 'strong_migrations', '~> 0.6'
|
||||
gem 'tty-command', '~> 0.9', require: false
|
||||
gem 'tty-prompt', '~> 0.21', require: false
|
||||
gem 'twitter-text', '~> 1.14'
|
||||
gem 'tzinfo-data', '~> 1.2019'
|
||||
gem 'webpacker', '~> 4.2'
|
||||
gem 'tzinfo-data', '~> 1.2020'
|
||||
gem 'webpacker', '~> 5.1'
|
||||
gem 'webpush'
|
||||
|
||||
gem 'json-ld'
|
||||
|
@ -108,7 +109,7 @@ group :development, :test do
|
|||
gem 'fabrication', '~> 2.21'
|
||||
gem 'fuubar', '~> 2.5'
|
||||
gem 'i18n-tasks', '~> 0.9', require: false
|
||||
gem 'pry-byebug', '~> 3.8'
|
||||
gem 'pry-byebug', '~> 3.9'
|
||||
gem 'pry-rails', '~> 0.3'
|
||||
gem 'rspec-rails', '~> 4.0'
|
||||
end
|
||||
|
@ -118,33 +119,34 @@ group :production, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'capybara', '~> 3.31'
|
||||
gem 'capybara', '~> 3.33'
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'faker', '~> 2.11'
|
||||
gem 'faker', '~> 2.13'
|
||||
gem 'microformats', '~> 4.2'
|
||||
gem 'rails-controller-testing', '~> 1.0'
|
||||
gem 'rspec-sidekiq', '~> 3.0'
|
||||
gem 'rspec-sidekiq', '~> 3.1'
|
||||
gem 'simplecov', '~> 0.18', require: false
|
||||
gem 'webmock', '~> 3.8'
|
||||
gem 'parallel_tests', '~> 2.32'
|
||||
gem 'parallel_tests', '~> 3.0'
|
||||
gem 'rspec_junit_formatter', '~> 0.4'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'active_record_query_trace', '~> 1.7'
|
||||
gem 'annotate', '~> 3.0'
|
||||
gem 'better_errors', '~> 2.6'
|
||||
gem 'annotate', '~> 3.1'
|
||||
gem 'better_errors', '~> 2.7'
|
||||
gem 'binding_of_caller', '~> 0.7'
|
||||
gem 'bullet', '~> 6.1'
|
||||
gem 'letter_opener', '~> 1.7'
|
||||
gem 'letter_opener_web', '~> 1.4'
|
||||
gem 'memory_profiler'
|
||||
gem 'rubocop', '~> 0.79', require: false
|
||||
gem 'rubocop-rails', '~> 2.4', require: false
|
||||
gem 'rubocop', '~> 0.86', require: false
|
||||
gem 'rubocop-rails', '~> 2.6', require: false
|
||||
gem 'brakeman', '~> 4.8', require: false
|
||||
gem 'bundler-audit', '~> 0.6', require: false
|
||||
gem 'bundler-audit', '~> 0.7', require: false
|
||||
|
||||
gem 'capistrano', '~> 3.12'
|
||||
gem 'capistrano-rails', '~> 1.4'
|
||||
gem 'capistrano', '~> 3.14'
|
||||
gem 'capistrano-rails', '~> 1.5'
|
||||
gem 'capistrano-rbenv', '~> 2.1'
|
||||
gem 'capistrano-yarn', '~> 2.0'
|
||||
|
||||
|
|
429
Gemfile.lock
429
Gemfile.lock
|
@ -31,25 +31,25 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (5.2.4.2)
|
||||
actionpack (= 5.2.4.2)
|
||||
actioncable (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailer (5.2.4.2)
|
||||
actionpack (= 5.2.4.2)
|
||||
actionview (= 5.2.4.2)
|
||||
activejob (= 5.2.4.2)
|
||||
actionmailer (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activejob (= 5.2.4.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (5.2.4.2)
|
||||
actionview (= 5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
actionpack (5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
actionview (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
|
@ -60,20 +60,20 @@ GEM
|
|||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
active_record_query_trace (1.7)
|
||||
activejob (5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
activejob (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
activerecord (5.2.4.2)
|
||||
activemodel (= 5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
activemodel (5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
activerecord (5.2.4.3)
|
||||
activemodel (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
arel (>= 9.0)
|
||||
activestorage (5.2.4.2)
|
||||
actionpack (= 5.2.4.2)
|
||||
activerecord (= 5.2.4.2)
|
||||
activestorage (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
activerecord (= 5.2.4.3)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (5.2.4.2)
|
||||
activesupport (5.2.4.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
@ -82,33 +82,33 @@ GEM
|
|||
public_suffix (>= 2.0.2, < 5.0)
|
||||
airbrussh (1.4.0)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
annotate (3.0.3)
|
||||
annotate (3.1.1)
|
||||
activerecord (>= 3.2, < 7.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
arel (9.0.0)
|
||||
ast (2.4.0)
|
||||
ast (2.4.1)
|
||||
attr_encrypted (3.1.0)
|
||||
encryptor (~> 3.0.0)
|
||||
av (0.9.0)
|
||||
cocaine (~> 0.5.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.286.0)
|
||||
aws-sdk-core (3.92.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.340.0)
|
||||
aws-sdk-core (3.103.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.30.0)
|
||||
aws-sdk-core (~> 3, >= 3.71.0)
|
||||
aws-sdk-kms (1.36.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.61.1)
|
||||
aws-sdk-core (~> 3, >= 3.83.0)
|
||||
aws-sdk-s3 (1.74.0)
|
||||
aws-sdk-core (~> 3, >= 3.102.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.1.1)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
bcrypt (3.1.13)
|
||||
better_errors (2.6.0)
|
||||
better_errors (2.7.1)
|
||||
coderay (>= 1.0.0)
|
||||
erubi (>= 1.0.0)
|
||||
rack (>= 0.9.0)
|
||||
|
@ -118,25 +118,24 @@ GEM
|
|||
ffi (~> 1.10.0)
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.0)
|
||||
browser (4.0.0)
|
||||
brakeman (4.8.2)
|
||||
browser (4.2.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundler-audit (0.6.1)
|
||||
bundler-audit (0.7.0.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 0.18)
|
||||
byebug (11.1.1)
|
||||
capistrano (3.12.1)
|
||||
thor (>= 0.18, < 2)
|
||||
byebug (11.1.3)
|
||||
capistrano (3.14.1)
|
||||
airbrussh (>= 1.0.0)
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (1.3.0)
|
||||
capistrano-bundler (1.6.0)
|
||||
capistrano (~> 3.1)
|
||||
sshkit (~> 1.2)
|
||||
capistrano-rails (1.4.0)
|
||||
capistrano-rails (1.5.0)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-bundler (~> 1.1)
|
||||
capistrano-rbenv (2.1.6)
|
||||
|
@ -144,7 +143,7 @@ GEM
|
|||
sshkit (~> 1.3)
|
||||
capistrano-yarn (2.0.2)
|
||||
capistrano (~> 3.0)
|
||||
capybara (3.31.0)
|
||||
capybara (3.33.0)
|
||||
addressable
|
||||
mini_mime (>= 0.1.3)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -165,16 +164,17 @@ GEM
|
|||
climate_control (0.2.0)
|
||||
cocaine (0.5.8)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
coderay (1.1.2)
|
||||
coderay (1.1.3)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.1.6)
|
||||
connection_pool (2.2.2)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.6)
|
||||
css_parser (1.7.1)
|
||||
addressable
|
||||
debug_inspector (0.0.3)
|
||||
devise (4.7.1)
|
||||
devise (4.7.2)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0)
|
||||
|
@ -189,38 +189,39 @@ GEM
|
|||
devise_pam_authenticatable2 (9.2.0)
|
||||
devise (>= 4.0.0)
|
||||
rpam2 (~> 4.0)
|
||||
diff-lcs (1.3)
|
||||
diff-lcs (1.4.4)
|
||||
discard (1.2.0)
|
||||
activerecord (>= 4.2, < 7)
|
||||
docile (1.3.2)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (5.3.1)
|
||||
doorkeeper (5.4.0)
|
||||
railties (>= 5)
|
||||
dotenv (2.7.5)
|
||||
dotenv-rails (2.7.5)
|
||||
dotenv (= 2.7.5)
|
||||
railties (>= 3.2, < 6.1)
|
||||
e2mmap (0.1.0)
|
||||
elasticsearch (7.5.0)
|
||||
elasticsearch-api (= 7.5.0)
|
||||
elasticsearch-transport (= 7.5.0)
|
||||
elasticsearch-api (7.5.0)
|
||||
ed25519 (1.2.4)
|
||||
elasticsearch (7.8.0)
|
||||
elasticsearch-api (= 7.8.0)
|
||||
elasticsearch-transport (= 7.8.0)
|
||||
elasticsearch-api (7.8.0)
|
||||
multi_json
|
||||
elasticsearch-dsl (0.1.8)
|
||||
elasticsearch-transport (7.5.0)
|
||||
faraday (>= 0.14, < 1)
|
||||
elasticsearch-dsl (0.1.9)
|
||||
elasticsearch-transport (7.8.0)
|
||||
faraday (~> 1)
|
||||
multi_json
|
||||
encryptor (3.0.0)
|
||||
equatable (0.6.1)
|
||||
erubi (1.9.0)
|
||||
et-orbi (1.2.3)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
excon (0.71.0)
|
||||
fabrication (2.21.0)
|
||||
faker (2.11.0)
|
||||
excon (0.75.0)
|
||||
fabrication (2.21.1)
|
||||
faker (2.13.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (0.17.3)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
fast_blank (1.0.0)
|
||||
fastimage (2.1.7)
|
||||
|
@ -236,14 +237,14 @@ GEM
|
|||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (0.3.7)
|
||||
fog-openstack (0.3.10)
|
||||
fog-core (>= 1.45, <= 2.1.0)
|
||||
fog-json (>= 1.0)
|
||||
ipaddress (>= 0.8)
|
||||
formatador (0.2.5)
|
||||
fugit (1.3.3)
|
||||
fugit (1.3.6)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.1)
|
||||
raabro (~> 1.3)
|
||||
fuubar (2.5.0)
|
||||
rspec-core (~> 3.0)
|
||||
ruby-progressbar (~> 1.4)
|
||||
|
@ -271,21 +272,21 @@ GEM
|
|||
hiredis (0.6.3)
|
||||
hkdf (0.3.0)
|
||||
htmlentities (4.3.4)
|
||||
http (4.3.0)
|
||||
http (4.4.1)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
http-parser (~> 1.2.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.2.0)
|
||||
http-form_data (2.3.0)
|
||||
http-parser (1.2.1)
|
||||
ffi-compiler (>= 1.0, < 2.0)
|
||||
http_accept_language (2.1.1)
|
||||
httplog (1.4.2)
|
||||
httplog (1.4.3)
|
||||
rack (>= 1.0)
|
||||
rainbow (>= 2.0.0)
|
||||
i18n (1.8.2)
|
||||
i18n (1.8.3)
|
||||
concurrent-ruby (~> 1.0)
|
||||
i18n-tasks (0.9.31)
|
||||
activesupport (>= 4.0.2)
|
||||
|
@ -300,36 +301,35 @@ GEM
|
|||
idn-ruby (0.1.0)
|
||||
ipaddress (0.8.3)
|
||||
iso-639 (0.3.5)
|
||||
jaro_winkler (1.5.4)
|
||||
jmespath (1.4.0)
|
||||
json (2.3.0)
|
||||
json (2.3.1)
|
||||
json-canonicalization (0.2.0)
|
||||
json-ld (3.1.2)
|
||||
json-ld (3.1.4)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 0.2)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.14)
|
||||
rack (~> 2.0)
|
||||
rdf (~> 3.1)
|
||||
json-ld-preloaded (3.1.2)
|
||||
json-ld-preloaded (3.1.3)
|
||||
json-ld (~> 3.1)
|
||||
rdf (~> 3.1)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.1.0)
|
||||
kaminari (1.1.1)
|
||||
jwt (2.2.1)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.1.1)
|
||||
kaminari-activerecord (= 1.1.1)
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-actionview (1.1.1)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
kaminari-activerecord (= 1.2.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-actionview (1.2.1)
|
||||
actionview
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-activerecord (1.1.1)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-activerecord (1.2.1)
|
||||
activerecord
|
||||
kaminari-core (= 1.1.1)
|
||||
kaminari-core (1.1.1)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
kaminari-core (= 1.2.1)
|
||||
kaminari-core (1.2.1)
|
||||
launchy (2.5.0)
|
||||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.4.0)
|
||||
|
@ -342,7 +342,7 @@ GEM
|
|||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.5.0)
|
||||
loofah (2.6.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
|
@ -354,36 +354,36 @@ GEM
|
|||
mario-redis-lock (1.2.1)
|
||||
redis (>= 3.0.5)
|
||||
memory_profiler (0.9.14)
|
||||
method_source (0.9.2)
|
||||
method_source (1.0.0)
|
||||
microformats (4.2.0)
|
||||
json (~> 2.2)
|
||||
nokogiri (~> 1.10)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2019.1009)
|
||||
mimemagic (0.3.4)
|
||||
mime-types-data (3.2020.0512)
|
||||
mimemagic (0.3.5)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.0)
|
||||
minitest (5.14.1)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.14.1)
|
||||
multipart-post (2.1.1)
|
||||
necromancer (0.5.1)
|
||||
net-ldap (0.16.2)
|
||||
net-scp (2.0.0)
|
||||
net-ssh (>= 2.6.5, < 6.0.0)
|
||||
net-ssh (5.2.0)
|
||||
net-scp (3.0.0)
|
||||
net-ssh (>= 2.6.5, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.9)
|
||||
nokogiri (1.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
nokogumbo (2.0.1)
|
||||
nokogumbo (2.0.2)
|
||||
nokogiri (~> 1.8, >= 1.8.4)
|
||||
nsa (0.2.7)
|
||||
activesupport (>= 4.2, < 6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sidekiq (>= 3.5)
|
||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.10.5)
|
||||
oj (3.10.6)
|
||||
omniauth (1.9.1)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
|
@ -391,12 +391,12 @@ GEM
|
|||
addressable (~> 2.3)
|
||||
nokogiri (~> 1.5)
|
||||
omniauth (~> 1.2)
|
||||
omniauth-saml (1.10.1)
|
||||
omniauth-saml (1.10.2)
|
||||
omniauth (~> 1.3, >= 1.3.2)
|
||||
ruby-saml (~> 1.7)
|
||||
ruby-saml (~> 1.9)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.13.2)
|
||||
paperclip (6.0.0)
|
||||
paperclip (6.1.0)
|
||||
activemodel (>= 4.2.0)
|
||||
activesupport (>= 4.2.0)
|
||||
mime-types
|
||||
|
@ -405,69 +405,67 @@ GEM
|
|||
paperclip-av-transcoder (0.6.4)
|
||||
av (~> 0.9.0)
|
||||
paperclip (>= 2.5.2)
|
||||
parallel (1.19.1)
|
||||
parallel_tests (2.32.0)
|
||||
parallel (1.19.2)
|
||||
parallel_tests (3.0.0)
|
||||
parallel
|
||||
parser (2.7.0.5)
|
||||
ast (~> 2.4.0)
|
||||
parslet (1.8.2)
|
||||
pastel (0.7.3)
|
||||
parser (2.7.1.4)
|
||||
ast (~> 2.4.1)
|
||||
parslet (2.0.0)
|
||||
pastel (0.7.4)
|
||||
equatable (~> 0.6)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.2.3)
|
||||
pghero (2.4.1)
|
||||
pghero (2.6.0)
|
||||
activerecord (>= 5)
|
||||
pkg-config (1.4.1)
|
||||
premailer (1.11.1)
|
||||
addressable
|
||||
css_parser (>= 1.6.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.10.3)
|
||||
premailer-rails (1.11.1)
|
||||
actionmailer (>= 3)
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
private_address_check (0.5.0)
|
||||
pry (0.12.2)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
pry-byebug (3.8.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-byebug (3.9.0)
|
||||
byebug (~> 11.0)
|
||||
pry (~> 0.10)
|
||||
pry (~> 0.13.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.4)
|
||||
puma (4.3.3)
|
||||
public_suffix (4.0.5)
|
||||
puma (4.3.5)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.1.6)
|
||||
rack (2.2.2)
|
||||
rack-attack (6.2.2)
|
||||
raabro (1.3.1)
|
||||
rack (2.2.3)
|
||||
rack-attack (6.3.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (2.0.8.1)
|
||||
rack
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.2.4.2)
|
||||
actioncable (= 5.2.4.2)
|
||||
actionmailer (= 5.2.4.2)
|
||||
actionpack (= 5.2.4.2)
|
||||
actionview (= 5.2.4.2)
|
||||
activejob (= 5.2.4.2)
|
||||
activemodel (= 5.2.4.2)
|
||||
activerecord (= 5.2.4.2)
|
||||
activestorage (= 5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
rails (5.2.4.3)
|
||||
actioncable (= 5.2.4.3)
|
||||
actionmailer (= 5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
actionview (= 5.2.4.3)
|
||||
activejob (= 5.2.4.3)
|
||||
activemodel (= 5.2.4.3)
|
||||
activerecord (= 5.2.4.3)
|
||||
activestorage (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 5.2.4.2)
|
||||
railties (= 5.2.4.3)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.4)
|
||||
actionpack (>= 5.0.1.x)
|
||||
actionview (>= 5.0.1.x)
|
||||
activesupport (>= 5.0.1.x)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
|
@ -476,61 +474,62 @@ GEM
|
|||
rails-i18n (5.1.3)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 5.0, < 6)
|
||||
rails-settings-cached (0.6.6)
|
||||
rails-settings-cached (0.7.2)
|
||||
rails (>= 4.2.0)
|
||||
railties (5.2.4.2)
|
||||
actionpack (= 5.2.4.2)
|
||||
activesupport (= 5.2.4.2)
|
||||
railties (5.2.4.3)
|
||||
actionpack (= 5.2.4.3)
|
||||
activesupport (= 5.2.4.3)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.19.0, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rdf (3.1.1)
|
||||
rdf (3.1.4)
|
||||
hamster (~> 3.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.4.0)
|
||||
rdf (~> 3.1)
|
||||
redis (4.1.3)
|
||||
redis-actionpack (5.0.2)
|
||||
actionpack (>= 4.0, < 6)
|
||||
redis-rack (>= 1, < 3)
|
||||
redis (4.2.1)
|
||||
redis-actionpack (5.2.0)
|
||||
actionpack (>= 5, < 7)
|
||||
redis-rack (>= 2.1.0, < 3)
|
||||
redis-store (>= 1.1.0, < 2)
|
||||
redis-activesupport (5.0.4)
|
||||
activesupport (>= 3, < 6)
|
||||
redis-activesupport (5.2.0)
|
||||
activesupport (>= 3, < 7)
|
||||
redis-store (>= 1.3, < 2)
|
||||
redis-namespace (1.7.0)
|
||||
redis (>= 3.0.4)
|
||||
redis-rack (2.0.4)
|
||||
rack (>= 1.5, < 3)
|
||||
redis-rack (2.1.2)
|
||||
rack (>= 2.0.8, < 3)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-rails (5.0.2)
|
||||
redis-actionpack (>= 5.0, < 6)
|
||||
redis-activesupport (>= 5.0, < 6)
|
||||
redis-store (>= 1.2, < 2)
|
||||
redis-store (1.5.0)
|
||||
redis (>= 2.2, < 5)
|
||||
regexp_parser (1.6.0)
|
||||
redis-store (1.9.0)
|
||||
redis (>= 4, < 5)
|
||||
regexp_parser (1.7.1)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.0.0)
|
||||
responders (3.0.1)
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.4)
|
||||
rotp (2.1.2)
|
||||
rpam2 (4.0.2)
|
||||
rqrcode (1.1.2)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 0.1)
|
||||
rqrcode_core (0.1.1)
|
||||
rspec-core (3.9.1)
|
||||
rspec-support (~> 3.9.1)
|
||||
rspec-expectations (3.9.1)
|
||||
rqrcode_core (0.1.2)
|
||||
rspec-core (3.9.2)
|
||||
rspec-support (~> 3.9.3)
|
||||
rspec-expectations (3.9.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-mocks (3.9.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.9.0)
|
||||
rspec-rails (4.0.0)
|
||||
rspec-rails (4.0.1)
|
||||
actionpack (>= 4.2)
|
||||
activesupport (>= 4.2)
|
||||
railties (>= 4.2)
|
||||
|
@ -538,35 +537,42 @@ GEM
|
|||
rspec-expectations (~> 3.9)
|
||||
rspec-mocks (~> 3.9)
|
||||
rspec-support (~> 3.9)
|
||||
rspec-sidekiq (3.0.3)
|
||||
rspec-sidekiq (3.1.0)
|
||||
rspec-core (~> 3.0, >= 3.0.0)
|
||||
sidekiq (>= 2.4.0)
|
||||
rspec-support (3.9.2)
|
||||
rubocop (0.79.0)
|
||||
jaro_winkler (~> 1.5.1)
|
||||
rspec-support (3.9.3)
|
||||
rspec_junit_formatter (0.4.1)
|
||||
rspec-core (>= 2, < 4, != 2.12.0)
|
||||
rubocop (0.87.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.0.1)
|
||||
parser (>= 2.7.1.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
rexml
|
||||
rubocop-ast (>= 0.1.0, < 1.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 1.7)
|
||||
rubocop-rails (2.4.2)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.1.0)
|
||||
parser (>= 2.7.0.1)
|
||||
rubocop-rails (2.6.0)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
rubocop (>= 0.82.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-saml (1.9.0)
|
||||
ruby-saml (1.11.0)
|
||||
nokogiri (>= 1.5.10)
|
||||
rufus-scheduler (3.6.0)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safe_yaml (1.0.5)
|
||||
sanitize (5.1.0)
|
||||
sanitize (5.2.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.8.0)
|
||||
nokogumbo (~> 2.0)
|
||||
sidekiq (6.0.4)
|
||||
semantic_range (2.3.0)
|
||||
sidekiq (6.1.0)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-protection (>= 2.0.0)
|
||||
redis (>= 4.1.0)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-bulk (0.2.0)
|
||||
sidekiq
|
||||
sidekiq-scheduler (3.0.1)
|
||||
|
@ -576,7 +582,7 @@ GEM
|
|||
sidekiq (>= 3)
|
||||
thwait
|
||||
tilt (>= 1.4.0)
|
||||
sidekiq-unique-jobs (6.0.21)
|
||||
sidekiq-unique-jobs (6.0.22)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||
sidekiq (>= 4.0, < 7.0)
|
||||
thor (~> 0)
|
||||
|
@ -604,7 +610,7 @@ GEM
|
|||
stoplight (2.2.0)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
strong_migrations (0.6.2)
|
||||
strong_migrations (0.6.8)
|
||||
activerecord (>= 5)
|
||||
temple (0.8.2)
|
||||
terminal-table (1.8.0)
|
||||
|
@ -616,8 +622,6 @@ GEM
|
|||
thwait (0.1.0)
|
||||
tilt (2.0.10)
|
||||
tty-color (0.5.1)
|
||||
tty-command (0.9.0)
|
||||
pastel (~> 0.7.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-prompt (0.21.0)
|
||||
necromancer (~> 0.5.0)
|
||||
|
@ -627,17 +631,17 @@ GEM
|
|||
tty-cursor (~> 0.7)
|
||||
tty-screen (~> 0.7)
|
||||
wisper (~> 2.0.0)
|
||||
tty-screen (0.7.1)
|
||||
tty-screen (0.8.0)
|
||||
twitter-text (1.14.7)
|
||||
unf (~> 0.1.0)
|
||||
tzinfo (1.2.7)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2019.3)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.6)
|
||||
unicode-display_width (1.6.1)
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
warden (1.2.8)
|
||||
rack (>= 2.0.6)
|
||||
|
@ -645,16 +649,17 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (4.2.2)
|
||||
activesupport (>= 4.2)
|
||||
webpacker (5.1.1)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 4.2)
|
||||
webpush (0.3.8)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.1)
|
||||
websocket-driver (0.7.2)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.4)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.1)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
|
@ -666,36 +671,38 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10)
|
||||
active_record_query_trace (~> 1.7)
|
||||
addressable (~> 2.7)
|
||||
annotate (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.61)
|
||||
better_errors (~> 2.6)
|
||||
annotate (~> 3.1)
|
||||
aws-sdk-s3 (~> 1.73)
|
||||
better_errors (~> 2.7)
|
||||
binding_of_caller (~> 0.7)
|
||||
blurhash (~> 0.1)
|
||||
bootsnap (~> 1.4)
|
||||
brakeman (~> 4.8)
|
||||
browser
|
||||
bullet (~> 6.1)
|
||||
bundler-audit (~> 0.6)
|
||||
capistrano (~> 3.12)
|
||||
capistrano-rails (~> 1.4)
|
||||
bundler-audit (~> 0.7)
|
||||
capistrano (~> 3.14)
|
||||
capistrano-rails (~> 1.5)
|
||||
capistrano-rbenv (~> 2.1)
|
||||
capistrano-yarn (~> 2.0)
|
||||
capybara (~> 3.31)
|
||||
capybara (~> 3.33)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 5.1)
|
||||
cld3 (~> 3.3.0)
|
||||
climate_control (~> 0.2)
|
||||
color_diff (~> 0.1)
|
||||
concurrent-ruby
|
||||
connection_pool
|
||||
devise (~> 4.7)
|
||||
devise-two-factor (~> 3.1)
|
||||
devise_pam_authenticatable2 (~> 9.2)
|
||||
discard (~> 1.2)
|
||||
doorkeeper (~> 5.3)
|
||||
doorkeeper (~> 5.4)
|
||||
dotenv-rails (~> 2.7)
|
||||
e2mmap (~> 0.1.0)
|
||||
ed25519 (~> 1.2)
|
||||
fabrication (~> 2.21)
|
||||
faker (~> 2.11)
|
||||
faker (~> 2.13)
|
||||
fast_blank (~> 1.0)
|
||||
fastimage
|
||||
fog-core (<= 2.1.0)
|
||||
|
@ -706,16 +713,16 @@ DEPENDENCIES
|
|||
health_check!
|
||||
hiredis (~> 0.6)
|
||||
htmlentities (~> 4.3)
|
||||
http (~> 4.3)
|
||||
http (~> 4.4)
|
||||
http_accept_language (~> 2.1)
|
||||
http_parser.rb (~> 0.6)!
|
||||
httplog (~> 1.4.2)
|
||||
httplog (~> 1.4.3)
|
||||
i18n-tasks (~> 0.9)
|
||||
idn-ruby
|
||||
iso-639
|
||||
json-ld
|
||||
json-ld-preloaded (~> 3.1)
|
||||
kaminari (~> 1.1)
|
||||
kaminari (~> 1.2)
|
||||
letter_opener (~> 1.7)
|
||||
letter_opener_web (~> 1.4)
|
||||
link_header (~> 0.0)
|
||||
|
@ -737,36 +744,37 @@ DEPENDENCIES
|
|||
paperclip (~> 6.0)
|
||||
paperclip-av-transcoder (~> 0.6)
|
||||
parallel (~> 1.19)
|
||||
parallel_tests (~> 2.32)
|
||||
parallel_tests (~> 3.0)
|
||||
parslet
|
||||
pg (~> 1.2)
|
||||
pghero (~> 2.4)
|
||||
pghero (~> 2.5)
|
||||
pkg-config (~> 1.4)
|
||||
posix-spawn!
|
||||
premailer-rails
|
||||
private_address_check (~> 0.5)
|
||||
pry-byebug (~> 3.8)
|
||||
pry-byebug (~> 3.9)
|
||||
pry-rails (~> 0.3)
|
||||
puma (~> 4.3)
|
||||
pundit (~> 2.1)
|
||||
rack (~> 2.2.2)
|
||||
rack-attack (~> 6.2)
|
||||
rack (~> 2.2.3)
|
||||
rack-attack (~> 6.3)
|
||||
rack-cors (~> 1.1)
|
||||
rails (~> 5.2.4.2)
|
||||
rails (~> 5.2.4.3)
|
||||
rails-controller-testing (~> 1.0)
|
||||
rails-i18n (~> 5.1)
|
||||
rails-settings-cached (~> 0.6)
|
||||
rdf-normalize (~> 0.4)
|
||||
redis (~> 4.1)
|
||||
redis (~> 4.2)
|
||||
redis-namespace (~> 1.7)
|
||||
redis-rails (~> 5.0)
|
||||
rqrcode (~> 1.1)
|
||||
rspec-rails (~> 4.0)
|
||||
rspec-sidekiq (~> 3.0)
|
||||
rubocop (~> 0.79)
|
||||
rubocop-rails (~> 2.4)
|
||||
rspec-sidekiq (~> 3.1)
|
||||
rspec_junit_formatter (~> 0.4)
|
||||
rubocop (~> 0.86)
|
||||
rubocop-rails (~> 2.6)
|
||||
ruby-progressbar (~> 1.10)
|
||||
sanitize (~> 5.1)
|
||||
sanitize (~> 5.2)
|
||||
sidekiq (~> 6.0)
|
||||
sidekiq-bulk (~> 0.2.0)
|
||||
sidekiq-scheduler (~> 3.0)
|
||||
|
@ -782,16 +790,15 @@ DEPENDENCIES
|
|||
strong_migrations (~> 0.6)
|
||||
thor (~> 0.20)
|
||||
thwait (~> 0.1.0)
|
||||
tty-command (~> 0.9)
|
||||
tty-prompt (~> 0.21)
|
||||
twitter-text (~> 1.14)
|
||||
tzinfo-data (~> 1.2019)
|
||||
tzinfo-data (~> 1.2020)
|
||||
webmock (~> 3.8)
|
||||
webpacker (~> 4.2)
|
||||
webpacker (~> 5.1)
|
||||
webpush
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.4p104
|
||||
ruby 2.6.6p146
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
|
|
|
@ -80,7 +80,7 @@ A **Vagrant** configuration is included for development purposes.
|
|||
|
||||
Mastodon is **free, open-source software** licensed under **AGPLv3**.
|
||||
|
||||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Weblate. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
|
||||
You can open issues for bugs you've found or features you think are missing. You can also submit pull requests to this repository, or submit translations using Crowdin. To get started, take a look at [CONTRIBUTING.md](CONTRIBUTING.md). If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
|
||||
|
||||
**IRC channel**: #mastodon on irc.freenode.net
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 3.1.x | :white_check_mark: |
|
||||
| < 3.1 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
hello@joinmastodon.org
|
3
app.json
3
app.json
|
@ -88,9 +88,6 @@
|
|||
{
|
||||
"url": "https://github.com/heroku/heroku-buildpack-apt"
|
||||
},
|
||||
{
|
||||
"url": "heroku/nodejs"
|
||||
},
|
||||
{
|
||||
"url": "heroku/ruby"
|
||||
}
|
||||
|
|
|
@ -31,9 +31,9 @@ class StatusesIndex < Chewy::Index
|
|||
},
|
||||
}
|
||||
|
||||
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments), delete_if: ->(status) { status.searchable_by.empty? } do
|
||||
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
|
||||
crutch :mentions do |collection|
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
|
||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
|
||||
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountsController < ApplicationController
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE_MAX = 200
|
||||
|
||||
include AccountControllerConcern
|
||||
include SignatureAuthentication
|
||||
|
@ -10,7 +11,7 @@ class AccountsController < ApplicationController
|
|||
before_action :set_body_classes
|
||||
|
||||
skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
|
@ -27,7 +28,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
||||
@statuses = filtered_status_page(params)
|
||||
@statuses = filtered_status_page
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
@rss_url = rss_url
|
||||
|
||||
|
@ -40,7 +41,8 @@ class AccountsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 1.minute, public: true
|
||||
|
||||
@statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE)
|
||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
@statuses = filtered_statuses.without_reblogs.limit(limit)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag])
|
||||
end
|
||||
|
@ -129,23 +131,23 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
|
||||
def media_requested?
|
||||
request.path.ends_with?('/media') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/media') && !tag_requested?
|
||||
end
|
||||
|
||||
def replies_requested?
|
||||
request.path.ends_with?('/with_replies') && !tag_requested?
|
||||
request.path.split('.').first.ends_with?('/with_replies') && !tag_requested?
|
||||
end
|
||||
|
||||
def tag_requested?
|
||||
request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||
end
|
||||
|
||||
def filtered_status_page(params)
|
||||
if params[:min_id].present?
|
||||
filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse
|
||||
else
|
||||
filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a
|
||||
end
|
||||
def filtered_status_page
|
||||
filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id))
|
||||
end
|
||||
|
||||
def params_slice(*keys)
|
||||
params.slice(*keys).permit(*keys)
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::ClaimsController < ActivityPub::BaseController
|
||||
include SignatureVerification
|
||||
include AccountOwnedConcern
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
|
||||
before_action :require_signature!
|
||||
before_action :set_claim_result
|
||||
|
||||
def create
|
||||
render json: @claim_result, serializer: ActivityPub::OneTimeKeySerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_claim_result
|
||||
@claim_result = ::Keys::ClaimService.new.call(@account.id, params[:id])
|
||||
end
|
||||
end
|
|
@ -5,8 +5,9 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
include AccountOwnedConcern
|
||||
|
||||
before_action :require_signature!, if: :authorized_fetch_mode?
|
||||
before_action :set_items
|
||||
before_action :set_size
|
||||
before_action :set_statuses
|
||||
before_action :set_type
|
||||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
|
@ -16,37 +17,53 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||
|
||||
private
|
||||
|
||||
def set_statuses
|
||||
@statuses = scope_for_collection
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
def set_items
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@items = begin
|
||||
# Because in public fetch mode we cache the response, there would be no
|
||||
# benefit from performing the check below, since a blocked account or domain
|
||||
# would likely be served the cache from the reverse proxy anyway
|
||||
|
||||
if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain)))
|
||||
[]
|
||||
else
|
||||
cache_collection(@account.pinned_statuses, Status)
|
||||
end
|
||||
end
|
||||
when 'devices'
|
||||
@items = @account.devices
|
||||
else
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def set_size
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
@account.pinned_statuses.count
|
||||
when 'featured', 'devices'
|
||||
@size = @items.size
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def scope_for_collection
|
||||
def set_type
|
||||
case params[:id]
|
||||
when 'featured'
|
||||
return Status.none if @account.blocking?(signed_request_account)
|
||||
|
||||
@account.pinned_statuses
|
||||
@type = :ordered
|
||||
when 'devices'
|
||||
@type = :unordered
|
||||
else
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
end
|
||||
|
||||
def collection_presenter
|
||||
ActivityPub::CollectionPresenter.new(
|
||||
id: account_collection_url(@account, params[:id]),
|
||||
type: :ordered,
|
||||
type: @type,
|
||||
size: @size,
|
||||
items: @statuses
|
||||
items: @items
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,7 +49,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
ResolveAccountWorker.perform_async(signed_request_account.acct)
|
||||
end
|
||||
|
||||
DeliveryFailureTracker.track_inverse_success!(signed_request_account)
|
||||
DeliveryFailureTracker.reset!(signed_request_account.inbox_url)
|
||||
end
|
||||
|
||||
def process_payload
|
||||
|
|
|
@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
before_action :set_cache_headers
|
||||
|
||||
def show
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
|
||||
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
end
|
||||
|
||||
|
@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||
return unless page_requested?
|
||||
|
||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
||||
@statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id])
|
||||
@statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id))
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page] == 'true'
|
||||
truthy_param?(:page)
|
||||
end
|
||||
|
||||
def page_params
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::RepliesController < ActivityPub::BaseController
|
||||
include SignatureAuthentication
|
||||
include SignatureVerification
|
||||
include Authorization
|
||||
include AccountOwnedConcern
|
||||
|
||||
|
@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
|
||||
private
|
||||
|
||||
def pundit_user
|
||||
signed_request_account
|
||||
end
|
||||
|
||||
def set_status
|
||||
@status = @account.statuses.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_replies
|
||||
@replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses
|
||||
@replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses
|
||||
@replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted])
|
||||
@replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id])
|
||||
end
|
||||
|
@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
type: :unordered,
|
||||
part_of: account_status_replies_url(@account, @status),
|
||||
next: next_page,
|
||||
items: @replies.map { |status| status.local ? status : status.uri }
|
||||
items: @replies.map { |status| status.local? ? status : status.uri }
|
||||
)
|
||||
|
||||
return page if page_requested?
|
||||
|
@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController
|
|||
end
|
||||
|
||||
def page_requested?
|
||||
params[:page] == 'true'
|
||||
truthy_param?(:page)
|
||||
end
|
||||
|
||||
def only_other_accounts?
|
||||
truthy_param?(:only_other_accounts)
|
||||
end
|
||||
|
||||
def next_page
|
||||
only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT)
|
||||
|
||||
account_status_replies_url(
|
||||
@account,
|
||||
@status,
|
||||
page: true,
|
||||
min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id,
|
||||
min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id,
|
||||
only_other_accounts: only_other_accounts
|
||||
)
|
||||
end
|
||||
|
|
|
@ -33,6 +33,8 @@ module Admin
|
|||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||
rescue Mastodon::NotPermittedError
|
||||
flash[:alert] = I18n.t('admin.custom_emojis.not_permitted')
|
||||
ensure
|
||||
redirect_to admin_custom_emojis_path(filter_params)
|
||||
end
|
||||
|
|
|
@ -19,7 +19,7 @@ module Admin
|
|||
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
|
||||
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
|
||||
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
|
||||
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
|
||||
@available = DeliveryFailureTracker.available?(params[:id])
|
||||
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
|
||||
@private_comment = @domain_block&.private_comment
|
||||
@public_comment = @domain_block&.public_comment
|
||||
|
|
|
@ -7,7 +7,7 @@ class Api::BaseController < ApplicationController
|
|||
include RateLimitHeaders
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
|
||||
before_action :set_cache_headers
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||
return [] if hide_results?
|
||||
|
||||
scope = default_accounts
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
|
||||
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil? || current_account.id == @account.id
|
||||
scope.merge(paginated_follows).to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::NotesController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
def create
|
||||
if params[:comment].blank?
|
||||
AccountNote.find_by(account: current_account, target_account: @account)&.destroy
|
||||
else
|
||||
@note = AccountNote.find_or_initialize_by(account: current_account, target_account: @account)
|
||||
@note.comment = params[:comment]
|
||||
@note.save! if @note.changed?
|
||||
end
|
||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::DeliveriesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
def create
|
||||
devices.each do |device_params|
|
||||
DeliverToDeviceService.new.call(current_account, @current_device, device_params)
|
||||
end
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:device)
|
||||
params.permit(device: [:account_id, :device_id, :type, :body, :hmac])
|
||||
end
|
||||
|
||||
def devices
|
||||
Array(resource_params[:device])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
||||
LIMIT = 80
|
||||
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
before_action :set_encrypted_messages, only: :index
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
def index
|
||||
render json: @encrypted_messages, each_serializer: REST::EncryptedMessageSerializer
|
||||
end
|
||||
|
||||
def clear
|
||||
@current_device.encrypted_messages.up_to(params[:up_to_id]).delete_all
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
|
||||
def set_encrypted_messages
|
||||
@encrypted_messages = @current_device.encrypted_messages.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_crypto_encrypted_messages_url pagination_params(max_id: pagination_max_id) if records_continue?
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@encrypted_messages.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@encrypted_messages.first.id
|
||||
end
|
||||
|
||||
def records_continue?
|
||||
@encrypted_messages.size == limit_param(LIMIT)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::ClaimsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_claim_results
|
||||
|
||||
def create
|
||||
render json: @claim_results, each_serializer: REST::Keys::ClaimResultSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_claim_results
|
||||
@claim_results = devices.map { |device_params| ::Keys::ClaimService.new.call(current_account, device_params[:account_id], device_params[:device_id]) }.compact
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.permit(device: [:account_id, :device_id])
|
||||
end
|
||||
|
||||
def devices
|
||||
Array(resource_params[:device])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::CountsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_current_device
|
||||
|
||||
def show
|
||||
render json: { one_time_keys: @current_device.one_time_keys.count }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_current_device
|
||||
@current_device = Device.find_by!(access_token: doorkeeper_token)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::QueriesController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
before_action :set_accounts
|
||||
before_action :set_query_results
|
||||
|
||||
def create
|
||||
render json: @query_results, each_serializer: REST::Keys::QueryResultSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.where(id: account_ids).includes(:devices)
|
||||
end
|
||||
|
||||
def set_query_results
|
||||
@query_results = @accounts.map { |account| ::Keys::QueryService.new.call(account) }.compact
|
||||
end
|
||||
|
||||
def account_ids
|
||||
Array(params[:id]).map(&:to_i)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Crypto::Keys::UploadsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :crypto }
|
||||
before_action :require_user!
|
||||
|
||||
def create
|
||||
device = Device.find_or_initialize_by(access_token: doorkeeper_token)
|
||||
|
||||
device.transaction do
|
||||
device.account = current_account
|
||||
device.update!(resource_params[:device])
|
||||
|
||||
if resource_params[:one_time_keys].present? && resource_params[:one_time_keys].is_a?(Enumerable)
|
||||
resource_params[:one_time_keys].each do |one_time_key_params|
|
||||
device.one_time_keys.create!(one_time_key_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
render json: device, serializer: REST::Keys::DeviceSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource_params
|
||||
params.permit(device: [:device_id, :name, :fingerprint_key, :identity_key], one_time_keys: [:key_id, :key, :signature])
|
||||
end
|
||||
end
|
|
@ -39,7 +39,7 @@ class Api::V1::MediaController < Api::BaseController
|
|||
end
|
||||
|
||||
def media_attachment_params
|
||||
params.permit(:file, :description, :focus)
|
||||
params.permit(:file, :thumbnail, :description, :focus)
|
||||
end
|
||||
|
||||
def file_type_error
|
||||
|
|
|
@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController
|
|||
@poll = Poll.attached.find(params[:poll_id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def vote_params
|
||||
|
|
|
@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController
|
|||
@poll = Poll.attached.find(params[:id])
|
||||
authorize @poll.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def refresh_poll
|
||||
|
|
|
@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
before_action -> { doorkeeper_authorize! :push }
|
||||
before_action :require_user!
|
||||
before_action :set_web_push_subscription
|
||||
before_action :check_web_push_subscription, only: [:show, :update]
|
||||
|
||||
def create
|
||||
@web_subscription&.destroy!
|
||||
|
@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
end
|
||||
|
||||
def show
|
||||
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||
|
||||
@web_subscription.update!(data: data_params)
|
||||
|
||||
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
|
@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||
end
|
||||
|
||||
def check_web_push_subscription
|
||||
not_found if @web_subscription.nil?
|
||||
end
|
||||
|
||||
def subscription_params
|
||||
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||
end
|
||||
|
||||
def data_params
|
||||
return {} if params[:data].blank?
|
||||
|
||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController
|
|||
@status = Status.find(params[:status_id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
# Reraise in order to get a 404 instead of a 403 error code
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
|
|
@ -5,7 +5,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||
before_action :require_user!
|
||||
before_action :set_reblog
|
||||
before_action :set_reblog, only: [:create]
|
||||
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
|
||||
|
@ -16,15 +16,21 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
|
|||
end
|
||||
|
||||
def destroy
|
||||
@status = current_account.statuses.find_by(reblog_of_id: @reblog.id)
|
||||
@status = current_account.statuses.find_by(reblog_of_id: params[:status_id])
|
||||
|
||||
if @status
|
||||
authorize @status, :unreblog?
|
||||
@status.discard
|
||||
RemovalWorker.perform_async(@status.id)
|
||||
@reblog = @status.reblog
|
||||
else
|
||||
@reblog = Status.find(params[:status_id])
|
||||
authorize @reblog, :show?
|
||||
end
|
||||
|
||||
render json: @reblog, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false })
|
||||
rescue Mastodon::NotPermittedError
|
||||
not_found
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -67,7 +67,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_thread
|
||||
|
|
|
@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
def public_timeline_statuses
|
||||
Status.as_public_timeline(current_account, truthy_param?(:local))
|
||||
Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
|
|||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
|
||||
params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
|
|
|
@ -8,7 +8,10 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
|
||||
def update
|
||||
super do |resource|
|
||||
resource.session_activations.destroy_all if resource.errors.empty?
|
||||
if resource.errors.empty?
|
||||
resource.session_activations.destroy_all
|
||||
resource.forget_me!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||
include Devise::Controllers::Rememberable
|
||||
|
||||
layout :determine_layout
|
||||
|
||||
before_action :set_invite, only: [:new, :create]
|
||||
|
@ -24,7 +26,11 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
def update
|
||||
super do |resource|
|
||||
resource.clear_other_sessions(current_session.session_id) if resource.saved_change_to_encrypted_password?
|
||||
if resource.saved_change_to_encrypted_password?
|
||||
resource.clear_other_sessions(current_session.session_id)
|
||||
resource.forget_me!
|
||||
remember_me(resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,8 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
skip_before_action :require_no_authentication, only: [:create]
|
||||
skip_before_action :require_functional!
|
||||
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
include TwoFactorAuthenticationConcern
|
||||
include SignInTokenAuthenticationConcern
|
||||
|
||||
before_action :set_instance_presenter, only: [:new]
|
||||
before_action :set_body_classes
|
||||
|
@ -39,17 +40,18 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
protected
|
||||
|
||||
def find_user
|
||||
if session[:otp_user_id]
|
||||
User.find(session[:otp_user_id])
|
||||
if session[:attempt_user_id]
|
||||
User.find(session[:attempt_user_id])
|
||||
else
|
||||
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
|
||||
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
|
||||
user ||= User.find_for_authentication(email: user_params[:email])
|
||||
user
|
||||
end
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email, :password, :otp_attempt)
|
||||
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt)
|
||||
end
|
||||
|
||||
def after_sign_in_path_for(resource)
|
||||
|
@ -70,45 +72,11 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
super
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
find_user&.otp_required_for_login?
|
||||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
rescue OpenSSL::Cipher::CipherError
|
||||
false
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||
authenticate_with_two_factor_via_otp(user)
|
||||
elsif user.present? && (user.encrypted_password.blank? || user.valid_password?(user_params[:password]))
|
||||
# If encrypted_password is blank, we got the user from LDAP or PAM,
|
||||
# so credentials are already valid
|
||||
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor_via_otp(user)
|
||||
if valid_otp_attempt?(user)
|
||||
session.delete(:otp_user_id)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def prompt_for_two_factor(user)
|
||||
session[:otp_user_id] = user.id
|
||||
@body_classes = 'lighter'
|
||||
render :two_factor
|
||||
def require_no_authentication
|
||||
super
|
||||
# Delete flash message that isn't entirely useful and may be confusing in
|
||||
# most cases because /web doesn't display/clear flash messages.
|
||||
flash.delete(:alert) if flash[:alert] == I18n.t('devise.failure.already_authenticated')
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -7,8 +7,6 @@ module Localized
|
|||
around_action :set_locale
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_locale
|
||||
locale = current_user.locale if respond_to?(:user_signed_in?) && user_signed_in?
|
||||
locale ||= session[:locale] ||= default_locale
|
||||
|
@ -19,6 +17,8 @@ module Localized
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_locale
|
||||
if ENV['DEFAULT_LOCALE'].present?
|
||||
I18n.default_locale
|
||||
|
@ -28,18 +28,6 @@ module Localized
|
|||
end
|
||||
|
||||
def request_locale
|
||||
preferred_locale || compatible_locale
|
||||
end
|
||||
|
||||
def preferred_locale
|
||||
http_accept_language.preferred_language_from(available_locales)
|
||||
end
|
||||
|
||||
def compatible_locale
|
||||
http_accept_language.compatible_language_from(available_locales)
|
||||
end
|
||||
|
||||
def available_locales
|
||||
I18n.available_locales.reverse
|
||||
http_accept_language.language_region_compatible_from(I18n.available_locales)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SignInTokenAuthenticationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :authenticate_with_sign_in_token, if: :sign_in_token_required?, only: [:create]
|
||||
end
|
||||
|
||||
def sign_in_token_required?
|
||||
find_user&.suspicious_sign_in?(request.remote_ip)
|
||||
end
|
||||
|
||||
def valid_sign_in_token_attempt?(user)
|
||||
Devise.secure_compare(user.sign_in_token, user_params[:sign_in_token_attempt])
|
||||
end
|
||||
|
||||
def authenticate_with_sign_in_token
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:sign_in_token_attempt].present? && session[:attempt_user_id]
|
||||
authenticate_with_sign_in_token_attempt(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_sign_in_token(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_sign_in_token_attempt(user)
|
||||
if valid_sign_in_token_attempt?(user)
|
||||
session.delete(:attempt_user_id)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')
|
||||
prompt_for_sign_in_token(user)
|
||||
end
|
||||
end
|
||||
|
||||
def prompt_for_sign_in_token(user)
|
||||
if user.sign_in_token_expired?
|
||||
user.generate_sign_in_token && user.save
|
||||
UserMailer.sign_in_token(user, request.remote_ip, request.user_agent, Time.now.utc.to_s).deliver_later!
|
||||
end
|
||||
|
||||
set_locale do
|
||||
session[:attempt_user_id] = user.id
|
||||
@body_classes = 'lighter'
|
||||
render :sign_in_token
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TwoFactorAuthenticationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create]
|
||||
end
|
||||
|
||||
def two_factor_enabled?
|
||||
find_user&.otp_required_for_login?
|
||||
end
|
||||
|
||||
def valid_otp_attempt?(user)
|
||||
user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
|
||||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
|
||||
rescue OpenSSL::Cipher::CipherError
|
||||
false
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor
|
||||
user = self.resource = find_user
|
||||
|
||||
if user_params[:otp_attempt].present? && session[:attempt_user_id]
|
||||
authenticate_with_two_factor_attempt(user)
|
||||
elsif user.present? && user.external_or_valid_password?(user_params[:password])
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def authenticate_with_two_factor_attempt(user)
|
||||
if valid_otp_attempt?(user)
|
||||
session.delete(:attempt_user_id)
|
||||
remember_me(user)
|
||||
sign_in(user)
|
||||
else
|
||||
flash.now[:alert] = I18n.t('users.invalid_otp_token')
|
||||
prompt_for_two_factor(user)
|
||||
end
|
||||
end
|
||||
|
||||
def prompt_for_two_factor(user)
|
||||
set_locale do
|
||||
session[:attempt_user_id] = user.id
|
||||
@body_classes = 'lighter'
|
||||
render :two_factor
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ class DirectoriesController < ApplicationController
|
|||
before_action :set_tag, only: :show
|
||||
before_action :set_accounts
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def index
|
||||
render :index
|
||||
|
|
|
@ -8,7 +8,7 @@ class FollowerAccountsController < ApplicationController
|
|||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
|
|
@ -8,7 +8,7 @@ class FollowingAccountsController < ApplicationController
|
|||
before_action :set_cache_headers
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def index
|
||||
respond_to do |format|
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
before_action :redirect_unauthenticated_to_permalinks!
|
||||
before_action :authenticate_user!
|
||||
before_action :set_referrer_policy_header
|
||||
|
||||
|
@ -10,7 +11,7 @@ class HomeController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def authenticate_user!
|
||||
def redirect_unauthenticated_to_permalinks!
|
||||
return if user_signed_in?
|
||||
|
||||
matches = request.path.match(/\A\/web\/(statuses|accounts)\/([\d]+)\z/)
|
||||
|
@ -35,6 +36,7 @@ class HomeController < ApplicationController
|
|||
end
|
||||
|
||||
matches = request.path.match(%r{\A/web/timelines/tag/(?<tag>.+)\z})
|
||||
|
||||
redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
|
||||
end
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ class MediaController < ApplicationController
|
|||
include Authorization
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_media_attachment
|
||||
|
@ -33,7 +33,7 @@ class MediaController < ApplicationController
|
|||
def verify_permitted_status!
|
||||
authorize @media_attachment.status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def check_playable
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class MediaProxyController < ApplicationController
|
||||
include RoutingHelper
|
||||
include Authorization
|
||||
|
||||
skip_before_action :store_current_location
|
||||
skip_before_action :require_functional!
|
||||
|
@ -10,12 +11,14 @@ class MediaProxyController < ApplicationController
|
|||
|
||||
rescue_from ActiveRecord::RecordInvalid, with: :not_found
|
||||
rescue_from Mastodon::UnexpectedResponseError, with: :not_found
|
||||
rescue_from Mastodon::NotPermittedError, with: :not_found
|
||||
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
|
||||
def show
|
||||
RedisLock.acquire(lock_options) do |lock|
|
||||
if lock.acquired?
|
||||
@media_attachment = MediaAttachment.remote.find(params[:id])
|
||||
@media_attachment = MediaAttachment.remote.attached.find(params[:id])
|
||||
authorize @media_attachment.status, :show?
|
||||
redownload! if @media_attachment.needs_redownload? && !reject_media?
|
||||
else
|
||||
raise Mastodon::RaceConditionError
|
||||
|
@ -28,8 +31,8 @@ class MediaProxyController < ApplicationController
|
|||
private
|
||||
|
||||
def redownload!
|
||||
@media_attachment.file_remote_url = @media_attachment.remote_url
|
||||
@media_attachment.created_at = Time.now.utc
|
||||
@media_attachment.download_file!
|
||||
@media_attachment.created_at = Time.now.utc
|
||||
@media_attachment.save!
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ class RemoteInteractionController < ApplicationController
|
|||
before_action :set_status
|
||||
before_action :set_body_classes
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def new
|
||||
@remote_follow = RemoteFollow.new(session_params)
|
||||
|
@ -41,7 +41,7 @@ class RemoteInteractionController < ApplicationController
|
|||
@status = Status.find(params[:id])
|
||||
authorize @status, :show?
|
||||
rescue Mastodon::NotPermittedError
|
||||
raise ActiveRecord::RecordNotFound
|
||||
not_found
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -21,8 +21,7 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
if current_account.username.casecmp(params[:username]).zero?
|
||||
render layout: 'auth'
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,11 +33,16 @@ class Settings::IdentityProofsController < Settings::BaseController
|
|||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
||||
redirect_to @proof.on_success_path(params[:user_agent])
|
||||
else
|
||||
flash[:alert] = I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
redirect_to settings_identity_proofs_path
|
||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.failed', provider: @proof.provider.capitalize)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@proof = current_account.identity_proofs.find(params[:id])
|
||||
@proof.destroy!
|
||||
redirect_to settings_identity_proofs_path, success: I18n.t('identity_proofs.removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_required_params
|
||||
|
|
|
@ -18,7 +18,7 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||
if @redirect.valid_with_challenge?(current_user)
|
||||
current_account.update!(moved_to_account: @redirect.target_account)
|
||||
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.moved_msg', acct: current_account.moved_to_account.acct)
|
||||
redirect_to settings_migration_path, notice: I18n.t('migrations.redirected_msg', acct: current_account.moved_to_account.acct)
|
||||
else
|
||||
render :new
|
||||
end
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Settings
|
||||
class PicturesController < BaseController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_account
|
||||
before_action :set_picture
|
||||
|
||||
def destroy
|
||||
if valid_picture?
|
||||
msg = I18n.t('generic.changes_saved_msg') if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
|
||||
redirect_to settings_profile_path, notice: msg, status: 303
|
||||
else
|
||||
bad_request
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = current_account
|
||||
end
|
||||
|
||||
def set_picture
|
||||
@picture = params[:id]
|
||||
end
|
||||
|
||||
def valid_picture?
|
||||
%w(avatar header).include?(@picture)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,7 +19,7 @@ class StatusesController < ApplicationController
|
|||
before_action :set_autoplay, only: :embed
|
||||
|
||||
skip_around_action :set_locale, if: -> { request.format == :json }
|
||||
skip_before_action :require_functional!, only: [:show, :embed]
|
||||
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
|
||||
|
||||
content_security_policy only: :embed do |p|
|
||||
p.frame_ancestors(false)
|
||||
|
@ -42,11 +42,11 @@ class StatusesController < ApplicationController
|
|||
|
||||
def activity
|
||||
expires_in 3.minutes, public: @status.distributable? && public_fetch_mode?
|
||||
render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
|
||||
render_with_cache json: ActivityPub::ActivityPresenter.from_status(@status), content_type: 'application/activity+json', serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter
|
||||
end
|
||||
|
||||
def embed
|
||||
return not_found if @status.hidden?
|
||||
return not_found if @status.hidden? || @status.reblog?
|
||||
|
||||
expires_in 180, public: true
|
||||
response.headers['X-Frame-Options'] = 'ALLOWALL'
|
||||
|
|
|
@ -3,17 +3,19 @@
|
|||
class TagsController < ApplicationController
|
||||
include SignatureVerification
|
||||
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE = 20
|
||||
PAGE_SIZE_MAX = 200
|
||||
|
||||
layout 'public'
|
||||
|
||||
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||
before_action :authenticate_user!, if: :whitelist_mode?
|
||||
before_action :set_tag
|
||||
before_action :set_local
|
||||
before_action :set_body_classes
|
||||
before_action :set_instance_presenter
|
||||
|
||||
skip_before_action :require_functional!
|
||||
skip_before_action :require_functional!, unless: :whitelist_mode?
|
||||
|
||||
def show
|
||||
respond_to do |format|
|
||||
|
@ -24,7 +26,8 @@ class TagsController < ApplicationController
|
|||
format.rss do
|
||||
expires_in 0, public: true
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params).limit(PAGE_SIZE)
|
||||
limit = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, nil, @local).limit(limit)
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render xml: RSS::TagSerializer.render(@tag, @statuses)
|
||||
|
@ -33,7 +36,7 @@ class TagsController < ApplicationController
|
|||
format.json do
|
||||
expires_in 3.minutes, public: public_fetch_mode?
|
||||
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, params[:local]).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = HashtagQueryService.new.call(@tag, filter_params, current_account, @local).paginate_by_max_id(PAGE_SIZE, params[:max_id])
|
||||
@statuses = cache_collection(@statuses, Status)
|
||||
|
||||
render json: collection_presenter, serializer: ActivityPub::CollectionSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
|
||||
|
@ -47,6 +50,10 @@ class TagsController < ApplicationController
|
|||
@tag = Tag.usable.find_normalized!(params[:id])
|
||||
end
|
||||
|
||||
def set_local
|
||||
@local = truthy_param?(:local)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'with-modals'
|
||||
end
|
||||
|
|
|
@ -8,7 +8,8 @@ module WellKnown
|
|||
before_action :set_account
|
||||
before_action :check_account_suspension
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound, ActionController::ParameterMissing, with: :not_found
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||
rescue_from ActionController::ParameterMissing, WebfingerResource::InvalidRequest, with: :bad_request
|
||||
|
||||
def show
|
||||
expires_in 3.days, public: true
|
||||
|
@ -37,6 +38,10 @@ module WellKnown
|
|||
expires_in(3.minutes, public: true) && gone if @account.suspended?
|
||||
end
|
||||
|
||||
def bad_request
|
||||
head 400
|
||||
end
|
||||
|
||||
def not_found
|
||||
head 404
|
||||
end
|
||||
|
|
|
@ -47,7 +47,7 @@ module Admin::ActionLogsHelper
|
|||
I18n.t('admin.action_logs.deleted_status')
|
||||
end
|
||||
when 'Announcement'
|
||||
truncate(attributes['text'])
|
||||
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -77,6 +77,18 @@ module ApplicationHelper
|
|||
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
|
||||
end
|
||||
|
||||
def visibility_icon(status)
|
||||
if status.public_visibility?
|
||||
fa_icon('globe', title: I18n.t('statuses.visibilities.public'))
|
||||
elsif status.unlisted_visibility?
|
||||
fa_icon('unlock', title: I18n.t('statuses.visibilities.unlisted'))
|
||||
elsif status.private_visibility? || status.limited_visibility?
|
||||
fa_icon('lock', title: I18n.t('statuses.visibilities.private'))
|
||||
elsif status.direct_visibility?
|
||||
fa_icon('envelope', title: I18n.t('statuses.visibilities.direct'))
|
||||
end
|
||||
end
|
||||
|
||||
def custom_emoji_tag(custom_emoji, animate = true)
|
||||
if animate
|
||||
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
|
||||
|
@ -136,6 +148,11 @@ module ApplicationHelper
|
|||
text: [params[:title], params[:text], params[:url]].compact.join(' '),
|
||||
}
|
||||
|
||||
permit_visibilities = %w(public unlisted private direct)
|
||||
default_privacy = current_account&.user&.setting_default_privacy
|
||||
permit_visibilities.shift(permit_visibilities.index(default_privacy) + 1) if default_privacy.present?
|
||||
state_params[:visibility] = params[:visibility] if permit_visibilities.include? params[:visibility]
|
||||
|
||||
if user_signed_in?
|
||||
state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {})
|
||||
state_params[:push_subscription] = current_account.user.web_push_subscription(current_session)
|
||||
|
|
|
@ -7,13 +7,13 @@ module HomeHelper
|
|||
}
|
||||
end
|
||||
|
||||
def account_link_to(account, button = '', size: 36, path: nil)
|
||||
def account_link_to(account, button = '', path: nil)
|
||||
content_tag(:div, class: 'account') do
|
||||
content_tag(:div, class: 'account__wrapper') do
|
||||
section = if account.nil?
|
||||
content_tag(:div, class: 'account__display-name') do
|
||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url('avatars/original/missing.png', skip_pipeline: true)})")
|
||||
image_tag(full_asset_url('avatars/original/missing.png', skip_pipeline: true), class: 'account__avatar')
|
||||
end +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:strong, t('about.contact_missing')) +
|
||||
|
@ -23,7 +23,7 @@ module HomeHelper
|
|||
else
|
||||
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
|
||||
content_tag(:div, class: 'account__avatar-wrapper') do
|
||||
content_tag(:div, '', class: 'account__avatar', style: "width: #{size}px; height: #{size}px; background-size: #{size}px #{size}px; background-image: url(#{full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url)})")
|
||||
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
|
||||
end +
|
||||
content_tag(:span, class: 'display-name') do
|
||||
content_tag(:bdi) do
|
||||
|
|
|
@ -68,6 +68,7 @@ module SettingsHelper
|
|||
tr: 'Türkçe',
|
||||
uk: 'Українська',
|
||||
ur: 'اُردُو',
|
||||
vi: 'Tiếng Việt',
|
||||
'zh-CN': '简体中文',
|
||||
'zh-HK': '繁體中文(香港)',
|
||||
'zh-TW': '繁體中文(臺灣)',
|
||||
|
@ -105,4 +106,13 @@ module SettingsHelper
|
|||
safe_join([image_tag(account.avatar.url, width: 15, height: 15, alt: display_name(account), class: 'avatar'), content_tag(:span, account.acct, class: 'username')], ' ')
|
||||
end
|
||||
end
|
||||
|
||||
def picture_hint(hint, picture)
|
||||
if picture.original_filename.nil?
|
||||
hint
|
||||
else
|
||||
link = link_to t('generic.delete'), settings_profile_picture_path(picture.name.to_s), data: { method: :delete }
|
||||
safe_join([hint, link], '<br/>'.html_safe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,11 +15,13 @@ module StatusesHelper
|
|||
end
|
||||
|
||||
def media_summary(status)
|
||||
attachments = { image: 0, video: 0 }
|
||||
attachments = { image: 0, video: 0, audio: 0 }
|
||||
|
||||
status.media_attachments.each do |media|
|
||||
if media.video?
|
||||
attachments[:video] += 1
|
||||
elsif media.audio?
|
||||
attachments[:audio] += 1
|
||||
else
|
||||
attachments[:image] += 1
|
||||
end
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Monkey-patch on monkey-patch.
|
||||
# Because it conflicts with the request.rb patch.
|
||||
class HTTP::Timeout::PerOperationOriginal < HTTP::Timeout::PerOperation
|
||||
def connect(socket_class, host, port, nodelay = false)
|
||||
::Timeout.timeout(@connect_timeout, HTTP::TimeoutError) do
|
||||
@socket = socket_class.open(host, port)
|
||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module WebfingerHelper
|
||||
def webfinger!(uri)
|
||||
hidden_service_uri = /\.(onion|i2p)(:\d+)?$/.match(uri)
|
||||
|
||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if !Rails.configuration.x.access_to_hidden_service && hidden_service_uri
|
||||
|
||||
opts = {
|
||||
ssl: !hidden_service_uri,
|
||||
|
||||
headers: {
|
||||
'User-Agent': Mastodon::Version.user_agent,
|
||||
},
|
||||
|
||||
timeout_class: HTTP::Timeout::PerOperationOriginal,
|
||||
|
||||
timeout_options: {
|
||||
write_timeout: 10,
|
||||
connect_timeout: 5,
|
||||
read_timeout: 10,
|
||||
},
|
||||
}
|
||||
|
||||
Goldfinger::Client.new(uri, opts.merge(Rails.configuration.x.http_client_proxy)).finger
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,37 @@
|
|||
import api from '../api';
|
||||
|
||||
export const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
export const ACCOUNT_NOTE_SUBMIT_SUCCESS = 'ACCOUNT_NOTE_SUBMIT_SUCCESS';
|
||||
export const ACCOUNT_NOTE_SUBMIT_FAIL = 'ACCOUNT_NOTE_SUBMIT_FAIL';
|
||||
|
||||
export function submitAccountNote(id, value) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(submitAccountNoteRequest());
|
||||
|
||||
api(getState).post(`/api/v1/accounts/${id}/note`, {
|
||||
comment: value,
|
||||
}).then(response => {
|
||||
dispatch(submitAccountNoteSuccess(response.data));
|
||||
}).catch(error => dispatch(submitAccountNoteFail(error)));
|
||||
};
|
||||
};
|
||||
|
||||
export function submitAccountNoteRequest() {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function submitAccountNoteSuccess(relationship) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
relationship,
|
||||
};
|
||||
};
|
||||
|
||||
export function submitAccountNoteFail(error) {
|
||||
return {
|
||||
type: ACCOUNT_NOTE_SUBMIT_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -27,6 +27,11 @@ export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL';
|
|||
export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS';
|
||||
export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO';
|
||||
|
||||
export const THUMBNAIL_UPLOAD_REQUEST = 'THUMBNAIL_UPLOAD_REQUEST';
|
||||
export const THUMBNAIL_UPLOAD_SUCCESS = 'THUMBNAIL_UPLOAD_SUCCESS';
|
||||
export const THUMBNAIL_UPLOAD_FAIL = 'THUMBNAIL_UPLOAD_FAIL';
|
||||
export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
|
||||
|
||||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
||||
|
@ -259,6 +264,49 @@ export function uploadCompose(files) {
|
|||
};
|
||||
}
|
||||
|
||||
export const uploadThumbnail = (id, file) => (dispatch, getState) => {
|
||||
dispatch(uploadThumbnailRequest());
|
||||
|
||||
const total = file.size;
|
||||
const data = new FormData();
|
||||
|
||||
data.append('thumbnail', file);
|
||||
|
||||
api(getState).put(`/api/v1/media/${id}`, data, {
|
||||
onUploadProgress: ({ loaded }) => {
|
||||
dispatch(uploadThumbnailProgress(loaded, total));
|
||||
},
|
||||
}).then(({ data }) => {
|
||||
dispatch(uploadThumbnailSuccess(data));
|
||||
}).catch(error => {
|
||||
dispatch(uploadThumbnailFail(id, error));
|
||||
});
|
||||
};
|
||||
|
||||
export const uploadThumbnailRequest = () => ({
|
||||
type: THUMBNAIL_UPLOAD_REQUEST,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const uploadThumbnailProgress = (loaded, total) => ({
|
||||
type: THUMBNAIL_UPLOAD_PROGRESS,
|
||||
loaded,
|
||||
total,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const uploadThumbnailSuccess = media => ({
|
||||
type: THUMBNAIL_UPLOAD_SUCCESS,
|
||||
media,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export const uploadThumbnailFail = error => ({
|
||||
type: THUMBNAIL_UPLOAD_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
});
|
||||
|
||||
export function changeUploadCompose(id, params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(changeUploadComposeRequest());
|
||||
|
@ -276,7 +324,8 @@ export function changeUploadComposeRequest() {
|
|||
type: COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function changeUploadComposeSuccess(media) {
|
||||
return {
|
||||
type: COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN';
|
||||
export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE';
|
||||
|
||||
export function openDropdownMenu(id, placement, keyboard) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard };
|
||||
export function openDropdownMenu(id, placement, keyboard, scroll_key) {
|
||||
return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard, scroll_key };
|
||||
}
|
||||
|
||||
export function closeDropdownMenu(id) {
|
||||
|
|
|
@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
|
|||
|
||||
export function searchTextFromRawStatus (status) {
|
||||
const spoilerText = status.spoiler_text || '';
|
||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||
return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,102 @@
|
|||
export const submitMarkers = () => (dispatch, getState) => {
|
||||
import api from '../api';
|
||||
import { debounce } from 'lodash';
|
||||
import compareId from '../compare_id';
|
||||
import { showAlertForError } from './alerts';
|
||||
|
||||
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
|
||||
|
||||
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
||||
const accessToken = getState().getIn(['meta', 'access_token'], '');
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = getState().getIn(['timelines', 'home', 'items', 0]);
|
||||
const lastNotificationId = getState().getIn(['notifications', 'items', 0, 'id']);
|
||||
|
||||
if (lastHomeId) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastNotificationId) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new XMLHttpRequest();
|
||||
// The Fetch API allows us to perform requests that will be carried out
|
||||
// after the page closes. But that only works if the `keepalive` attribute
|
||||
// is supported.
|
||||
if (window.fetch && 'keepalive' in new Request('')) {
|
||||
fetch('/api/v1/markers', {
|
||||
keepalive: true,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
return;
|
||||
} else if (navigator && navigator.sendBeacon) {
|
||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||
// FormData for DoorKeeper to recognize the token.
|
||||
const formData = new FormData();
|
||||
formData.append('bearer_token', accessToken);
|
||||
for (const [id, value] of Object.entries(params)) {
|
||||
formData.append(`${id}[last_read_id]`, value.last_read_id);
|
||||
}
|
||||
if (navigator.sendBeacon('/api/v1/markers', formData)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.send(JSON.stringify(params));
|
||||
// If neither Fetch nor sendBeacon worked, try to perform a synchronous
|
||||
// request.
|
||||
try {
|
||||
const client = new XMLHttpRequest();
|
||||
|
||||
client.open('POST', '/api/v1/markers', false);
|
||||
client.setRequestHeader('Content-Type', 'application/json');
|
||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||
client.SUBMIT(JSON.stringify(params));
|
||||
} catch (e) {
|
||||
// Do not make the BeforeUnload handler error out
|
||||
}
|
||||
};
|
||||
|
||||
const _buildParams = (state) => {
|
||||
const params = {};
|
||||
|
||||
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
|
||||
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
|
||||
|
||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||
params.home = {
|
||||
last_read_id: lastHomeId,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastNotificationId && compareId(lastNotificationId, state.getIn(['markers', 'notifications'])) > 0) {
|
||||
params.notifications = {
|
||||
last_read_id: lastNotificationId,
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
const debouncedSubmitMarkers = debounce((dispatch, getState) => {
|
||||
const params = _buildParams(getState());
|
||||
|
||||
if (Object.keys(params).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
api().post('/api/v1/markers', params).then(() => {
|
||||
dispatch(submitMarkersSuccess(params));
|
||||
}).catch(error => {
|
||||
dispatch(showAlertForError(error));
|
||||
});
|
||||
}, 300000, { leading: true, trailing: true });
|
||||
|
||||
export function submitMarkersSuccess({ home, notifications }) {
|
||||
return {
|
||||
type: MARKERS_SUBMIT_SUCCESS,
|
||||
home: (home || {}).last_read_id,
|
||||
notifications: (notifications || {}).last_read_id,
|
||||
};
|
||||
};
|
||||
|
||||
export function submitMarkers() {
|
||||
return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
importFetchedStatus,
|
||||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { saveSettings } from './settings';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
@ -70,6 +71,8 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||
filtered = regex && regex.test(searchIndex);
|
||||
}
|
||||
|
||||
dispatch(submitMarkers());
|
||||
|
||||
if (showInColumn) {
|
||||
dispatch(importFetchedAccount(notification.account));
|
||||
|
||||
|
@ -157,6 +160,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) {
|
|||
|
||||
dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent, isLoadingRecent && preferPendingItems));
|
||||
fetchRelatedRelationships(dispatch, response.data);
|
||||
dispatch(submitMarkers());
|
||||
}).catch(error => {
|
||||
dispatch(expandNotificationsFail(error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
|
|
|
@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
|
|||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
|
||||
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
|
||||
export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
|
||||
export const connectHashtagStream = (id, tag, local, accept) => connectTimelineStream(`hashtag:${id}${local ? ':local' : ''}`, `hashtag${local ? ':local' : ''}&tag=${tag}`, null, accept);
|
||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import api, { getLinks } from 'mastodon/api';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import compareId from 'mastodon/compare_id';
|
||||
|
@ -36,13 +37,17 @@ export function updateTimeline(timeline, status, accept) {
|
|||
status,
|
||||
usePendingItems: preferPendingItems,
|
||||
});
|
||||
|
||||
if (timeline === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteFromTimelines(id) {
|
||||
return (dispatch, getState) => {
|
||||
const accountId = getState().getIn(['statuses', id, 'account']);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]);
|
||||
const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id'));
|
||||
const reblogOf = getState().getIn(['statuses', id, 'reblog'], null);
|
||||
|
||||
dispatch({
|
||||
|
@ -98,6 +103,10 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
}).finally(() => {
|
||||
|
@ -107,18 +116,19 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
|
|||
}
|
||||
|
||||
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
|
||||
export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
|
||||
export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
any: parseTags(tags, 'any'),
|
||||
all: parseTags(tags, 'all'),
|
||||
none: parseTags(tags, 'none'),
|
||||
local: local,
|
||||
}, done);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import { render, fireEvent, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import Button from '../button';
|
||||
|
@ -21,16 +21,16 @@ describe('<Button />', () => {
|
|||
|
||||
it('handles click events using the given handler', () => {
|
||||
const handler = jest.fn();
|
||||
const button = shallow(<Button onClick={handler} />);
|
||||
button.find('button').simulate('click');
|
||||
render(<Button onClick={handler}>button</Button>);
|
||||
fireEvent.click(screen.getByText('button'));
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('does not handle click events if props.disabled given', () => {
|
||||
const handler = jest.fn();
|
||||
const button = shallow(<Button onClick={handler} disabled />);
|
||||
button.find('button').simulate('click');
|
||||
render(<Button onClick={handler} disabled>button</Button>);
|
||||
fireEvent.click(screen.getByText('button'));
|
||||
|
||||
expect(handler.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { shortNumberFormat } from 'mastodon/utils/numbers';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class AutosuggestHashtag extends React.PureComponent {
|
||||
|
@ -13,14 +13,28 @@ export default class AutosuggestHashtag extends React.PureComponent {
|
|||
}).isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { tag } = this.props;
|
||||
const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>#<strong>{tag.name}</strong></div>
|
||||
{tag.history !== undefined && <div className='autosuggest-hashtag__uses'><FormattedMessage id='autosuggest_hashtag.per_week' defaultMessage='{count} per week' values={{ count: weeklyUses }} /></div>}
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -220,7 +220,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
<span style={{ display: 'none' }}>{placeholder}</span >
|
||||
|
||||
<Textarea
|
||||
inputRef={this.setTextarea}
|
||||
ref={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// @ts-check
|
||||
|
||||
import { decode } from 'blurhash';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* @typedef BlurhashPropsBase
|
||||
* @property {string?} hash Hash to render
|
||||
* @property {number} width
|
||||
* Width of the blurred region in pixels. Defaults to 32
|
||||
* @property {number} [height]
|
||||
* Height of the blurred region in pixels. Defaults to width
|
||||
* @property {boolean} [dummy]
|
||||
* Whether dummy mode is enabled. If enabled, nothing is rendered
|
||||
* and canvas left untouched
|
||||
*/
|
||||
|
||||
/** @typedef {JSX.IntrinsicElements['canvas'] & BlurhashPropsBase} BlurhashProps */
|
||||
|
||||
/**
|
||||
* Component that is used to render blurred of blurhash string
|
||||
*
|
||||
* @param {BlurhashProps} param1 Props of the component
|
||||
* @returns Canvas which will render blurred region element to embed
|
||||
*/
|
||||
function Blurhash({
|
||||
hash,
|
||||
width = 32,
|
||||
height = width,
|
||||
dummy = false,
|
||||
...canvasProps
|
||||
}) {
|
||||
const canvasRef = /** @type {import('react').MutableRefObject<HTMLCanvasElement>} */ (useRef());
|
||||
|
||||
useEffect(() => {
|
||||
const { current: canvas } = canvasRef;
|
||||
canvas.width = canvas.width; // resets canvas
|
||||
|
||||
if (dummy || !hash) return;
|
||||
|
||||
try {
|
||||
const pixels = decode(hash, width, height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, width, height);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
} catch (err) {
|
||||
console.error('Blurhash decoding failure', { err, hash });
|
||||
}
|
||||
}, [dummy, hash, width, height]);
|
||||
|
||||
return (
|
||||
<canvas {...canvasProps} ref={canvasRef} width={width} height={height} />
|
||||
);
|
||||
}
|
||||
|
||||
Blurhash.propTypes = {
|
||||
hash: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
dummy: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default React.memo(Blurhash);
|
|
@ -0,0 +1,62 @@
|
|||
// @ts-check
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
/**
|
||||
* Returns custom renderer for one of the common counter types
|
||||
*
|
||||
* @param {"statuses" | "following" | "followers"} counterType
|
||||
* Type of the counter
|
||||
* @param {boolean} isBold Whether display number must be displayed in bold
|
||||
* @returns {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||
* Renderer function
|
||||
* @throws If counterType is not covered by this function
|
||||
*/
|
||||
export function counterRenderer(counterType, isBold = true) {
|
||||
/**
|
||||
* @type {(displayNumber: JSX.Element) => JSX.Element}
|
||||
*/
|
||||
const renderCounter = isBold
|
||||
? (displayNumber) => <strong>{displayNumber}</strong>
|
||||
: (displayNumber) => displayNumber;
|
||||
|
||||
switch (counterType) {
|
||||
case 'statuses': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.statuses_counter'
|
||||
defaultMessage='{count, plural, one {{counter} Toot} other {{counter} Toots}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'following': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.following_counter'
|
||||
defaultMessage='{count, plural, one {{counter} Following} other {{counter} Following}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'followers': {
|
||||
return (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='account.followers_counter'
|
||||
defaultMessage='{count, plural, one {{counter} Follower} other {{counter} Followers}}'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: renderCounter(displayNumber),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: throw Error(`Incorrect counter name: ${counterType}. Ensure it accepted by commonCounter function`);
|
||||
}
|
||||
}
|
|
@ -39,14 +39,14 @@ class DropdownMenu extends React.PureComponent {
|
|||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
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();
|
||||
this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ mounted: true });
|
||||
}
|
||||
|
@ -59,29 +59,23 @@ class DropdownMenu extends React.PureComponent {
|
|||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
}
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||
const index = items.indexOf(document.activeElement);
|
||||
let element;
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
element = items[index+1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index+1] || items[0];
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = items[index-1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[index-1] || items[items.length-1];
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
|
@ -89,35 +83,30 @@ class DropdownMenu extends React.PureComponent {
|
|||
} else {
|
||||
element = items[index+1] || items[0];
|
||||
}
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = items[0];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
break;
|
||||
case 'End':
|
||||
element = items[items.length - 1];
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
element = items[items.length-1];
|
||||
break;
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
handleItemKeyPress = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleClick(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
@ -132,7 +121,7 @@ class DropdownMenu extends React.PureComponent {
|
|||
e.preventDefault();
|
||||
this.context.router.history.push(to);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
renderItem (option, i) {
|
||||
if (option === null) {
|
||||
|
@ -142,48 +131,10 @@ class DropdownMenu extends React.PureComponent {
|
|||
const { text, href = '#', target = '_blank', method } = option;
|
||||
|
||||
return (
|
||||
<li
|
||||
className='dropdown-menu__item'
|
||||
key={`${text}-${i}`}
|
||||
>
|
||||
<a
|
||||
href={href}
|
||||
target={target}
|
||||
data-method={method}
|
||||
rel='noopener noreferrer'
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
ref={i === 0 ? this.setFocusRef : null}
|
||||
onClick={this.handleClick}
|
||||
onKeyPress={this.handleItemKeyPress}
|
||||
data-index={i}
|
||||
>
|
||||
|
||||
{/*{i} )*/}
|
||||
|
||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
|
||||
{text}
|
||||
<span> </span>
|
||||
{i === 0 &&
|
||||
<i className='fa fa-eye-o' />
|
||||
}
|
||||
{i === 1 &&
|
||||
<i className='fa fa-bookmark' />
|
||||
}
|
||||
{i === 5 &&
|
||||
<i className='fa fa-comment' />
|
||||
}
|
||||
{i === 6 &&
|
||||
<i className='fa fa-plane' />
|
||||
}
|
||||
{i === 10 &&
|
||||
<i className='fa fa-flag' />
|
||||
}
|
||||
{i === 12 &&
|
||||
<i className='fa fa-gears' />
|
||||
}{i === 13 &&
|
||||
<i className='fa fa-comment' />
|
||||
}
|
||||
</a >
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
@ -250,7 +201,7 @@ export default class Dropdown extends React.PureComponent {
|
|||
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
|
||||
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
if (this.activeElement) {
|
||||
|
@ -258,25 +209,25 @@ export default class Dropdown extends React.PureComponent {
|
|||
this.activeElement = null;
|
||||
}
|
||||
this.props.onClose(this.state.id);
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
switch (e.key) {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
|
@ -284,7 +235,7 @@ export default class Dropdown extends React.PureComponent {
|
|||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleItemClick = e => {
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
@ -299,21 +250,21 @@ export default class Dropdown extends React.PureComponent {
|
|||
e.preventDefault();
|
||||
this.context.router.history.push(to);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
if (this.state.id === this.props.openDropdownId) {
|
||||
this.handleClose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
|
||||
|
|
|
@ -1,26 +1,65 @@
|
|||
// @ts-check
|
||||
import React from 'react';
|
||||
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from './permalink';
|
||||
import { shortNumberFormat } from '../utils/numbers';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
|
||||
/**
|
||||
* Used to render counter of how much people are talking about hashtag
|
||||
*
|
||||
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||
*/
|
||||
const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='trends.counter_by_accounts'
|
||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Hashtag = ({ hashtag }) => (
|
||||
<div className='trends__item'>
|
||||
<div className='trends__item__name'>
|
||||
<Permalink href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
|
||||
<Permalink
|
||||
href={hashtag.get('url')}
|
||||
to={`/timelines/tag/${hashtag.get('name')}`}
|
||||
>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
|
||||
<FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1, count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1)}</strong> }} />
|
||||
<ShortNumber
|
||||
value={
|
||||
hashtag.getIn(['history', 0, 'accounts']) * 1 +
|
||||
hashtag.getIn(['history', 1, 'accounts']) * 1
|
||||
}
|
||||
renderer={accountsCountRenderer}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='trends__item__current'>
|
||||
{shortNumberFormat(hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1)}
|
||||
<ShortNumber
|
||||
value={
|
||||
hashtag.getIn(['history', 0, 'uses']) * 1 +
|
||||
hashtag.getIn(['history', 1, 'uses']) * 1
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='trends__item__sparkline'>
|
||||
<Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}>
|
||||
<Sparklines
|
||||
width={50}
|
||||
height={28}
|
||||
data={hashtag
|
||||
.get('history')
|
||||
.reverse()
|
||||
.map((day) => day.get('uses'))
|
||||
.toArray()}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} />
|
||||
</Sparklines>
|
||||
</div>
|
||||
|
|
|
@ -7,10 +7,11 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import { isIOS } from '../is_mobile';
|
||||
import classNames from 'classnames';
|
||||
import { autoPlayGif, cropImages, displayMedia, useBlurhash } from '../initial_state';
|
||||
import { decode } from 'blurhash';
|
||||
import { debounce } from 'lodash';
|
||||
import Blurhash from 'mastodon/components/blurhash';
|
||||
|
||||
const messages = defineMessages({
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide media' },
|
||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Hide {number, plural, one {image} other {images}}' },
|
||||
});
|
||||
|
||||
class Item extends React.PureComponent {
|
||||
|
@ -73,36 +74,6 @@ class Item extends React.PureComponent {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
|
||||
this._decode();
|
||||
}
|
||||
}
|
||||
|
||||
_decode () {
|
||||
if (!useBlurhash) return;
|
||||
|
||||
const hash = this.props.attachment.get('blurhash');
|
||||
const pixels = decode(hash, 32, 32);
|
||||
|
||||
if (pixels) {
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const imageData = new ImageData(pixels, 32, 32);
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
}
|
||||
|
||||
handleImageLoad = () => {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
@ -165,7 +136,11 @@ class Item extends React.PureComponent {
|
|||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} target='_blank' rel='noopener noreferrer'>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
className='media-gallery__preview'
|
||||
dummy={!useBlurhash}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
|
@ -231,7 +206,13 @@ class Item extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
||||
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': visible && this.state.loaded })} />
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
dummy={!useBlurhash}
|
||||
className={classNames('media-gallery__preview', {
|
||||
'media-gallery__preview--hidden': visible && this.state.loaded,
|
||||
})}
|
||||
/>
|
||||
{visible && thumbnail}
|
||||
</div>
|
||||
);
|
||||
|
@ -266,6 +247,14 @@ class MediaGallery extends React.PureComponent {
|
|||
width: this.props.defaultWidth,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!is(nextProps.media, this.props.media) && nextProps.visible === undefined) {
|
||||
this.setState({ visible: displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all' });
|
||||
|
@ -274,6 +263,14 @@ class MediaGallery extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleResize = debounce(() => {
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}, 250, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
handleOpen = () => {
|
||||
if (this.props.onToggleVisibility) {
|
||||
this.props.onToggleVisibility();
|
||||
|
@ -286,17 +283,27 @@ class MediaGallery extends React.PureComponent {
|
|||
this.props.onOpenMedia(this.props.media, index);
|
||||
}
|
||||
|
||||
handleRef = (node) => {
|
||||
if (node) {
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
|
||||
handleRef = c => {
|
||||
this.node = c;
|
||||
|
||||
this.setState({
|
||||
width: node.offsetWidth,
|
||||
});
|
||||
if (this.node) {
|
||||
this._setDimensions();
|
||||
}
|
||||
}
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.node.offsetWidth;
|
||||
|
||||
// offsetWidth triggers a layout, so only calculate when we need to
|
||||
if (this.props.cacheWidth) {
|
||||
this.props.cacheWidth(width);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
width: width,
|
||||
});
|
||||
}
|
||||
|
||||
isFullSizeEligible() {
|
||||
const { media } = this.props;
|
||||
return media.size === 1 && media.getIn([0, 'meta', 'small', 'aspect']);
|
||||
|
@ -338,7 +345,7 @@ class MediaGallery extends React.PureComponent {
|
|||
</button>
|
||||
);
|
||||
} else if (visible) {
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible)} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' overlay onClick={this.handleOpen} />;
|
||||
} else {
|
||||
spoilerButton = (
|
||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||
|
|
|
@ -66,7 +66,7 @@ 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();
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
this.activeElement = null;
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
|
|
|
@ -2,9 +2,8 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||
import Motion from 'mastodon/features/ui/util/optional_motion';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import escapeTextContentForBrowser from 'escape-html';
|
||||
|
@ -28,8 +27,9 @@ class Poll extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
poll: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
refresh: PropTypes.func,
|
||||
onVote: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -81,7 +81,7 @@ class Poll extends ImmutablePureComponent {
|
|||
tmp[value] = true;
|
||||
this.setState({ selected: tmp });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleOptionChange = ({ target: { value } }) => {
|
||||
this._toggleOption(value);
|
||||
|
@ -93,14 +93,14 @@ class Poll extends ImmutablePureComponent {
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleVote = () => {
|
||||
if (this.props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)));
|
||||
this.props.onVote(Object.keys(this.state.selected));
|
||||
};
|
||||
|
||||
handleRefresh = () => {
|
||||
|
@ -108,7 +108,7 @@ class Poll extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchPoll(this.props.poll.get('id')));
|
||||
this.props.refresh();
|
||||
};
|
||||
|
||||
renderOption (option, optionIndex, showResults) {
|
||||
|
@ -150,7 +150,7 @@ class Poll extends ImmutablePureComponent {
|
|||
)}
|
||||
{showResults && <span className='poll__number'>
|
||||
{Math.round(percent)}%
|
||||
</span >}
|
||||
</span>}
|
||||
|
||||
<span
|
||||
className='poll__option__text'
|
||||
|
@ -158,28 +158,18 @@ class Poll extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
{!!voted && <span className='poll__voted'>
|
||||
<Icon
|
||||
id='check'
|
||||
className='poll__voted__mark'
|
||||
title={intl.formatMessage(messages.voted)}
|
||||
/>
|
||||
</span >}
|
||||
</label >
|
||||
<Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
|
||||
</span>}
|
||||
</label>
|
||||
|
||||
{showResults && (
|
||||
<Motion
|
||||
defaultStyle={{ width: 0 }}
|
||||
style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}
|
||||
>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ width }) =>
|
||||
<span
|
||||
className={classNames('poll__chart', { leading })}
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
|
||||
}
|
||||
</Motion >
|
||||
</Motion>
|
||||
)}
|
||||
</li >
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,18 @@ import { List as ImmutableList } from 'immutable';
|
|||
import classNames from 'classnames';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||
import LoadingIndicator from './loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
const MOUSE_IDLE_DELAY = 300;
|
||||
|
||||
export default class ScrollableList extends PureComponent {
|
||||
const mapStateToProps = (state, { scrollKey }) => {
|
||||
return {
|
||||
preventScroll: scrollKey === state.getIn(['dropdown_menu', 'scroll_key']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class ScrollableList extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
|
@ -32,10 +40,12 @@ export default class ScrollableList extends PureComponent {
|
|||
hasMore: PropTypes.bool,
|
||||
numPending: PropTypes.number,
|
||||
prepend: PropTypes.node,
|
||||
append: PropTypes.node,
|
||||
alwaysPrepend: PropTypes.bool,
|
||||
emptyMessage: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -128,7 +138,7 @@ export default class ScrollableList extends PureComponent {
|
|||
});
|
||||
|
||||
handleMouseIdle = () => {
|
||||
if (this.scrollToTopOnMouseIdle) {
|
||||
if (this.scrollToTopOnMouseIdle && !this.props.preventScroll) {
|
||||
this.setScrollTop(0);
|
||||
}
|
||||
|
||||
|
@ -178,7 +188,7 @@ export default class ScrollableList extends PureComponent {
|
|||
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
|
||||
const pendingChanged = (prevProps.numPending > 0) !== (this.props.numPending > 0);
|
||||
|
||||
if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently)) {
|
||||
if (pendingChanged || someItemInserted && (this.getScrollTop() > 0 || this.mouseMovedRecently || this.props.preventScroll)) {
|
||||
return this.getScrollHeight() - this.getScrollTop();
|
||||
} else {
|
||||
return null;
|
||||
|
@ -280,7 +290,7 @@ export default class ScrollableList extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props;
|
||||
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = React.Children.count(children);
|
||||
|
||||
|
@ -327,6 +337,8 @@ export default class ScrollableList extends PureComponent {
|
|||
))}
|
||||
|
||||
{loadMore}
|
||||
|
||||
{!hasMore && append}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { toShortNumber, pluralReady, DECIMAL_UNITS } from '../utils/numbers';
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @callback ShortNumberRenderer
|
||||
* @param {JSX.Element} displayNumber Number to display
|
||||
* @param {number} pluralReady Number used for pluralization
|
||||
* @returns {JSX.Element} Final render of number
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ShortNumberProps
|
||||
* @property {number} value Number to display in short variant
|
||||
* @property {ShortNumberRenderer} [renderer]
|
||||
* Custom renderer for numbers, provided as a prop. If another renderer
|
||||
* passed as a child of this component, this prop won't be used.
|
||||
* @property {ShortNumberRenderer} [children]
|
||||
* Custom renderer for numbers, provided as a child. If another renderer
|
||||
* passed as a prop of this component, this one will be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Component that renders short big number to a shorter version
|
||||
*
|
||||
* @param {ShortNumberProps} param0 Props for the component
|
||||
* @returns {JSX.Element} Rendered number
|
||||
*/
|
||||
function ShortNumber({ value, renderer, children }) {
|
||||
const shortNumber = toShortNumber(value);
|
||||
const [, division] = shortNumber;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
if (children != null && renderer != null) {
|
||||
console.warn('Both renderer prop and renderer as a child provided. This is a mistake and you really should fix that. Only renderer passed as a child will be used.');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const customRenderer = children != null ? children : renderer;
|
||||
|
||||
const displayNumber = <ShortNumberCounter value={shortNumber} />;
|
||||
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return customRenderer != null
|
||||
? customRenderer(displayNumber, pluralReady(value, division))
|
||||
: displayNumber;
|
||||
}
|
||||
|
||||
ShortNumber.propTypes = {
|
||||
value: PropTypes.number.isRequired,
|
||||
renderer: PropTypes.func,
|
||||
children: PropTypes.func,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} ShortNumberCounterProps
|
||||
* @property {import('../utils/number').ShortNumber} value Short number
|
||||
*/
|
||||
|
||||
/**
|
||||
* Renders short number into corresponding localizable react fragment
|
||||
*
|
||||
* @param {ShortNumberCounterProps} param0 Props for the component
|
||||
* @returns {JSX.Element} FormattedMessage ready to be embedded in code
|
||||
*/
|
||||
function ShortNumberCounter({ value }) {
|
||||
const [rawNumber, unit, maxFractionDigits = 0] = value;
|
||||
|
||||
const count = (
|
||||
<FormattedNumber
|
||||
value={rawNumber}
|
||||
maximumFractionDigits={maxFractionDigits}
|
||||
/>
|
||||
);
|
||||
|
||||
let values = { count, rawNumber };
|
||||
|
||||
switch (unit) {
|
||||
case DECIMAL_UNITS.THOUSAND: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.thousand'
|
||||
defaultMessage='{count}K'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case DECIMAL_UNITS.MILLION: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.million'
|
||||
defaultMessage='{count}M'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case DECIMAL_UNITS.BILLION: {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='units.short.billion'
|
||||
defaultMessage='{count}B'
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// Not sure if we should go farther - @Sasha-Sorokin
|
||||
default: return count;
|
||||
}
|
||||
}
|
||||
|
||||
ShortNumberCounter.propTypes = {
|
||||
value: PropTypes.arrayOf(PropTypes.number),
|
||||
};
|
||||
|
||||
export default React.memo(ShortNumber);
|
|
@ -10,13 +10,14 @@ import StatusContent from './status_content';
|
|||
import StatusActionBar from './status_action_bar';
|
||||
import AttachmentList from './attachment_list';
|
||||
import Card from '../features/status/components/card';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { Audio, MediaGallery, Video } from '../features/ui/util/async-components';
|
||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import { displayMedia, isStaff } from '../initial_state';
|
||||
import { displayMedia } from '../initial_state';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
import Bundle from '../features/ui/components/bundle';
|
||||
|
@ -50,6 +51,13 @@ export const defaultMediaVisibility = (status) => {
|
|||
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
|
@ -58,37 +66,35 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
static propTypes = {
|
||||
status : ImmutablePropTypes.map,
|
||||
account : ImmutablePropTypes.map,
|
||||
otherAccounts : ImmutablePropTypes.list,
|
||||
onClick : PropTypes.func,
|
||||
onReply : PropTypes.func,
|
||||
onFavourite : PropTypes.func,
|
||||
onReblog : PropTypes.func,
|
||||
onDelete : PropTypes.func,
|
||||
onDirect : PropTypes.func,
|
||||
onMention : PropTypes.func,
|
||||
onPin : PropTypes.func,
|
||||
onOpenMedia : PropTypes.func,
|
||||
onOpenVideo : PropTypes.func,
|
||||
onBlock : PropTypes.func,
|
||||
onEmbed : PropTypes.func,
|
||||
onHeightChange : PropTypes.func,
|
||||
onToggleHidden : PropTypes.func,
|
||||
muted : PropTypes.bool,
|
||||
hidden : PropTypes.bool,
|
||||
unread : PropTypes.bool,
|
||||
onMoveUp : PropTypes.func,
|
||||
onMoveDown : PropTypes.func,
|
||||
showThread : PropTypes.bool,
|
||||
threadsCompile : PropTypes.bool,
|
||||
getScrollPosition : PropTypes.func,
|
||||
status: ImmutablePropTypes.map,
|
||||
account: ImmutablePropTypes.map,
|
||||
otherAccounts: ImmutablePropTypes.list,
|
||||
onClick: PropTypes.func,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
onMention: PropTypes.func,
|
||||
onPin: PropTypes.func,
|
||||
onOpenMedia: PropTypes.func,
|
||||
onOpenVideo: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onEmbed: PropTypes.func,
|
||||
onHeightChange: PropTypes.func,
|
||||
onToggleHidden: PropTypes.func,
|
||||
onToggleCollapsed: PropTypes.func,
|
||||
muted: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
showThread: PropTypes.bool,
|
||||
getScrollPosition: PropTypes.func,
|
||||
updateScrollBottom: PropTypes.func,
|
||||
cacheMediaWidth : PropTypes.func,
|
||||
cachedMediaWidth : PropTypes.number,
|
||||
};
|
||||
static defaultProps = {
|
||||
threadsCompile: true,
|
||||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
scrollKey: PropTypes.string,
|
||||
};
|
||||
|
||||
// Avoid checking props that are functions (and whose equality will always
|
||||
|
@ -102,62 +108,23 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status),
|
||||
statusId : undefined,
|
||||
statusId: undefined,
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
||||
return {
|
||||
showMedia: defaultMediaVisibility(nextProps.status),
|
||||
statusId : nextProps.status.get('id'),
|
||||
statusId: nextProps.status.get('id'),
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Track height changes we know about to compensate scrolling
|
||||
componentDidMount() {
|
||||
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
}
|
||||
|
||||
getSnapshotBeforeUpdate() {
|
||||
if (this.props.getScrollPosition) {
|
||||
return this.props.getScrollPosition();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Compensate height changes
|
||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
||||
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
|
||||
|
||||
if (doShowCard && !this.didShowCard) {
|
||||
this.didShowCard = true;
|
||||
|
||||
if (snapshot !== null && this.props.updateScrollBottom) {
|
||||
if (this.node && this.node.offsetTop < snapshot.top) {
|
||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.node && this.props.getScrollPosition) {
|
||||
const position = this.props.getScrollPosition();
|
||||
if (position !== null && this.node.offsetTop < position.top) {
|
||||
requestAnimationFrame(() => {
|
||||
this.props.updateScrollBottom(position.height - position.top);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleMediaVisibility = () => {
|
||||
this.setState({ showMedia: !this.state.showMedia });
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
if (this.props.onClick) {
|
||||
|
@ -171,7 +138,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
const { status } = this.props;
|
||||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
};
|
||||
}
|
||||
|
||||
handleExpandClick = (e) => {
|
||||
if (this.props.onClick) {
|
||||
|
@ -187,7 +154,7 @@ class Status extends ImmutablePureComponent {
|
|||
const { status } = this.props;
|
||||
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
|
@ -195,40 +162,31 @@ class Status extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
this.context.router.history.push(`/accounts/${id}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
this.props.onToggleHidden(this._properStatus());
|
||||
};
|
||||
}
|
||||
|
||||
handleCollapsedToggle = isCollapsed => {
|
||||
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
|
||||
};
|
||||
|
||||
renderLoadingMediaGallery() {
|
||||
return (<div
|
||||
className='media-gallery'
|
||||
style={{ height: '110px' }}
|
||||
/>);
|
||||
}
|
||||
|
||||
renderLoadingVideoPlayer() {
|
||||
return (<div
|
||||
className='video-player'
|
||||
style={{ height: '110px' }}
|
||||
/>);
|
||||
renderLoadingMediaGallery () {
|
||||
return <div className='media-gallery' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
renderLoadingAudioPlayer() {
|
||||
return (<div
|
||||
className='audio-player'
|
||||
style={{ height: '110px' }}
|
||||
/>);
|
||||
renderLoadingVideoPlayer () {
|
||||
return <div className='video-player' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
this.props.onOpenVideo(media, startTime);
|
||||
};
|
||||
renderLoadingAudioPlayer () {
|
||||
return <div className='audio-player' style={{ height: '110px' }} />;
|
||||
}
|
||||
|
||||
handleOpenVideo = (media, options) => {
|
||||
this.props.onOpenVideo(media, options);
|
||||
}
|
||||
|
||||
handleHotkeyOpenMedia = e => {
|
||||
const { onOpenMedia, onOpenVideo } = this.props;
|
||||
|
@ -240,56 +198,56 @@ class Status extends ImmutablePureComponent {
|
|||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
// TODO: toggle play/paused?
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
|
||||
onOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
|
||||
} else {
|
||||
onOpenMedia(status.get('media_attachments'), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.props.onReply(this._properStatus(), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyFavourite = () => {
|
||||
this.props.onFavourite(this._properStatus());
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyBoost = e => {
|
||||
this.props.onReblog(this._properStatus(), e);
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyMention = e => {
|
||||
e.preventDefault();
|
||||
this.props.onMention(this._properStatus().get('account'), this.context.router.history);
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyMoveUp = e => {
|
||||
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyMoveDown = e => {
|
||||
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyToggleHidden = () => {
|
||||
this.props.onToggleHidden(this._properStatus());
|
||||
};
|
||||
}
|
||||
|
||||
handleHotkeyToggleSensitive = () => {
|
||||
this.handleToggleMediaVisibility();
|
||||
};
|
||||
}
|
||||
|
||||
_properStatus() {
|
||||
_properStatus () {
|
||||
const { status } = this.props;
|
||||
|
||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||
|
@ -301,13 +259,13 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
render () {
|
||||
let media = null;
|
||||
let statusAvatar, prepend, rebloggedByText;
|
||||
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread } = this.props;
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
|
@ -316,109 +274,68 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const handlers = this.props.muted ? {} : {
|
||||
reply : this.handleHotkeyReply,
|
||||
favourite : this.handleHotkeyFavourite,
|
||||
boost : this.handleHotkeyBoost,
|
||||
mention : this.handleHotkeyMention,
|
||||
open : this.handleHotkeyOpen,
|
||||
openProfile : this.handleHotkeyOpenProfile,
|
||||
moveUp : this.handleHotkeyMoveUp,
|
||||
moveDown : this.handleHotkeyMoveDown,
|
||||
toggleHidden : this.handleHotkeyToggleHidden,
|
||||
reply: this.handleHotkeyReply,
|
||||
favourite: this.handleHotkeyFavourite,
|
||||
boost: this.handleHotkeyBoost,
|
||||
mention: this.handleHotkeyMention,
|
||||
open: this.handleHotkeyOpen,
|
||||
openProfile: this.handleHotkeyOpenProfile,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleHotkeyToggleHidden,
|
||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||
openMedia : this.handleHotkeyOpenMedia,
|
||||
openMedia: this.handleHotkeyOpenMedia,
|
||||
};
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div
|
||||
ref={this.handleRef}
|
||||
className={classNames('status__wrapper', { focusable: !this.props.muted })}
|
||||
tabIndex='0'
|
||||
>
|
||||
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
|
||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||
{status.get('content')}
|
||||
</div >
|
||||
</HotKeys >
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
|
||||
const minHandlers = this.props.muted ? {} : {
|
||||
moveUp : this.handleHotkeyMoveUp,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={minHandlers}>
|
||||
<div
|
||||
className='status__wrapper status__wrapper--filtered focusable'
|
||||
tabIndex='0'
|
||||
ref={this.handleRef}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='status.filtered'
|
||||
defaultMessage='Filtered'
|
||||
/>
|
||||
</div >
|
||||
</HotKeys >
|
||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
if (featured) {
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon
|
||||
id='thumb-tack'
|
||||
className='status__prepend-icon'
|
||||
fixedWidth
|
||||
/></div >
|
||||
<FormattedMessage
|
||||
id='status.pinned'
|
||||
defaultMessage='Pinned toot'
|
||||
/>
|
||||
</div >
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
|
||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
|
||||
</div>
|
||||
);
|
||||
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
||||
|
||||
prepend = (
|
||||
<div className='status__prepend'>
|
||||
<div className='status__prepend-icon-wrapper'><Icon
|
||||
id='retweet'
|
||||
className='status__prepend-icon'
|
||||
fixedWidth
|
||||
/>
|
||||
</div >
|
||||
<FormattedMessage
|
||||
id='status.reblogged_by'
|
||||
defaultMessage='{name} boosted'
|
||||
values={{
|
||||
name: <a
|
||||
onClick={this.handleAccountClick}
|
||||
data-id={status.getIn(['account', 'id'])}
|
||||
href={status.getIn(['account', 'url'])}
|
||||
className='status__display-name muted'
|
||||
>
|
||||
<bdi ><strong dangerouslySetInnerHTML={display_name_html} /></bdi >
|
||||
</a >,
|
||||
}}
|
||||
/>
|
||||
</div >
|
||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
rebloggedByText = intl.formatMessage({
|
||||
id : 'status.reblogged_by',
|
||||
defaultMessage: '{name} boosted',
|
||||
}, { name: status.getIn(['account', 'acct']) });
|
||||
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
|
||||
|
||||
account = status.get('account');
|
||||
status = status.get('reblog');
|
||||
status = status.get('reblog');
|
||||
}
|
||||
/**
|
||||
* manage attached medias
|
||||
* */
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
|
@ -431,29 +348,28 @@ class Status extends ImmutablePureComponent {
|
|||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle
|
||||
fetchComponent={Audio}
|
||||
loading={this.renderLoadingAudioPlayer}
|
||||
>
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={attachment.get('url')}
|
||||
alt={attachment.get('description')}
|
||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||
peaks={[0]}
|
||||
height={70}
|
||||
width={this.props.cachedMediaWidth}
|
||||
height={110}
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
)}
|
||||
</Bundle >
|
||||
</Bundle>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle
|
||||
fetchComponent={Video}
|
||||
loading={this.renderLoadingVideoPlayer}
|
||||
>
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={attachment.get('preview_url')}
|
||||
|
@ -470,14 +386,11 @@ class Status extends ImmutablePureComponent {
|
|||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle >
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle
|
||||
fetchComponent={MediaGallery}
|
||||
loading={this.renderLoadingMediaGallery}
|
||||
>
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
<Component
|
||||
media={status.get('media_attachments')}
|
||||
|
@ -490,7 +403,7 @@ class Status extends ImmutablePureComponent {
|
|||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
/>
|
||||
)}
|
||||
</Bundle >
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
|
||||
|
@ -501,113 +414,56 @@ class Status extends ImmutablePureComponent {
|
|||
compact
|
||||
cacheWidth={this.props.cacheMediaWidth}
|
||||
defaultWidth={this.props.cachedMediaWidth}
|
||||
sensitive={status.get('sensitive')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* avatars
|
||||
* */
|
||||
if (otherAccounts && otherAccounts.size > 0) {
|
||||
statusAvatar = (<AvatarComposite
|
||||
accounts={otherAccounts}
|
||||
size={48}
|
||||
/>);
|
||||
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
|
||||
} else if (account === undefined || account === null) {
|
||||
statusAvatar = (<Avatar
|
||||
account={status.get('account')}
|
||||
size={48}
|
||||
/>);
|
||||
statusAvatar = <Avatar account={status.get('account')} size={48} />;
|
||||
} else {
|
||||
statusAvatar = (<AvatarOverlay
|
||||
account={status.get('account')}
|
||||
friend={account}
|
||||
/>);
|
||||
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
|
||||
}
|
||||
|
||||
const visibilityIconInfo = {
|
||||
'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
|
||||
'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
|
||||
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
|
||||
'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
|
||||
};
|
||||
|
||||
const visibilityIcon = visibilityIconInfo[status.get('visibility')];
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div
|
||||
className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, {
|
||||
'status__wrapper-reply': !!status.get('in_reply_to_id'),
|
||||
read : unread === false,
|
||||
focusable : !this.props.muted,
|
||||
})}
|
||||
tabIndex={this.props.muted ? null : 0}
|
||||
data-featured={featured ? 'true' : null}
|
||||
aria-label={textForScreenReader(intl, status, rebloggedByText)}
|
||||
ref={this.handleRef}
|
||||
>
|
||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||
{prepend}
|
||||
|
||||
<div
|
||||
className={classNames('status', `status-${status.get('visibility')}`, {
|
||||
'status-reply': !!status.get('in_reply_to_id'),
|
||||
muted : this.props.muted,
|
||||
read : unread === false,
|
||||
})}
|
||||
data-id={status.get('id')}
|
||||
>
|
||||
<div
|
||||
className='status__expand'
|
||||
onClick={this.handleExpandClick}
|
||||
role='presentation'
|
||||
/>
|
||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||
<div className='status__info'>
|
||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||
|
||||
{isStaff && (<div className='administrate-stuff pull-left'>
|
||||
<a
|
||||
href={`/admin/accounts/${status.getIn(['account', 'id'])}`}
|
||||
>
|
||||
<i className='fa fa-gears' />
|
||||
</a >
|
||||
</div >
|
||||
)}
|
||||
<a
|
||||
href={status.get('url')}
|
||||
className='status__relative-time'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
><RelativeTimestamp timestamp={status.get('created_at')} /></a >
|
||||
|
||||
<a
|
||||
onClick={this.handleAccountClick}
|
||||
data-id={status.getIn(['account', 'id'])}
|
||||
href={status.getIn(['account', 'url'])}
|
||||
title={status.getIn(['account', 'acct'])}
|
||||
className='status__display-name'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div >
|
||||
</div>
|
||||
|
||||
<DisplayName
|
||||
account={status.get('account')}
|
||||
others={otherAccounts}
|
||||
/>
|
||||
</a >
|
||||
</div >
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
status={status}
|
||||
onClick={this.handleClick}
|
||||
expanded={!status.get('hidden')}
|
||||
showThread={showThread}
|
||||
onExpandedToggle={this.handleExpandedToggle}
|
||||
collapsable
|
||||
onCollapsedToggle={this.handleCollapsedToggle}
|
||||
/>
|
||||
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
|
||||
|
||||
{media}
|
||||
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={account} {...other} />
|
||||
</div >
|
||||
</div >
|
||||
</HotKeys >
|
||||
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onPin: PropTypes.func,
|
||||
onBookmark: PropTypes.func,
|
||||
withDismiss: PropTypes.bool,
|
||||
scrollKey: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
|
@ -229,7 +230,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { status, relationship, intl, withDismiss } = this.props;
|
||||
const { status, relationship, intl, withDismiss, scrollKey } = this.props;
|
||||
|
||||
const mutingConversation = status.get('muted');
|
||||
const anonymousAccess = !me;
|
||||
|
@ -237,9 +238,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
const account = status.get('account');
|
||||
|
||||
let menu = [];
|
||||
let reblogIcon = 'retweet';
|
||||
let replyIcon;
|
||||
let replyTitle;
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
|
||||
|
||||
|
@ -259,10 +257,6 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
if (status.getIn(['account', 'id']) === me) {
|
||||
if (publicStatus) {
|
||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||
} else {
|
||||
if (status.get('visibility') === 'private') {
|
||||
menu.push({ text: intl.formatMessage(status.get('reblogged') ? messages.cancel_reblog_private : messages.reblog_private), action: this.handleReblogClick });
|
||||
}
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick });
|
||||
|
@ -305,12 +299,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (status.get('visibility') === 'direct') {
|
||||
reblogIcon = 'envelope';
|
||||
} else if (status.get('visibility') === 'private') {
|
||||
reblogIcon = 'lock';
|
||||
}
|
||||
|
||||
let replyIcon;
|
||||
let replyTitle;
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyTitle = intl.formatMessage(messages.reply);
|
||||
|
@ -319,6 +309,19 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||
|
||||
let reblogTitle = '';
|
||||
if (status.get('reblogged')) {
|
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||
} else if (publicStatus) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog);
|
||||
} else if (reblogPrivate) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||
} else {
|
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||
}
|
||||
|
||||
const shareButton = ('share' in navigator) && publicStatus && (
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} />
|
||||
);
|
||||
|
@ -326,12 +329,21 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='status__action-bar'>
|
||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
||||
<IconButton className='status__action-bar-button' disabled={!publicStatus} active={status.get('reblogged')} pressed={status.get('reblogged')} title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} />
|
||||
<IconButton className='status__action-bar-button' disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||
{shareButton}
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
disabled={anonymousAccess}
|
||||
status={status}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -99,6 +99,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
contextType={timelineId}
|
||||
scrollKey={this.props.scrollKey}
|
||||
showThread
|
||||
/>
|
||||
))
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const TimelineHint = ({ resource, url }) => (
|
||||
<div className='timeline-hint'>
|
||||
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
|
||||
<br />
|
||||
<a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
|
||||
</div>
|
||||
);
|
||||
|
||||
TimelineHint.propTypes = {
|
||||
resource: PropTypes.node.isRequired,
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default TimelineHint;
|
|
@ -12,7 +12,7 @@ const mapStateToProps = state => ({
|
|||
openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { status, items }) => ({
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, dropdownPlacement, keyboard) {
|
||||
if (status) {
|
||||
dispatch(fetchRelationships([status.getIn(['account', 'id'])]));
|
||||
|
@ -22,7 +22,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({
|
|||
status,
|
||||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey));
|
||||
},
|
||||
|
||||
onClose(id) {
|
||||
|
|
|
@ -1,8 +1,25 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import Poll from 'mastodon/components/poll';
|
||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||
|
||||
const mapDispatchToProps = (dispatch, { pollId }) => ({
|
||||
refresh: debounce(
|
||||
() => {
|
||||
dispatch(fetchPoll(pollId));
|
||||
},
|
||||
1000,
|
||||
{ leading: true },
|
||||
),
|
||||
|
||||
onVote (choices) {
|
||||
dispatch(vote(pollId, choices));
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { pollId }) => ({
|
||||
poll: state.getIn(['polls', pollId]),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(Poll);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Poll);
|
||||
|
|
|
@ -150,8 +150,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(openModal('MEDIA', { media, index }));
|
||||
},
|
||||
|
||||
onOpenVideo (media, time) {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
onOpenVideo (media, options) {
|
||||
dispatch(openModal('VIDEO', { media, options }));
|
||||
},
|
||||
|
||||
onBlock (status) {
|
||||
|
|
|
@ -38,7 +38,7 @@ export default class TimelineContainer extends React.PureComponent {
|
|||
let timeline;
|
||||
|
||||
if (hashtag) {
|
||||
timeline = <HashtagTimeline hashtag={hashtag} />;
|
||||
timeline = <HashtagTimeline hashtag={hashtag} local={local} />;
|
||||
} else {
|
||||
timeline = <PublicTimeline local={local} />;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
import { is } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'account_note.placeholder', defaultMessage: 'Click to add a note' },
|
||||
});
|
||||
|
||||
class InlineAlert extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
show: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
mountMessage: false,
|
||||
};
|
||||
|
||||
static TRANSITION_DELAY = 200;
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
this.setState({ mountMessage: true });
|
||||
} else if (this.props.show && !nextProps.show) {
|
||||
setTimeout(() => this.setState({ mountMessage: false }), InlineAlert.TRANSITION_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { show } = this.props;
|
||||
const { mountMessage } = this.state;
|
||||
|
||||
return (
|
||||
<span aria-live='polite' role='status' className='inline-alert' style={{ opacity: show ? 1 : 0 }}>
|
||||
{mountMessage && <FormattedMessage id='generic.saved' defaultMessage='Saved' />}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @injectIntl
|
||||
class AccountNote extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
value: PropTypes.string,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
value: null,
|
||||
saving: false,
|
||||
saved: false,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
this._reset();
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const accountWillChange = !is(this.props.account, nextProps.account);
|
||||
const newState = {};
|
||||
|
||||
if (accountWillChange && this._isDirty()) {
|
||||
this._save(false);
|
||||
}
|
||||
|
||||
if (accountWillChange || nextProps.value === this.state.value) {
|
||||
newState.saving = false;
|
||||
}
|
||||
|
||||
if (this.props.value !== nextProps.value) {
|
||||
newState.value = nextProps.value;
|
||||
}
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this._isDirty()) {
|
||||
this._save(false);
|
||||
}
|
||||
}
|
||||
|
||||
setTextareaRef = c => {
|
||||
this.textarea = c;
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({ value: e.target.value, saving: false });
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
this._save();
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.blur();
|
||||
}
|
||||
} else if (e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
|
||||
this._reset(() => {
|
||||
if (this.textarea) {
|
||||
this.textarea.blur();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
if (this._isDirty()) {
|
||||
this._save();
|
||||
}
|
||||
}
|
||||
|
||||
_save (showMessage = true) {
|
||||
this.setState({ saving: true }, () => this.props.onSave(this.state.value));
|
||||
|
||||
if (showMessage) {
|
||||
this.setState({ saved: true }, () => setTimeout(() => this.setState({ saved: false }), 2000));
|
||||
}
|
||||
}
|
||||
|
||||
_reset (callback) {
|
||||
this.setState({ value: this.props.value }, callback);
|
||||
}
|
||||
|
||||
_isDirty () {
|
||||
return !this.state.saving && this.props.value !== null && this.state.value !== null && this.state.value !== this.props.value;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, intl } = this.props;
|
||||
const { value, saved } = this.state;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account__header__account-note'>
|
||||
<label htmlFor={`account-note-${account.get('id')}`}>
|
||||
<FormattedMessage id='account.account_note_header' defaultMessage='Note' /> <InlineAlert show={saved} />
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
id={`account-note-${account.get('id')}`}
|
||||
className='account__header__account-note__content'
|
||||
disabled={this.props.value === null || value === null}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value || ''}
|
||||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleBlur}
|
||||
ref={this.setTextareaRef}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue