Merge remote-tracking branch 'tootsuite/main' into custom/quote
# Conflicts: # app/services/fetch_link_card_service.rb
This commit is contained in:
commit
71530857a5
@ -1,255 +1,189 @@
|
|||||||
version: 2
|
version: 2.1
|
||||||
|
|
||||||
aliases:
|
orbs:
|
||||||
- &defaults
|
ruby: circleci/ruby@1.2.0
|
||||||
|
node: circleci/node@4.7.0
|
||||||
|
|
||||||
|
executors:
|
||||||
|
default:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/ruby:2.7-buster-node
|
- image: cimg/ruby:<< parameters.ruby-version >>
|
||||||
environment: &ruby_environment
|
environment:
|
||||||
BUNDLE_JOBS: 3
|
BUNDLE_JOBS: 3
|
||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
BUNDLE_APP_CONFIG: ./.bundle/
|
CONTINUOUS_INTEGRATION: true
|
||||||
BUNDLE_PATH: ./vendor/bundle/
|
|
||||||
DB_HOST: localhost
|
DB_HOST: localhost
|
||||||
DB_USER: root
|
DB_USER: root
|
||||||
RAILS_ENV: test
|
|
||||||
ALLOW_NOPAM: true
|
|
||||||
CONTINUOUS_INTEGRATION: true
|
|
||||||
DISABLE_SIMPLECOV: true
|
DISABLE_SIMPLECOV: true
|
||||||
PAM_ENABLED: true
|
RAILS_ENV: test
|
||||||
PAM_DEFAULT_SERVICE: pam_test
|
- image: cimg/postgres:14.0
|
||||||
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
environment:
|
||||||
working_directory: ~/projects/mastodon/
|
POSTGRES_USER: root
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
- image: circleci/redis:6-alpine
|
||||||
|
|
||||||
- &attach_workspace
|
commands:
|
||||||
attach_workspace:
|
install-system-dependencies:
|
||||||
at: ~/projects/
|
|
||||||
|
|
||||||
- &persist_to_workspace
|
|
||||||
persist_to_workspace:
|
|
||||||
root: ~/projects/
|
|
||||||
paths:
|
|
||||||
- ./mastodon/
|
|
||||||
|
|
||||||
- &restore_ruby_dependencies
|
|
||||||
restore_cache:
|
|
||||||
keys:
|
|
||||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
|
||||||
- v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-
|
|
||||||
- v3-ruby-dependencies-
|
|
||||||
|
|
||||||
- &install_steps
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
|
||||||
- *attach_workspace
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v2-node-dependencies-{{ checksum "yarn.lock" }}
|
|
||||||
- v2-node-dependencies-
|
|
||||||
- run:
|
- run:
|
||||||
name: Install yarn dependencies
|
|
||||||
command: yarn install --frozen-lockfile
|
|
||||||
- save_cache:
|
|
||||||
key: v2-node-dependencies-{{ checksum "yarn.lock" }}
|
|
||||||
paths:
|
|
||||||
- ./node_modules/
|
|
||||||
- *persist_to_workspace
|
|
||||||
|
|
||||||
- &install_system_dependencies
|
|
||||||
run:
|
|
||||||
name: Install system dependencies
|
name: Install system dependencies
|
||||||
command: |
|
command: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
|
||||||
|
install-ruby-dependencies:
|
||||||
- &install_ruby_dependencies
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
|
||||||
- *install_system_dependencies
|
|
||||||
- run:
|
- run:
|
||||||
name: Set Ruby version
|
|
||||||
command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
|
|
||||||
- *restore_ruby_dependencies
|
|
||||||
- run:
|
|
||||||
name: Set bundler settings
|
|
||||||
command: |
|
command: |
|
||||||
bundle config --local clean 'true'
|
bundle config clean 'true'
|
||||||
bundle config --local deployment 'true'
|
bundle config frozen 'true'
|
||||||
bundle config --local with 'pam_authentication'
|
bundle config without 'development production'
|
||||||
bundle config --local without 'development production'
|
name: Set bundler settings
|
||||||
bundle config --local frozen 'true'
|
- ruby/install-deps:
|
||||||
bundle config --local path $BUNDLE_PATH
|
bundler-version: '2.2.31'
|
||||||
|
key: ruby<< parameters.ruby-version >>-gems-v1
|
||||||
|
wait-db:
|
||||||
|
steps:
|
||||||
- run:
|
- run:
|
||||||
name: Install bundler dependencies
|
command: dockerize -wait tcp://localhost:5432 -wait tcp://localhost:6379 -timeout 1m
|
||||||
command: bundle check || (bundle install && bundle clean)
|
name: Wait for PostgreSQL and Redis
|
||||||
- save_cache:
|
|
||||||
key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }}
|
|
||||||
paths:
|
|
||||||
- ./.bundle/
|
|
||||||
- ./vendor/bundle/
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/projects/
|
|
||||||
paths:
|
|
||||||
- ./mastodon/.bundle/
|
|
||||||
- ./mastodon/vendor/bundle/
|
|
||||||
|
|
||||||
- &test_steps
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/ruby:3.0-node
|
||||||
|
environment:
|
||||||
|
RAILS_ENV: test
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- node/install-packages:
|
||||||
|
cache-version: v1
|
||||||
|
pkg-manager: yarn
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails assets:precompile
|
||||||
|
name: Precompile assets
|
||||||
|
- persist_to_workspace:
|
||||||
|
paths:
|
||||||
|
- public/assets
|
||||||
|
- public/packs-test
|
||||||
|
root: .
|
||||||
|
|
||||||
|
test:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
type: string
|
||||||
|
executor:
|
||||||
|
name: default
|
||||||
|
ruby-version: << parameters.ruby-version >>
|
||||||
|
environment:
|
||||||
|
ALLOW_NOPAM: true
|
||||||
|
PAM_ENABLED: true
|
||||||
|
PAM_DEFAULT_SERVICE: pam_test
|
||||||
|
PAM_CONTROLLED_SERVICE: pam_test_controlled
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- checkout
|
||||||
- *install_system_dependencies
|
- install-system-dependencies
|
||||||
- run:
|
- run:
|
||||||
name: Install FFMPEG
|
command: sudo apt-get install -y ffmpeg imagemagick libpam-dev
|
||||||
command: sudo apt-get install -y ffmpeg
|
name: Install additional system dependencies
|
||||||
|
- run:
|
||||||
|
command: bundle config with 'pam_authentication'
|
||||||
|
name: Enable PAM authentication
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: << parameters.ruby-version >>
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
- wait-db
|
||||||
- run:
|
- run:
|
||||||
name: Load database schema
|
|
||||||
command: ./bin/rails db:create db:schema:load db:seed
|
command: ./bin/rails db:create db:schema:load db:seed
|
||||||
- run:
|
name: Load database schema
|
||||||
name: Run rspec in parallel
|
- ruby/rspec-test
|
||||||
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
|
|
||||||
<<: *install_steps
|
|
||||||
|
|
||||||
install-ruby2.7:
|
|
||||||
<<: *defaults
|
|
||||||
<<: *install_ruby_dependencies
|
|
||||||
|
|
||||||
install-ruby2.6:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:2.6-buster-node
|
|
||||||
environment: *ruby_environment
|
|
||||||
<<: *install_ruby_dependencies
|
|
||||||
|
|
||||||
install-ruby3.0:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:3.0-buster-node
|
|
||||||
environment: *ruby_environment
|
|
||||||
<<: *install_ruby_dependencies
|
|
||||||
|
|
||||||
build:
|
|
||||||
<<: *defaults
|
|
||||||
steps:
|
|
||||||
- *attach_workspace
|
|
||||||
- *install_system_dependencies
|
|
||||||
- run:
|
|
||||||
name: Precompile assets
|
|
||||||
command: ./bin/rails assets:precompile
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: ~/projects/
|
|
||||||
paths:
|
|
||||||
- ./mastodon/public/assets
|
|
||||||
- ./mastodon/public/packs-test/
|
|
||||||
|
|
||||||
test-migrations:
|
test-migrations:
|
||||||
<<: *defaults
|
executor:
|
||||||
docker:
|
name: default
|
||||||
- image: circleci/ruby:2.7-buster-node
|
ruby-version: '3.0'
|
||||||
environment: *ruby_environment
|
|
||||||
- image: circleci/postgres:12.2
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: root
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
- image: circleci/redis:5-alpine
|
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- checkout
|
||||||
- *install_system_dependencies
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
- run:
|
- run:
|
||||||
name: Create database
|
|
||||||
command: ./bin/rails db:create
|
command: ./bin/rails db:create
|
||||||
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
- run:
|
- run:
|
||||||
name: Run migrations
|
|
||||||
command: ./bin/rails db:migrate
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all remaining migrations
|
||||||
|
|
||||||
test-ruby2.7:
|
test-two-step-migrations:
|
||||||
<<: *defaults
|
executor:
|
||||||
docker:
|
name: default
|
||||||
- image: circleci/ruby:2.7-buster-node
|
ruby-version: '3.0'
|
||||||
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-ruby2.6:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:2.6-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-ruby3.0:
|
|
||||||
<<: *defaults
|
|
||||||
docker:
|
|
||||||
- image: circleci/ruby:3.0-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:14-buster
|
|
||||||
steps:
|
steps:
|
||||||
- *attach_workspace
|
- checkout
|
||||||
|
- install-system-dependencies
|
||||||
|
- install-ruby-dependencies:
|
||||||
|
ruby-version: '3.0'
|
||||||
|
- wait-db
|
||||||
- run:
|
- run:
|
||||||
name: Run jest
|
command: ./bin/rails db:create
|
||||||
command: yarn test:jest
|
name: Create database
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate VERSION=20171010025614
|
||||||
|
name: Run migrations up to v2.0.0
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails tests:migrations:populate_v2
|
||||||
|
name: Populate database with test data
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all pre-deployment migrations
|
||||||
|
evironment:
|
||||||
|
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||||
|
- run:
|
||||||
|
command: ./bin/rails db:migrate
|
||||||
|
name: Run all post-deployment remaining migrations
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2
|
version: 2
|
||||||
build-and-test:
|
build-and-test:
|
||||||
jobs:
|
jobs:
|
||||||
- install
|
- build
|
||||||
- install-ruby2.7:
|
- test:
|
||||||
|
matrix:
|
||||||
|
parameters:
|
||||||
|
ruby-version:
|
||||||
|
- '2.7'
|
||||||
|
- '3.0'
|
||||||
|
name: test-ruby<< matrix.ruby-version >>
|
||||||
requires:
|
requires:
|
||||||
- install
|
- build
|
||||||
- install-ruby2.6:
|
|
||||||
requires:
|
|
||||||
- install
|
|
||||||
- install-ruby2.7
|
|
||||||
- install-ruby3.0:
|
|
||||||
requires:
|
|
||||||
- install
|
|
||||||
- install-ruby2.7
|
|
||||||
- build:
|
|
||||||
requires:
|
|
||||||
- install-ruby2.7
|
|
||||||
- test-migrations:
|
- test-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.7
|
|
||||||
- test-ruby2.7:
|
|
||||||
requires:
|
|
||||||
- install-ruby2.7
|
|
||||||
- build
|
- build
|
||||||
- test-ruby2.6:
|
- test-two-step-migrations:
|
||||||
requires:
|
requires:
|
||||||
- install-ruby2.6
|
|
||||||
- build
|
- build
|
||||||
- test-ruby3.0:
|
- node/run:
|
||||||
|
cache-version: v1
|
||||||
|
name: test-webui
|
||||||
|
pkg-manager: yarn
|
||||||
requires:
|
requires:
|
||||||
- install-ruby3.0
|
|
||||||
- build
|
- build
|
||||||
- test-webui:
|
version: lts
|
||||||
requires:
|
yarn-run: test:jest
|
||||||
- install
|
|
||||||
|
@ -35,4 +35,7 @@ plugins:
|
|||||||
enabled: true
|
enabled: true
|
||||||
exclude_patterns:
|
exclude_patterns:
|
||||||
- spec/
|
- spec/
|
||||||
- vendor/asset
|
- vendor/asset/
|
||||||
|
|
||||||
|
- app/javascript/mastodon/locales/**/*.json
|
||||||
|
- config/locales/**/*.yml
|
||||||
|
@ -15,6 +15,7 @@ vendor/bundle
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
postgres
|
postgres
|
||||||
|
postgres14
|
||||||
redis
|
redis
|
||||||
elasticsearch
|
elasticsearch
|
||||||
chart
|
chart
|
||||||
|
@ -13,7 +13,7 @@ DB_PORT=5432
|
|||||||
|
|
||||||
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
|
# DATABASE_URL=postgresql://$DATA_DB_USER:$DATA_DB_PASS@$DATA_DB_HOST/gonano
|
||||||
|
|
||||||
# Optional ElasticSearch configuration
|
# Optional Elasticsearch configuration
|
||||||
ES_ENABLED=true
|
ES_ENABLED=true
|
||||||
ES_HOST=$DATA_ELASTIC_HOST
|
ES_HOST=$DATA_ELASTIC_HOST
|
||||||
ES_PORT=9200
|
ES_PORT=9200
|
||||||
@ -202,10 +202,6 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
|
|||||||
# Name of the pam service used for checking if an user can register (pam "account" section is evaluated) (nil (disabled) by default)
|
# 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
|
# 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) :
|
# Optional CAS authentication (cf. omniauth-cas) :
|
||||||
# CAS_ENABLED=true
|
# CAS_ENABLED=true
|
||||||
# CAS_URL=https://sso.myserver.com/
|
# CAS_URL=https://sso.myserver.com/
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
# not demonstrate all available configuration options. Please look at
|
# not demonstrate all available configuration options. Please look at
|
||||||
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
|
||||||
|
|
||||||
|
# Note that this file accepts slightly different syntax depending on whether
|
||||||
|
# you are using `docker-compose` or not. In particular, if you use
|
||||||
|
# `docker-compose`, the value of each declared variable will be taken verbatim,
|
||||||
|
# including surrounding quotes.
|
||||||
|
# See: https://github.com/mastodon/mastodon/issues/16895
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
# ----------
|
# ----------
|
||||||
# This identifies your server and cannot be changed safely later
|
# This identifies your server and cannot be changed safely later
|
||||||
@ -23,11 +29,14 @@ DB_NAME=mastodon_production
|
|||||||
DB_PASS=
|
DB_PASS=
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
|
|
||||||
# ElasticSearch (optional)
|
# Elasticsearch (optional)
|
||||||
# ------------------------
|
# ------------------------
|
||||||
ES_ENABLED=true
|
ES_ENABLED=true
|
||||||
ES_HOST=localhost
|
ES_HOST=localhost
|
||||||
ES_PORT=9200
|
ES_PORT=9200
|
||||||
|
# Authentication for ES (optional)
|
||||||
|
ES_USER=elastic
|
||||||
|
ES_PASS=password
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
# -------
|
# -------
|
||||||
|
22
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
22
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
@ -8,6 +8,17 @@ body:
|
|||||||
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||||
|
|
||||||
Please use a concise and distinct title for the issue.
|
Please use a concise and distinct title for the issue.
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce the problem
|
||||||
|
description: What were you trying to do?
|
||||||
|
value: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Expected behaviour
|
label: Expected behaviour
|
||||||
@ -20,17 +31,6 @@ body:
|
|||||||
description: What happened?
|
description: What happened?
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Steps to reproduce the problem
|
|
||||||
description: What were you trying to do?
|
|
||||||
value: |
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
...
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: Specifications
|
label: Specifications
|
||||||
|
34
.github/workflows/build-image.yml
vendored
Normal file
34
.github/workflows/build-image.yml
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Build container image
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
jobs:
|
||||||
|
build-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
- uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- uses: docker/metadata-action@v3
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: tootsuite/mastodon
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
tags: |
|
||||||
|
type=edge,branch=main
|
||||||
|
type=semver,pattern={{ raw }}
|
||||||
|
- uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
cache-from: type=registry,ref=tootsuite/mastodon:latest
|
||||||
|
cache-to: type=inline
|
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: '2.7'
|
ruby-version: '3.0'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
- name: Check locale file normalization
|
- name: Check locale file normalization
|
||||||
run: bundle exec i18n-tasks check-normalized
|
run: bundle exec i18n-tasks check-normalized
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -40,6 +40,7 @@
|
|||||||
|
|
||||||
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
||||||
/postgres
|
/postgres
|
||||||
|
/postgres14
|
||||||
/redis
|
/redis
|
||||||
/elasticsearch
|
/elasticsearch
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
2.7.4
|
3.0.3
|
||||||
|
77
CHANGELOG.md
77
CHANGELOG.md
@ -3,6 +3,83 @@ Changelog
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [3.4.5] - 2022-01-31
|
||||||
|
### Added
|
||||||
|
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))
|
||||||
|
- Add github workflow to build Docker images ([unasuke](https://github.com/mastodon/mastodon/pull/16973), [Gargron](https://github.com/mastodon/mastodon/pull/16980), [Gargron](https://github.com/mastodon/mastodon/pull/17000))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fix some old migrations failing when skipping releases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17394))
|
||||||
|
- Fix migrations script failing in certain edge cases ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17398))
|
||||||
|
- Fix Docker build ([tribela](https://github.com/mastodon/mastodon/pull/17188))
|
||||||
|
- Fix Ruby 3.0 dependencies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16723))
|
||||||
|
- Fix followers synchronization mechanism ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16510))
|
||||||
|
|
||||||
|
## [3.4.4] - 2021-11-26
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix error when suspending user with an already blocked canonical email ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17036))
|
||||||
|
- Fix overflow of long profile fields in admin UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17010))
|
||||||
|
- Fix confusing error when WebFinger request returns empty document ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16986))
|
||||||
|
- Fix upload of remote media with OpenStack Swift sometimes failing ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16998))
|
||||||
|
- Fix logout link not working in Safari ([noellabo](https://github.com/mastodon/mastodon/pull/16574))
|
||||||
|
- Fix “open” link of media modal not closing modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16524))
|
||||||
|
- Fix replying from modal in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16516))
|
||||||
|
- Fix `mastodon:setup` command crashing in some circumstances ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16976))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix filtering DMs from non-followed users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17042))
|
||||||
|
- Fix handling of recursive toots in WebUI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17041))
|
||||||
|
|
||||||
|
## [3.4.3] - 2021-11-06
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix login being broken due to inaccurately applied backport fix in 3.4.2 ([Gargron](https://github.com/mastodon/mastodon/commit/5c47a18c8df3231aa25c6d1f140a71a7fac9cbf9))
|
||||||
|
|
||||||
|
## [3.4.2] - 2021-11-06
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `configuration` attribute to `GET /api/v1/instance` ([Gargron](https://github.com/mastodon/mastodon/pull/16485))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix handling of back button with modal windows in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16499))
|
||||||
|
- Fix pop-in player when author has long username in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16468))
|
||||||
|
- Fix crash when a status with a playing video gets deleted in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16384))
|
||||||
|
- Fix crash with Microsoft Translate in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16525))
|
||||||
|
- Fix PWA not being usable from alternate domains ([HolgerHuo](https://github.com/mastodon/mastodon/pull/16714))
|
||||||
|
- Fix locale-specific number rounding errors ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16469))
|
||||||
|
- Fix scheduling a status decreasing status count ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16791))
|
||||||
|
- Fix user's canonical email address being blocked when user deletes own account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16503))
|
||||||
|
- Fix not being able to suspend users that already have their canonical e-mail blocked ([Gargron](https://github.com/mastodon/mastodon/pull/16455))
|
||||||
|
- Fix anonymous access to outbox not being cached by the reverse proxy ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16458))
|
||||||
|
- Fix followers synchronization mechanism not working when URI has empty path ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16744))
|
||||||
|
- Fix serialization of counts in REST API when user hides their network ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16418))
|
||||||
|
- Fix inefficiencies in auto-linking code ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16506))
|
||||||
|
- Fix `tootctl self-destruct` not sending delete activities for recently-suspended accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16688))
|
||||||
|
- Fix suspicious sign-in e-mail text being out of date ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16690))
|
||||||
|
- Fix some frameworks being unnecessarily loaded ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16725))
|
||||||
|
- Fix canonical e-mail blocks missing foreign key constraints ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16448))
|
||||||
|
- Fix inconsistent order on account's statuses page in admin UI ([tribela](https://github.com/mastodon/mastodon/pull/16937))
|
||||||
|
- Fix media from blocked domains being redownloaded by `tootctl media refresh` ([tribela](https://github.com/mastodon/mastodon/pull/16914))
|
||||||
|
- Fix `mastodon:setup` generated env-file syntax ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16896))
|
||||||
|
- Fix link previews being incorrectly generated from earlier links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16885))
|
||||||
|
- Fix wrong `to`/`cc` values for remote groups in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16700))
|
||||||
|
- Fix mentions with non-ascii TLDs not being processed ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16689))
|
||||||
|
- Fix authentication failures halfway through a sign-in attempt ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16607), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/16792))
|
||||||
|
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
|
||||||
|
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
|
||||||
|
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
|
||||||
|
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||||
|
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
|
||||||
|
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix user notes not having a length limit ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16942))
|
||||||
|
- Fix revoking a specific session not working ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||||
|
|
||||||
## [3.4.1] - 2021-06-03
|
## [3.4.1] - 2021-06-03
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
22
Dockerfile
22
Dockerfile
@ -2,9 +2,10 @@ FROM ubuntu:20.04 as build-dep
|
|||||||
|
|
||||||
# Use bash for the shell
|
# Use bash for the shell
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
|
|
||||||
# Install Node v14 (LTS)
|
# Install Node v16 (LTS)
|
||||||
ENV NODE_VER="14.17.6"
|
ENV NODE_VER="16.13.2"
|
||||||
RUN ARCH= && \
|
RUN ARCH= && \
|
||||||
dpkgArch="$(dpkg --print-architecture)" && \
|
dpkgArch="$(dpkg --print-architecture)" && \
|
||||||
case "${dpkgArch##*-}" in \
|
case "${dpkgArch##*-}" in \
|
||||||
@ -18,15 +19,15 @@ RUN ARCH= && \
|
|||||||
esac && \
|
esac && \
|
||||||
echo "Etc/UTC" > /etc/localtime && \
|
echo "Etc/UTC" > /etc/localtime && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ca-certificates wget python && \
|
apt-get install -y --no-install-recommends ca-certificates wget python apt-utils && \
|
||||||
cd ~ && \
|
cd ~ && \
|
||||||
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
wget -q https://nodejs.org/download/release/v$NODE_VER/node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||||
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
tar xf node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||||
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
rm node-v$NODE_VER-linux-$ARCH.tar.gz && \
|
||||||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||||
|
|
||||||
# Install Ruby
|
# Install Ruby 3.0
|
||||||
ENV RUBY_VER="2.7.4"
|
ENV RUBY_VER="3.0.3"
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends build-essential \
|
apt-get install -y --no-install-recommends build-essential \
|
||||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||||
@ -45,7 +46,8 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin"
|
||||||
|
|
||||||
RUN npm install -g yarn && \
|
RUN npm install -g npm@latest && \
|
||||||
|
npm install -g yarn && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
apt-get install -y --no-install-recommends git libicu-dev libidn11-dev \
|
||||||
@ -54,8 +56,9 @@ RUN npm install -g yarn && \
|
|||||||
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
COPY Gemfile* package.json yarn.lock /opt/mastodon/
|
||||||
|
|
||||||
RUN cd /opt/mastodon && \
|
RUN cd /opt/mastodon && \
|
||||||
bundle config set deployment 'true' && \
|
bundle config set --local deployment 'true' && \
|
||||||
bundle config set without 'development test' && \
|
bundle config set --local without 'development test' && \
|
||||||
|
bundle config set silence_root_warning true && \
|
||||||
bundle install -j"$(nproc)" && \
|
bundle install -j"$(nproc)" && \
|
||||||
yarn install --pure-lockfile
|
yarn install --pure-lockfile
|
||||||
|
|
||||||
@ -81,11 +84,12 @@ RUN apt-get update && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install mastodon runtime deps
|
# Install mastodon runtime deps
|
||||||
|
RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get -y --no-install-recommends install \
|
apt-get -y --no-install-recommends install \
|
||||||
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
libssl1.1 libpq5 imagemagick ffmpeg libjemalloc2 \
|
||||||
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
libicu66 libprotobuf17 libidn11 libyaml-0-2 \
|
||||||
file ca-certificates tzdata libreadline8 gcc tini && \
|
file ca-certificates tzdata libreadline8 gcc tini apt-utils && \
|
||||||
ln -s /opt/mastodon /mastodon && \
|
ln -s /opt/mastodon /mastodon && \
|
||||||
gem install bundler && \
|
gem install bundler && \
|
||||||
rm -rf /var/cache && \
|
rm -rf /var/cache && \
|
||||||
|
30
FEDERATION.md
Normal file
30
FEDERATION.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
## ActivityPub federation in Mastodon
|
||||||
|
|
||||||
|
Mastodon largely follows the ActivityPub server-to-server specification but it makes uses of some non-standard extensions, some of which are required for interacting with Mastodon at all.
|
||||||
|
|
||||||
|
Supported vocabulary: https://docs.joinmastodon.org/spec/activitypub/
|
||||||
|
|
||||||
|
### Required extensions
|
||||||
|
|
||||||
|
#### Webfinger
|
||||||
|
|
||||||
|
In Mastodon, users are identified by a `username` and `domain` pair (e.g., `Gargron@mastodon.social`).
|
||||||
|
This is used both for discovery and for unambiguously mentioning users across the fediverse. Furthermore, this is part of Mastodon's database design from its very beginnings.
|
||||||
|
|
||||||
|
As a result, Mastodon requires that each ActivityPub actor uniquely maps back to an `acct:` URI that can be resolved via WebFinger.
|
||||||
|
|
||||||
|
More information and examples are available at: https://docs.joinmastodon.org/spec/webfinger/
|
||||||
|
|
||||||
|
#### HTTP Signatures
|
||||||
|
|
||||||
|
In order to authenticate activities, Mastodon relies on HTTP Signatures, signing every `POST` and `GET` request to other ActivityPub implementations on behalf of the user authoring an activity (for `POST` requests) or an actor representing the Mastodon server itself (for most `GET` requests).
|
||||||
|
|
||||||
|
Mastodon requires all `POST` requests to be signed, and MAY require `GET` requests to be signed, depending on the configuration of the Mastodon server.
|
||||||
|
|
||||||
|
More information on HTTP Signatures, as well as examples, can be found here: https://docs.joinmastodon.org/spec/security/#http
|
||||||
|
|
||||||
|
### Optional extensions
|
||||||
|
|
||||||
|
- Linked-Data Signatures: https://docs.joinmastodon.org/spec/security/#ld
|
||||||
|
- Bearcaps: https://docs.joinmastodon.org/spec/bearcaps/
|
||||||
|
- Followers collection synchronization: https://git.activitypub.dev/ActivityPubDev/Fediverse-Enhancement-Proposals/src/branch/main/feps/fep-8fcf.md
|
47
Gemfile
47
Gemfile
@ -4,20 +4,21 @@ source 'https://rubygems.org'
|
|||||||
ruby '>= 2.5.0', '< 3.1.0'
|
ruby '>= 2.5.0', '< 3.1.0'
|
||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
|
gem 'rexml', '~> 3.2'
|
||||||
|
|
||||||
gem 'puma', '~> 5.5'
|
gem 'puma', '~> 5.5'
|
||||||
gem 'rails', '~> 6.1.4'
|
gem 'rails', '~> 6.1.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 1.1'
|
gem 'thor', '~> 1.2'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
|
||||||
gem 'hamlit-rails', '~> 0.2'
|
gem 'hamlit-rails', '~> 0.2'
|
||||||
gem 'pg', '~> 1.2'
|
gem 'pg', '~> 1.3'
|
||||||
gem 'makara', '~> 0.5'
|
gem 'makara', '~> 0.5'
|
||||||
gem 'pghero', '~> 2.8'
|
gem 'pghero', '~> 2.8'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.103', require: false
|
gem 'aws-sdk-s3', '~> 1.111', require: false
|
||||||
gem 'fog-core', '<= 2.1.0'
|
gem 'fog-core', '<= 2.1.0'
|
||||||
gem 'fog-openstack', '~> 0.3', require: false
|
gem 'fog-openstack', '~> 0.3', require: false
|
||||||
gem 'kt-paperclip', '~> 7.0'
|
gem 'kt-paperclip', '~> 7.0'
|
||||||
@ -25,12 +26,12 @@ gem 'blurhash', '~> 0.1'
|
|||||||
|
|
||||||
gem 'active_model_serializers', '~> 0.10'
|
gem 'active_model_serializers', '~> 0.10'
|
||||||
gem 'addressable', '~> 2.8'
|
gem 'addressable', '~> 2.8'
|
||||||
gem 'bootsnap', '~> 1.9.1', require: false
|
gem 'bootsnap', '~> 1.10.2', require: false
|
||||||
gem 'browser'
|
gem 'browser'
|
||||||
gem 'charlock_holmes', '~> 0.7.7'
|
gem 'charlock_holmes', '~> 0.7.7'
|
||||||
gem 'iso-639'
|
gem 'iso-639'
|
||||||
gem 'chewy', '~> 5.2'
|
gem 'chewy', '~> 7.2'
|
||||||
gem 'cld3', '~> 3.4.2'
|
gem 'cld3', '~> 3.4.4'
|
||||||
gem 'devise', '~> 4.8'
|
gem 'devise', '~> 4.8'
|
||||||
gem 'devise-two-factor', '~> 4.0'
|
gem 'devise-two-factor', '~> 4.0'
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
|
|||||||
gem 'color_diff', '~> 0.1'
|
gem 'color_diff', '~> 0.1'
|
||||||
gem 'discard', '~> 1.2'
|
gem 'discard', '~> 1.2'
|
||||||
gem 'doorkeeper', '~> 5.5'
|
gem 'doorkeeper', '~> 5.5'
|
||||||
gem 'ed25519', '~> 1.2'
|
gem 'ed25519', '~> 1.3'
|
||||||
gem 'fast_blank', '~> 1.0'
|
gem 'fast_blank', '~> 1.0'
|
||||||
gem 'fastimage'
|
gem 'fastimage'
|
||||||
gem 'hiredis', '~> 0.6'
|
gem 'hiredis', '~> 0.6'
|
||||||
@ -59,13 +60,12 @@ gem 'httplog', '~> 1.5.0'
|
|||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.2'
|
gem 'kaminari', '~> 1.2'
|
||||||
gem 'link_header', '~> 0.0'
|
gem 'link_header', '~> 0.0'
|
||||||
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
|
gem 'mime-types', '~> 3.4.1', require: 'mime/types/columnar'
|
||||||
gem 'nokogiri', '~> 1.12'
|
gem 'nokogiri', '~> 1.13'
|
||||||
gem 'nsa', '~> 0.2'
|
gem 'nsa', '~> 0.2'
|
||||||
gem 'oj', '~> 3.13'
|
gem 'oj', '~> 3.13'
|
||||||
gem 'ox', '~> 2.14'
|
gem 'ox', '~> 2.14'
|
||||||
gem 'parslet'
|
gem 'parslet'
|
||||||
gem 'parallel', '~> 1.21'
|
|
||||||
gem 'posix-spawn'
|
gem 'posix-spawn'
|
||||||
gem 'pundit', '~> 2.1'
|
gem 'pundit', '~> 2.1'
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
@ -73,19 +73,19 @@ gem 'rack-attack', '~> 6.5'
|
|||||||
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
|
||||||
gem 'rails-i18n', '~> 6.0'
|
gem 'rails-i18n', '~> 6.0'
|
||||||
gem 'rails-settings-cached', '~> 0.6'
|
gem 'rails-settings-cached', '~> 0.6'
|
||||||
gem 'redis', '~> 4.4', require: ['redis', 'redis/connection/hiredis']
|
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
|
||||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||||
gem 'rqrcode', '~> 2.1'
|
gem 'rqrcode', '~> 2.1'
|
||||||
gem 'ruby-progressbar', '~> 1.11'
|
gem 'ruby-progressbar', '~> 1.11'
|
||||||
gem 'sanitize', '~> 6.0'
|
gem 'sanitize', '~> 6.0'
|
||||||
gem 'scenic', '~> 1.5'
|
gem 'scenic', '~> 1.5'
|
||||||
gem 'sidekiq', '~> 6.2'
|
gem 'sidekiq', '~> 6.4'
|
||||||
gem 'sidekiq-scheduler', '~> 3.1'
|
gem 'sidekiq-scheduler', '~> 3.1'
|
||||||
gem 'sidekiq-unique-jobs', '~> 7.1'
|
gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
gem 'sidekiq-bulk', '~>0.2.0'
|
gem 'sidekiq-bulk', '~>0.2.0'
|
||||||
gem 'simple-navigation', '~> 4.3'
|
gem 'simple-navigation', '~> 4.3'
|
||||||
gem 'simple_form', '~> 5.1'
|
gem 'simple_form', '~> 5.1'
|
||||||
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
|
gem 'sprockets-rails', '~> 3.4', require: 'sprockets/railtie'
|
||||||
gem 'stoplight', '~> 2.2.1'
|
gem 'stoplight', '~> 2.2.1'
|
||||||
gem 'strong_migrations', '~> 0.7'
|
gem 'strong_migrations', '~> 0.7'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
@ -96,11 +96,11 @@ gem 'webpush', '~> 0.3'
|
|||||||
gem 'webauthn', '~> 3.0.0.alpha1'
|
gem 'webauthn', '~> 3.0.0.alpha1'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.1'
|
gem 'json-ld-preloaded', '~> 3.2'
|
||||||
gem 'rdf-normalize', '~> 0.4'
|
gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'fabrication', '~> 2.22'
|
gem 'fabrication', '~> 2.24'
|
||||||
gem 'fuubar', '~> 2.5'
|
gem 'fuubar', '~> 2.5'
|
||||||
gem 'i18n-tasks', '~> 0.9', require: false
|
gem 'i18n-tasks', '~> 0.9', require: false
|
||||||
gem 'pry-byebug', '~> 3.9'
|
gem 'pry-byebug', '~> 3.9'
|
||||||
@ -113,7 +113,7 @@ group :production, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.35'
|
gem 'capybara', '~> 3.36'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.19'
|
gem 'faker', '~> 2.19'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
@ -121,8 +121,7 @@ group :test do
|
|||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.21', require: false
|
gem 'simplecov', '~> 0.21', require: false
|
||||||
gem 'webmock', '~> 3.14'
|
gem 'webmock', '~> 3.14'
|
||||||
gem 'parallel_tests', '~> 3.7'
|
gem 'rspec_junit_formatter', '~> 0.5'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
@ -130,13 +129,13 @@ group :development do
|
|||||||
gem 'annotate', '~> 3.1'
|
gem 'annotate', '~> 3.1'
|
||||||
gem 'better_errors', '~> 2.9'
|
gem 'better_errors', '~> 2.9'
|
||||||
gem 'binding_of_caller', '~> 1.0'
|
gem 'binding_of_caller', '~> 1.0'
|
||||||
gem 'bullet', '~> 6.1'
|
gem 'bullet', '~> 7.0'
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 2.0'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 1.22', require: false
|
gem 'rubocop', '~> 1.25', require: false
|
||||||
gem 'rubocop-rails', '~> 2.12', require: false
|
gem 'rubocop-rails', '~> 2.13', require: false
|
||||||
gem 'brakeman', '~> 5.1', require: false
|
gem 'brakeman', '~> 5.2', require: false
|
||||||
gem 'bundler-audit', '~> 0.9', require: false
|
gem 'bundler-audit', '~> 0.9', require: false
|
||||||
|
|
||||||
gem 'capistrano', '~> 3.16'
|
gem 'capistrano', '~> 3.16'
|
||||||
|
391
Gemfile.lock
391
Gemfile.lock
@ -1,66 +1,66 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (6.1.4.1)
|
actioncable (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (6.1.4.1)
|
actionmailbox (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
actionmailer (6.1.4.1)
|
actionmailer (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (6.1.4.1)
|
actionpack (6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
rack (~> 2.0, >= 2.0.9)
|
rack (~> 2.0, >= 2.0.9)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (6.1.4.1)
|
actiontext (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (6.1.4.1)
|
actionview (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_model_serializers (0.10.12)
|
active_model_serializers (0.10.13)
|
||||||
actionpack (>= 4.1, < 6.2)
|
actionpack (>= 4.1, < 7.1)
|
||||||
activemodel (>= 4.1, < 6.2)
|
activemodel (>= 4.1, < 7.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (6.1.4.1)
|
activejob (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (6.1.4.1)
|
activemodel (6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
activerecord (6.1.4.1)
|
activerecord (6.1.4.4)
|
||||||
activemodel (= 6.1.4.1)
|
activemodel (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
activestorage (6.1.4.1)
|
activestorage (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
marcel (~> 1.0.0)
|
marcel (~> 1.0.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (6.1.4.1)
|
activesupport (6.1.4.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
@ -79,17 +79,17 @@ GEM
|
|||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.2.0)
|
||||||
aws-partitions (1.503.0)
|
aws-partitions (1.549.0)
|
||||||
aws-sdk-core (3.121.0)
|
aws-sdk-core (3.125.5)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.525.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.48.0)
|
aws-sdk-kms (1.53.0)
|
||||||
aws-sdk-core (~> 3, >= 3.120.0)
|
aws-sdk-core (~> 3, >= 3.125.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.103.0)
|
aws-sdk-s3 (1.111.3)
|
||||||
aws-sdk-core (~> 3, >= 3.120.0)
|
aws-sdk-core (~> 3, >= 3.125.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.4)
|
aws-sigv4 (~> 1.4)
|
||||||
aws-sigv4 (1.4.0)
|
aws-sigv4 (1.4.0)
|
||||||
@ -104,15 +104,15 @@ GEM
|
|||||||
debug_inspector (>= 0.0.1)
|
debug_inspector (>= 0.0.1)
|
||||||
blurhash (0.1.5)
|
blurhash (0.1.5)
|
||||||
ffi (~> 1.14)
|
ffi (~> 1.14)
|
||||||
bootsnap (1.9.1)
|
bootsnap (1.10.2)
|
||||||
msgpack (~> 1.0)
|
msgpack (~> 1.2)
|
||||||
brakeman (5.1.1)
|
brakeman (5.2.0)
|
||||||
browser (4.2.0)
|
browser (4.2.0)
|
||||||
brpoplpush-redis_script (0.1.2)
|
brpoplpush-redis_script (0.1.2)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
redis (>= 1.0, <= 5.0)
|
redis (>= 1.0, <= 5.0)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (6.1.5)
|
bullet (7.0.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
bundler-audit (0.9.0.1)
|
bundler-audit (0.9.0.1)
|
||||||
@ -134,8 +134,9 @@ GEM
|
|||||||
sshkit (~> 1.3)
|
sshkit (~> 1.3)
|
||||||
capistrano-yarn (2.0.2)
|
capistrano-yarn (2.0.2)
|
||||||
capistrano (~> 3.0)
|
capistrano (~> 3.0)
|
||||||
capybara (3.35.3)
|
capybara (3.36.0)
|
||||||
addressable
|
addressable
|
||||||
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
@ -146,12 +147,12 @@ GEM
|
|||||||
activesupport
|
activesupport
|
||||||
cbor (0.5.9.6)
|
cbor (0.5.9.6)
|
||||||
charlock_holmes (0.7.7)
|
charlock_holmes (0.7.7)
|
||||||
chewy (5.2.0)
|
chewy (7.2.3)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
elasticsearch (>= 2.0.0)
|
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||||
elasticsearch-dsl
|
elasticsearch-dsl
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cld3 (3.4.2)
|
cld3 (3.4.4)
|
||||||
ffi (>= 1.1.0, < 1.16.0)
|
ffi (>= 1.1.0, < 1.16.0)
|
||||||
climate_control (0.2.0)
|
climate_control (0.2.0)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
@ -167,7 +168,7 @@ GEM
|
|||||||
css_parser (1.7.1)
|
css_parser (1.7.1)
|
||||||
addressable
|
addressable
|
||||||
debug_inspector (1.0.0)
|
debug_inspector (1.0.0)
|
||||||
devise (4.8.0)
|
devise (4.8.1)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@ -183,26 +184,26 @@ GEM
|
|||||||
devise (>= 4.0.0)
|
devise (>= 4.0.0)
|
||||||
rpam2 (~> 4.0)
|
rpam2 (~> 4.0)
|
||||||
diff-lcs (1.4.4)
|
diff-lcs (1.4.4)
|
||||||
discard (1.2.0)
|
discard (1.2.1)
|
||||||
activerecord (>= 4.2, < 7)
|
activerecord (>= 4.2, < 8)
|
||||||
docile (1.3.4)
|
docile (1.3.4)
|
||||||
domain_name (0.5.20190701)
|
domain_name (0.5.20190701)
|
||||||
unf (>= 0.0.5, < 1.0.0)
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
doorkeeper (5.5.3)
|
doorkeeper (5.5.4)
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (2.7.6)
|
dotenv (2.7.6)
|
||||||
dotenv-rails (2.7.6)
|
dotenv-rails (2.7.6)
|
||||||
dotenv (= 2.7.6)
|
dotenv (= 2.7.6)
|
||||||
railties (>= 3.2)
|
railties (>= 3.2)
|
||||||
e2mmap (0.1.0)
|
e2mmap (0.1.0)
|
||||||
ed25519 (1.2.4)
|
ed25519 (1.3.0)
|
||||||
elasticsearch (7.10.1)
|
elasticsearch (7.13.3)
|
||||||
elasticsearch-api (= 7.10.1)
|
elasticsearch-api (= 7.13.3)
|
||||||
elasticsearch-transport (= 7.10.1)
|
elasticsearch-transport (= 7.13.3)
|
||||||
elasticsearch-api (7.10.1)
|
elasticsearch-api (7.13.3)
|
||||||
multi_json
|
multi_json
|
||||||
elasticsearch-dsl (0.1.9)
|
elasticsearch-dsl (0.1.10)
|
||||||
elasticsearch-transport (7.10.1)
|
elasticsearch-transport (7.13.3)
|
||||||
faraday (~> 1)
|
faraday (~> 1)
|
||||||
multi_json
|
multi_json
|
||||||
encryptor (3.0.0)
|
encryptor (3.0.0)
|
||||||
@ -210,17 +211,31 @@ GEM
|
|||||||
et-orbi (1.2.4)
|
et-orbi (1.2.4)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.22.0)
|
fabrication (2.24.0)
|
||||||
faker (2.19.0)
|
faker (2.19.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.3.0)
|
faraday (1.8.0)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0.1)
|
||||||
faraday-net_http (~> 1.0)
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.1)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
ruby2_keywords
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.1)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.2.5)
|
fastimage (2.2.6)
|
||||||
ffi (1.15.4)
|
ffi (1.15.5)
|
||||||
ffi-compiler (1.0.1)
|
ffi-compiler (1.0.1)
|
||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
@ -243,7 +258,7 @@ GEM
|
|||||||
fuubar (2.5.1)
|
fuubar (2.5.1)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.5.2)
|
globalid (1.0.0)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 5.0)
|
||||||
hamlit (2.13.0)
|
hamlit (2.13.0)
|
||||||
temple (>= 0.8.2)
|
temple (>= 0.8.2)
|
||||||
@ -254,15 +269,13 @@ GEM
|
|||||||
activesupport (>= 4.0.1)
|
activesupport (>= 4.0.1)
|
||||||
hamlit (>= 1.2.0)
|
hamlit (>= 1.2.0)
|
||||||
railties (>= 4.0.1)
|
railties (>= 4.0.1)
|
||||||
hamster (3.0.0)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashie (4.1.0)
|
hashie (4.1.0)
|
||||||
highline (2.0.3)
|
highline (2.0.3)
|
||||||
hiredis (0.6.3)
|
hiredis (0.6.3)
|
||||||
hkdf (0.3.0)
|
hkdf (0.3.0)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (5.0.2)
|
http (5.0.4)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
http-cookie (~> 1.0)
|
http-cookie (~> 1.0)
|
||||||
http-form_data (~> 2.2)
|
http-form_data (~> 2.2)
|
||||||
@ -274,9 +287,9 @@ GEM
|
|||||||
httplog (1.5.0)
|
httplog (1.5.0)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
rainbow (>= 2.0.0)
|
rainbow (>= 2.0.0)
|
||||||
i18n (1.8.10)
|
i18n (1.8.11)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.34)
|
i18n-tasks (0.9.37)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
erubi
|
erubi
|
||||||
@ -286,37 +299,37 @@ GEM
|
|||||||
rails-i18n
|
rails-i18n
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
terminal-table (>= 1.5.1)
|
terminal-table (>= 1.5.1)
|
||||||
idn-ruby (0.1.2)
|
idn-ruby (0.1.4)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
iso-639 (0.3.5)
|
iso-639 (0.3.5)
|
||||||
jmespath (1.4.0)
|
jmespath (1.5.0)
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
json-canonicalization (0.2.1)
|
json-canonicalization (0.3.0)
|
||||||
json-ld (3.1.10)
|
json-ld (3.2.0)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 0.2)
|
json-canonicalization (~> 0.3)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
multi_json (~> 1.14)
|
multi_json (~> 1.15)
|
||||||
rack (~> 2.0)
|
rack (~> 2.2)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
json-ld-preloaded (3.1.6)
|
json-ld-preloaded (3.2.0)
|
||||||
json-ld (~> 3.1)
|
json-ld (~> 3.2)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.2.2)
|
jwt (2.2.2)
|
||||||
kaminari (1.2.1)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.1)
|
kaminari-actionview (= 1.2.2)
|
||||||
kaminari-activerecord (= 1.2.1)
|
kaminari-activerecord (= 1.2.2)
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-actionview (1.2.1)
|
kaminari-actionview (1.2.2)
|
||||||
actionview
|
actionview
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-activerecord (1.2.1)
|
kaminari-activerecord (1.2.2)
|
||||||
activerecord
|
activerecord
|
||||||
kaminari-core (= 1.2.1)
|
kaminari-core (= 1.2.2)
|
||||||
kaminari-core (1.2.1)
|
kaminari-core (1.2.2)
|
||||||
kt-paperclip (7.0.0)
|
kt-paperclip (7.0.1)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
marcel (~> 1.0.1)
|
marcel (~> 1.0.1)
|
||||||
@ -326,10 +339,11 @@ GEM
|
|||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
letter_opener (1.7.0)
|
letter_opener (1.7.0)
|
||||||
launchy (~> 2.2)
|
launchy (~> 2.2)
|
||||||
letter_opener_web (1.4.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 3.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.0)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 3.2)
|
railties (>= 5.2)
|
||||||
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
@ -339,7 +353,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.12.0)
|
loofah (2.13.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
@ -349,18 +363,19 @@ GEM
|
|||||||
marcel (1.0.1)
|
marcel (1.0.1)
|
||||||
mario-redis-lock (1.2.1)
|
mario-redis-lock (1.2.1)
|
||||||
redis (>= 3.0.5)
|
redis (>= 3.0.5)
|
||||||
|
matrix (0.4.2)
|
||||||
memory_profiler (1.0.0)
|
memory_profiler (1.0.0)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
microformats (4.3.1)
|
microformats (4.3.1)
|
||||||
json (~> 2.2)
|
json (~> 2.2)
|
||||||
nokogiri (~> 1.10)
|
nokogiri (~> 1.10)
|
||||||
mime-types (3.3.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2020.0512)
|
mime-types-data (3.2021.1115)
|
||||||
mini_mime (1.1.1)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.6.1)
|
mini_portile2 (2.7.1)
|
||||||
minitest (5.14.4)
|
minitest (5.15.0)
|
||||||
msgpack (1.4.2)
|
msgpack (1.4.4)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.1.1)
|
multipart-post (2.1.1)
|
||||||
net-ldap (0.17.0)
|
net-ldap (0.17.0)
|
||||||
@ -368,15 +383,15 @@ GEM
|
|||||||
net-ssh (>= 2.6.5, < 7.0.0)
|
net-ssh (>= 2.6.5, < 7.0.0)
|
||||||
net-ssh (6.1.0)
|
net-ssh (6.1.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.12.5)
|
nokogiri (1.13.1)
|
||||||
mini_portile2 (~> 2.6.1)
|
mini_portile2 (~> 2.7.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.2.8)
|
nsa (0.2.8)
|
||||||
activesupport (>= 4.2, < 7)
|
activesupport (>= 4.2, < 7)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sidekiq (>= 3.5)
|
sidekiq (>= 3.5)
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.13.8)
|
oj (3.13.11)
|
||||||
omniauth (1.9.1)
|
omniauth (1.9.1)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 1.6.2, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
@ -393,19 +408,17 @@ GEM
|
|||||||
openssl (2.2.0)
|
openssl (2.2.0)
|
||||||
openssl-signature_algorithm (0.4.0)
|
openssl-signature_algorithm (0.4.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.5)
|
ox (2.14.6)
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parallel_tests (3.7.3)
|
parser (3.1.0.0)
|
||||||
parallel
|
|
||||||
parser (3.0.2.0)
|
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.2.3)
|
pg (1.3.0)
|
||||||
pghero (2.8.1)
|
pghero (2.8.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.6)
|
pkg-config (1.4.7)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.14.2)
|
premailer (1.14.2)
|
||||||
addressable
|
addressable
|
||||||
@ -424,12 +437,12 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (4.0.6)
|
public_suffix (4.0.6)
|
||||||
puma (5.5.0)
|
puma (5.5.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.1)
|
pundit (2.1.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.5.2)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
rack-attack (6.5.0)
|
rack-attack (6.5.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
@ -439,20 +452,20 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (6.1.4.1)
|
rails (6.1.4.4)
|
||||||
actioncable (= 6.1.4.1)
|
actioncable (= 6.1.4.4)
|
||||||
actionmailbox (= 6.1.4.1)
|
actionmailbox (= 6.1.4.4)
|
||||||
actionmailer (= 6.1.4.1)
|
actionmailer (= 6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
actiontext (= 6.1.4.1)
|
actiontext (= 6.1.4.4)
|
||||||
actionview (= 6.1.4.1)
|
actionview (= 6.1.4.4)
|
||||||
activejob (= 6.1.4.1)
|
activejob (= 6.1.4.4)
|
||||||
activemodel (= 6.1.4.1)
|
activemodel (= 6.1.4.4)
|
||||||
activerecord (= 6.1.4.1)
|
activerecord (= 6.1.4.4)
|
||||||
activestorage (= 6.1.4.1)
|
activestorage (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 6.1.4.1)
|
railties (= 6.1.4.4)
|
||||||
sprockets-rails (>= 2.0.0)
|
sprockets-rails (>= 2.0.0)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
@ -468,23 +481,22 @@ GEM
|
|||||||
railties (>= 6.0.0, < 7)
|
railties (>= 6.0.0, < 7)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (6.1.4.1)
|
railties (6.1.4.4)
|
||||||
actionpack (= 6.1.4.1)
|
actionpack (= 6.1.4.4)
|
||||||
activesupport (= 6.1.4.1)
|
activesupport (= 6.1.4.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 0.13)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
rainbow (3.0.0)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.0.6)
|
||||||
rdf (3.1.15)
|
rdf (3.2.3)
|
||||||
hamster (~> 3.0)
|
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.5.0)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.2)
|
||||||
redis (4.4.0)
|
redis (4.5.1)
|
||||||
redis-namespace (1.8.1)
|
redis-namespace (1.8.1)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
regexp_parser (2.1.1)
|
regexp_parser (2.2.0)
|
||||||
request_store (1.5.0)
|
request_store (1.5.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.0.1)
|
responders (3.0.1)
|
||||||
@ -516,21 +528,21 @@ GEM
|
|||||||
rspec-sidekiq (3.1.0)
|
rspec-sidekiq (3.1.0)
|
||||||
rspec-core (~> 3.0, >= 3.0.0)
|
rspec-core (~> 3.0, >= 3.0.0)
|
||||||
sidekiq (>= 2.4.0)
|
sidekiq (>= 2.4.0)
|
||||||
rspec-support (3.10.2)
|
rspec-support (3.10.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.5.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (1.22.1)
|
rubocop (1.25.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.1.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 1.12.0, < 2.0)
|
rubocop-ast (>= 1.15.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.12.0)
|
rubocop-ast (1.15.1)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.0.1.1)
|
||||||
rubocop-rails (2.12.2)
|
rubocop-rails (2.13.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
@ -538,7 +550,7 @@ GEM
|
|||||||
ruby-saml (1.13.0)
|
ruby-saml (1.13.0)
|
||||||
nokogiri (>= 1.10.5)
|
nokogiri (>= 1.10.5)
|
||||||
rexml
|
rexml
|
||||||
ruby2_keywords (0.0.4)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.7.0)
|
rufus-scheduler (3.7.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
@ -546,12 +558,12 @@ GEM
|
|||||||
sanitize (6.0.0)
|
sanitize (6.0.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.5.4)
|
scenic (1.5.5)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sidekiq (6.2.2)
|
sidekiq (6.4.0)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
@ -564,7 +576,7 @@ GEM
|
|||||||
sidekiq (>= 3)
|
sidekiq (>= 3)
|
||||||
thwait
|
thwait
|
||||||
tilt (>= 1.4.0)
|
tilt (>= 1.4.0)
|
||||||
sidekiq-unique-jobs (7.1.7)
|
sidekiq-unique-jobs (7.1.12)
|
||||||
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.5)
|
concurrent-ruby (~> 1.0, >= 1.0.5)
|
||||||
sidekiq (>= 5.0, < 8.0)
|
sidekiq (>= 5.0, < 8.0)
|
||||||
@ -583,9 +595,9 @@ GEM
|
|||||||
sprockets (3.7.2)
|
sprockets (3.7.2)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
rack (> 1, < 3)
|
rack (> 1, < 3)
|
||||||
sprockets-rails (3.2.2)
|
sprockets-rails (3.4.2)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 5.2)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 5.2)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sshkit (1.21.2)
|
sshkit (1.21.2)
|
||||||
net-scp (>= 1.1.2)
|
net-scp (>= 1.1.2)
|
||||||
@ -593,14 +605,14 @@ GEM
|
|||||||
stackprof (0.2.17)
|
stackprof (0.2.17)
|
||||||
statsd-ruby (1.5.0)
|
statsd-ruby (1.5.0)
|
||||||
stoplight (2.2.1)
|
stoplight (2.2.1)
|
||||||
strong_migrations (0.7.8)
|
strong_migrations (0.7.9)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
temple (0.8.2)
|
temple (0.8.2)
|
||||||
terminal-table (3.0.0)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (1.1.0)
|
thor (1.2.1)
|
||||||
thwait (0.2.0)
|
thwait (0.2.0)
|
||||||
e2mmap
|
e2mmap
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
@ -622,12 +634,12 @@ GEM
|
|||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (2.0.4)
|
tzinfo (2.0.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2021.3)
|
tzinfo-data (1.2021.5)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.7.7)
|
unf_ext (0.0.8)
|
||||||
unicode-display_width (1.8.0)
|
unicode-display_width (2.1.0)
|
||||||
uniform_notifier (1.14.2)
|
uniform_notifier (1.14.2)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
@ -660,7 +672,7 @@ GEM
|
|||||||
xorcist (1.1.2)
|
xorcist (1.1.2)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.5.3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@ -670,23 +682,23 @@ DEPENDENCIES
|
|||||||
active_record_query_trace (~> 1.8)
|
active_record_query_trace (~> 1.8)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.103)
|
aws-sdk-s3 (~> 1.111)
|
||||||
better_errors (~> 2.9)
|
better_errors (~> 2.9)
|
||||||
binding_of_caller (~> 1.0)
|
binding_of_caller (~> 1.0)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.9.1)
|
bootsnap (~> 1.10.2)
|
||||||
brakeman (~> 5.1)
|
brakeman (~> 5.2)
|
||||||
browser
|
browser
|
||||||
bullet (~> 6.1)
|
bullet (~> 7.0)
|
||||||
bundler-audit (~> 0.9)
|
bundler-audit (~> 0.9)
|
||||||
capistrano (~> 3.16)
|
capistrano (~> 3.16)
|
||||||
capistrano-rails (~> 1.6)
|
capistrano-rails (~> 1.6)
|
||||||
capistrano-rbenv (~> 2.2)
|
capistrano-rbenv (~> 2.2)
|
||||||
capistrano-yarn (~> 2.0)
|
capistrano-yarn (~> 2.0)
|
||||||
capybara (~> 3.35)
|
capybara (~> 3.36)
|
||||||
charlock_holmes (~> 0.7.7)
|
charlock_holmes (~> 0.7.7)
|
||||||
chewy (~> 5.2)
|
chewy (~> 7.2)
|
||||||
cld3 (~> 3.4.2)
|
cld3 (~> 3.4.4)
|
||||||
climate_control (~> 0.2)
|
climate_control (~> 0.2)
|
||||||
color_diff (~> 0.1)
|
color_diff (~> 0.1)
|
||||||
concurrent-ruby
|
concurrent-ruby
|
||||||
@ -697,8 +709,8 @@ DEPENDENCIES
|
|||||||
discard (~> 1.2)
|
discard (~> 1.2)
|
||||||
doorkeeper (~> 5.5)
|
doorkeeper (~> 5.5)
|
||||||
dotenv-rails (~> 2.7)
|
dotenv-rails (~> 2.7)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.3)
|
||||||
fabrication (~> 2.22)
|
fabrication (~> 2.24)
|
||||||
faker (~> 2.19)
|
faker (~> 2.19)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
@ -715,20 +727,20 @@ DEPENDENCIES
|
|||||||
idn-ruby
|
idn-ruby
|
||||||
iso-639
|
iso-639
|
||||||
json-ld
|
json-ld
|
||||||
json-ld-preloaded (~> 3.1)
|
json-ld-preloaded (~> 3.2)
|
||||||
kaminari (~> 1.2)
|
kaminari (~> 1.2)
|
||||||
kt-paperclip (~> 7.0)
|
kt-paperclip (~> 7.0)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
letter_opener_web (~> 1.4)
|
letter_opener_web (~> 2.0)
|
||||||
link_header (~> 0.0)
|
link_header (~> 0.0)
|
||||||
lograge (~> 0.11)
|
lograge (~> 0.11)
|
||||||
makara (~> 0.5)
|
makara (~> 0.5)
|
||||||
mario-redis-lock (~> 1.2)
|
mario-redis-lock (~> 1.2)
|
||||||
memory_profiler
|
memory_profiler
|
||||||
microformats (~> 4.2)
|
microformats (~> 4.2)
|
||||||
mime-types (~> 3.3.1)
|
mime-types (~> 3.4.1)
|
||||||
net-ldap (~> 0.17)
|
net-ldap (~> 0.17)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.13)
|
||||||
nsa (~> 0.2)
|
nsa (~> 0.2)
|
||||||
oj (~> 3.13)
|
oj (~> 3.13)
|
||||||
omniauth (~> 1.9)
|
omniauth (~> 1.9)
|
||||||
@ -736,10 +748,8 @@ DEPENDENCIES
|
|||||||
omniauth-rails_csrf_protection (~> 0.1)
|
omniauth-rails_csrf_protection (~> 0.1)
|
||||||
omniauth-saml (~> 1.10)
|
omniauth-saml (~> 1.10)
|
||||||
ox (~> 2.14)
|
ox (~> 2.14)
|
||||||
parallel (~> 1.21)
|
|
||||||
parallel_tests (~> 3.7)
|
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.2)
|
pg (~> 1.3)
|
||||||
pghero (~> 2.8)
|
pghero (~> 2.8)
|
||||||
pkg-config (~> 1.4)
|
pkg-config (~> 1.4)
|
||||||
posix-spawn
|
posix-spawn
|
||||||
@ -756,19 +766,20 @@ DEPENDENCIES
|
|||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 6.0)
|
rails-i18n (~> 6.0)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
rdf-normalize (~> 0.4)
|
rdf-normalize (~> 0.5)
|
||||||
redis (~> 4.4)
|
redis (~> 4.5)
|
||||||
redis-namespace (~> 1.8)
|
redis-namespace (~> 1.8)
|
||||||
|
rexml (~> 3.2)
|
||||||
rqrcode (~> 2.1)
|
rqrcode (~> 2.1)
|
||||||
rspec-rails (~> 5.0)
|
rspec-rails (~> 5.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.5)
|
||||||
rubocop (~> 1.22)
|
rubocop (~> 1.25)
|
||||||
rubocop-rails (~> 2.12)
|
rubocop-rails (~> 2.13)
|
||||||
ruby-progressbar (~> 1.11)
|
ruby-progressbar (~> 1.11)
|
||||||
sanitize (~> 6.0)
|
sanitize (~> 6.0)
|
||||||
scenic (~> 1.5)
|
scenic (~> 1.5)
|
||||||
sidekiq (~> 6.2)
|
sidekiq (~> 6.4)
|
||||||
sidekiq-bulk (~> 0.2.0)
|
sidekiq-bulk (~> 0.2.0)
|
||||||
sidekiq-scheduler (~> 3.1)
|
sidekiq-scheduler (~> 3.1)
|
||||||
sidekiq-unique-jobs (~> 7.1)
|
sidekiq-unique-jobs (~> 7.1)
|
||||||
@ -776,11 +787,11 @@ DEPENDENCIES
|
|||||||
simple_form (~> 5.1)
|
simple_form (~> 5.1)
|
||||||
simplecov (~> 0.21)
|
simplecov (~> 0.21)
|
||||||
sprockets (~> 3.7.2)
|
sprockets (~> 3.7.2)
|
||||||
sprockets-rails (~> 3.2)
|
sprockets-rails (~> 3.4)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 2.2.1)
|
stoplight (~> 2.2.1)
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 1.1)
|
thor (~> 1.2)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
twitter-text (~> 3.1.0)
|
twitter-text (~> 3.1.0)
|
||||||
tzinfo-data (~> 1.2021)
|
tzinfo-data (~> 1.2021)
|
||||||
|
36
README.md
36
README.md
@ -37,50 +37,56 @@ Click below to **learn more** in a video:
|
|||||||
|
|
||||||
<img src="https://docs.joinmastodon.org/elephant.svg" align="right" width="30%" />
|
<img src="https://docs.joinmastodon.org/elephant.svg" align="right" width="30%" />
|
||||||
|
|
||||||
**No vendor lock-in: Fully interoperable with any conforming platform**
|
### No vendor lock-in: Fully interoperable with any conforming platform
|
||||||
|
|
||||||
It doesn't have to be Mastodon, whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
|
It doesn't have to be Mastodon; whatever implements ActivityPub is part of the social network! [Learn more](https://blog.joinmastodon.org/2018/06/why-activitypub-is-the-future/)
|
||||||
|
|
||||||
**Real-time, chronological timeline updates**
|
### Real-time, chronological timeline updates
|
||||||
|
|
||||||
See the updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
|
Updates of people you're following appear in real-time in the UI via WebSockets. There's a firehose view as well!
|
||||||
|
|
||||||
**Media attachments like images and short videos**
|
### Media attachments like images and short videos
|
||||||
|
|
||||||
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos are looped - like vines!
|
Upload and view images and WebM/MP4 videos attached to the updates. Videos with no audio track are treated like GIFs; normal videos loop continuously!
|
||||||
|
|
||||||
**Safety and moderation tools**
|
### Safety and moderation tools
|
||||||
|
|
||||||
Private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
|
Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/)
|
||||||
|
|
||||||
**OAuth2 and a straightforward REST API**
|
### OAuth2 and a straightforward REST API
|
||||||
|
|
||||||
Mastodon acts as an OAuth2 provider so 3rd party apps can use the REST and Streaming APIs, resulting in a rich app ecosystem with a lot of choices!
|
Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Streaming APIs. This results in a rich app ecosystem with a lot of choices!
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
**Tech stack:**
|
### Tech stack:
|
||||||
|
|
||||||
- **Ruby on Rails** powers the REST API and other web pages
|
- **Ruby on Rails** powers the REST API and other web pages
|
||||||
- **React.js** and Redux are used for the dynamic parts of the interface
|
- **React.js** and Redux are used for the dynamic parts of the interface
|
||||||
- **Node.js** powers the streaming API
|
- **Node.js** powers the streaming API
|
||||||
|
|
||||||
**Requirements:**
|
### Requirements:
|
||||||
|
|
||||||
- **PostgreSQL** 9.5+
|
- **PostgreSQL** 9.5+
|
||||||
- **Redis** 4+
|
- **Redis** 4+
|
||||||
- **Ruby** 2.5+
|
- **Ruby** 2.5+
|
||||||
- **Node.js** 12+
|
- **Node.js** 12+
|
||||||
|
|
||||||
The repository includes deployment configurations for **Docker and docker-compose**, but also a few specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**stand-alone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
|
The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation.
|
||||||
|
|
||||||
A **Vagrant** configuration is included for development purposes.
|
A **Vagrant** configuration is included for development purposes. To use it, complete following steps:
|
||||||
|
|
||||||
|
- Install Vagrant and Virtualbox
|
||||||
|
- Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater`
|
||||||
|
- Run `vagrant up`
|
||||||
|
- Run `vagrant ssh -c "cd /vagrant && foreman start"`
|
||||||
|
- Open `http://mastodon.local` in your browser
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Mastodon is **free, open-source software** licensed under **AGPLv3**.
|
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 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).
|
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.libera.chat
|
**IRC channel**: #mastodon on irc.libera.chat
|
||||||
|
|
||||||
|
12
Vagrantfile
vendored
12
Vagrantfile
vendored
@ -45,16 +45,8 @@ sudo apt-get install \
|
|||||||
# Install rvm
|
# Install rvm
|
||||||
read RUBY_VERSION < .ruby-version
|
read RUBY_VERSION < .ruby-version
|
||||||
|
|
||||||
gpg_command="gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB"
|
curl -sSL https://rvm.io/mpapis.asc | gpg --import
|
||||||
$($gpg_command)
|
curl -sSL https://rvm.io/pkuczynski.asc | gpg --import
|
||||||
if [ $? -ne 0 ];then
|
|
||||||
echo "GPG command failed, This prevented RVM from installing."
|
|
||||||
echo "Retrying once..." && $($gpg_command)
|
|
||||||
if [ $? -ne 0 ];then
|
|
||||||
echo "GPG failed for the second time, please ensure network connectivity."
|
|
||||||
echo "Exiting..." && exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
curl -sSL https://raw.githubusercontent.com/rvm/rvm/stable/binscripts/rvm-installer | bash -s stable --ruby=$RUBY_VERSION
|
||||||
source /home/vagrant/.rvm/scripts/rvm
|
source /home/vagrant/.rvm/scripts/rvm
|
||||||
|
@ -23,7 +23,8 @@ class AccountsIndex < Chewy::Index
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do
|
index_scope ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? }
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :id, type: 'long'
|
field :id, type: 'long'
|
||||||
|
|
||||||
@ -39,5 +40,4 @@ class AccountsIndex < Chewy::Index
|
|||||||
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
field :followers_count, type: 'long', value: ->(account) { account.followers.local.count }
|
||||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -31,7 +31,8 @@ class StatusesIndex < Chewy::Index
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
define_type ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll) do
|
index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
|
||||||
|
|
||||||
crutch :mentions do |collection|
|
crutch :mentions do |collection|
|
||||||
data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).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) }
|
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
|
||||||
@ -62,5 +63,4 @@ class StatusesIndex < Chewy::Index
|
|||||||
|
|
||||||
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -23,15 +23,15 @@ class TagsIndex < Chewy::Index
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do
|
index_scope ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? }
|
||||||
|
|
||||||
root date_detection: false do
|
root date_detection: false do
|
||||||
field :name, type: 'text', analyzer: 'content' do
|
field :name, type: 'text', analyzer: 'content' do
|
||||||
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content'
|
||||||
end
|
end
|
||||||
|
|
||||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||||
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } }
|
field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day.accounts } }
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -28,7 +28,7 @@ class AccountsController < ApplicationController
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
@pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses?
|
@pinned_statuses = cached_filtered_status_pins if show_pinned_statuses?
|
||||||
@statuses = cached_filtered_status_page
|
@statuses = cached_filtered_status_page
|
||||||
@rss_url = rss_url
|
@rss_url = rss_url
|
||||||
|
|
||||||
@ -64,6 +64,10 @@ class AccountsController < ApplicationController
|
|||||||
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
[replies_requested?, media_requested?, tag_requested?, params[:max_id].present?, params[:min_id].present?].none?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filtered_pinned_statuses
|
||||||
|
@account.pinned_statuses.where(visibility: [:public, :unlisted])
|
||||||
|
end
|
||||||
|
|
||||||
def filtered_statuses
|
def filtered_statuses
|
||||||
default_statuses.tap do |statuses|
|
default_statuses.tap do |statuses|
|
||||||
statuses.merge!(hashtag_scope) if tag_requested?
|
statuses.merge!(hashtag_scope) if tag_requested?
|
||||||
@ -142,6 +146,13 @@ class AccountsController < ApplicationController
|
|||||||
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
request.path.split('.').first.end_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cached_filtered_status_pins
|
||||||
|
cache_collection(
|
||||||
|
filtered_pinned_statuses,
|
||||||
|
Status
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def cached_filtered_status_page
|
def cached_filtered_status_page
|
||||||
cache_collection_paginated_by_id(
|
cache_collection_paginated_by_id(
|
||||||
filtered_statuses,
|
filtered_statuses,
|
||||||
|
@ -21,6 +21,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
|
|||||||
case params[:id]
|
case params[:id]
|
||||||
when 'featured'
|
when 'featured'
|
||||||
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
|
@items = for_signed_account { cache_collection(@account.pinned_statuses, Status) }
|
||||||
|
@items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) }
|
||||||
when 'tags'
|
when 'tags'
|
||||||
@items = for_signed_account { @account.featured_tags }
|
@items = for_signed_account { @account.featured_tags }
|
||||||
when 'devices'
|
when 'devices'
|
||||||
|
@ -14,7 +14,7 @@ module Admin
|
|||||||
else
|
else
|
||||||
@account = @account_moderation_note.target_account
|
@account = @account_moderation_note.target_account
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.targeted_account_warnings.latest.custom
|
@warnings = @account.strikes.custom.latest
|
||||||
|
|
||||||
render template: 'admin/accounts/show'
|
render template: 'admin/accounts/show'
|
||||||
end
|
end
|
||||||
|
@ -2,13 +2,24 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
class AccountsController < BaseController
|
||||||
before_action :set_account, except: [:index]
|
before_action :set_account, except: [:index, :batch]
|
||||||
before_action :require_remote_account!, only: [:redownload]
|
before_action :require_remote_account!, only: [:redownload]
|
||||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :account, :index?
|
authorize :account, :index?
|
||||||
|
|
||||||
@accounts = filtered_accounts.page(params[:page])
|
@accounts = filtered_accounts.page(params[:page])
|
||||||
|
@form = Form::AccountBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_accounts_path(filter_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@ -17,7 +28,7 @@ module Admin
|
|||||||
@deletion_request = @account.deletion_request
|
@deletion_request = @account.deletion_request
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
@warnings = @account.targeted_account_warnings.latest.custom
|
@warnings = @account.strikes.custom.latest
|
||||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -38,13 +49,13 @@ module Admin
|
|||||||
def approve
|
def approve
|
||||||
authorize @account.user, :approve?
|
authorize @account.user, :approve?
|
||||||
@account.user.approve!
|
@account.user.approve!
|
||||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
redirect_to admin_accounts_path(status: 'pending'), notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@ -106,6 +117,16 @@ module Admin
|
|||||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unblock_email
|
||||||
|
authorize @account, :unblock_email?
|
||||||
|
|
||||||
|
CanonicalEmailBlock.where(reference_account: @account).delete_all
|
||||||
|
|
||||||
|
log_action :unblock_email, @account
|
||||||
|
|
||||||
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unblocked_email_msg', username: @account.acct)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@ -121,11 +142,25 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filtered_accounts
|
def filtered_accounts
|
||||||
AccountFilter.new(filter_params).results
|
AccountFilter.new(filter_params.with_defaults(order: 'recent')).results
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
params.slice(*AccountFilter::KEYS).permit(*AccountFilter::KEYS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_account_batch_params
|
||||||
|
params.require(:form_account_batch).permit(:action, account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:suspend]
|
||||||
|
'suspend'
|
||||||
|
elsif params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,49 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'sidekiq/api'
|
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class DashboardController < BaseController
|
class DashboardController < BaseController
|
||||||
def index
|
def index
|
||||||
@system_checks = Admin::SystemCheck.perform
|
@system_checks = Admin::SystemCheck.perform
|
||||||
@users_count = User.count
|
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||||
@pending_users_count = User.pending.count
|
@pending_users_count = User.pending.count
|
||||||
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
|
@pending_reports_count = Report.unresolved.count
|
||||||
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
|
|
||||||
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0
|
|
||||||
@relay_enabled = Relay.enabled.exists?
|
|
||||||
@single_user_mode = Rails.configuration.x.single_user_mode
|
|
||||||
@registrations_enabled = Setting.registrations_mode != 'none'
|
|
||||||
@deletions_enabled = Setting.open_deletion
|
|
||||||
@invites_enabled = Setting.min_invite_role == 'user'
|
|
||||||
@search_enabled = Chewy.enabled?
|
|
||||||
@version = Mastodon::Version.to_s
|
|
||||||
@database_version = ActiveRecord::Base.connection.execute('SELECT VERSION()').first['version'].match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
|
|
||||||
@redis_version = redis_info['redis_version']
|
|
||||||
@reports_count = Report.unresolved.count
|
|
||||||
@queue_backlog = Sidekiq::Stats.new.enqueued
|
|
||||||
@recent_users = User.confirmed.recent.includes(:account).limit(8)
|
|
||||||
@database_size = ActiveRecord::Base.connection.execute('SELECT pg_database_size(current_database())').first['pg_database_size']
|
|
||||||
@redis_size = redis_info['used_memory']
|
|
||||||
@ldap_enabled = ENV['LDAP_ENABLED'] == 'true'
|
|
||||||
@cas_enabled = ENV['CAS_ENABLED'] == 'true'
|
|
||||||
@saml_enabled = ENV['SAML_ENABLED'] == 'true'
|
|
||||||
@pam_enabled = ENV['PAM_ENABLED'] == 'true'
|
|
||||||
@hidden_service = ENV['ALLOW_ACCESS_TO_HIDDEN_SERVICE'] == 'true'
|
|
||||||
@trending_hashtags = TrendingTags.get(10, filtered: false)
|
|
||||||
@pending_tags_count = Tag.pending_review.count
|
@pending_tags_count = Tag.pending_review.count
|
||||||
@authorized_fetch = authorized_fetch_mode?
|
|
||||||
@whitelist_enabled = whitelist_mode?
|
|
||||||
@profile_directory = Setting.profile_directory
|
|
||||||
@timeline_preview = Setting.timeline_preview
|
|
||||||
@trends_enabled = Setting.trends
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def current_week
|
|
||||||
@current_week ||= Time.now.utc.to_date.cweek
|
|
||||||
end
|
|
||||||
|
|
||||||
def redis_info
|
def redis_info
|
||||||
@redis_info ||= begin
|
@redis_info ||= begin
|
||||||
if Redis.current.is_a?(Redis::Namespace)
|
if Redis.current.is_a?(Redis::Namespace)
|
||||||
|
@ -14,6 +14,15 @@ module Admin
|
|||||||
authorize :instance, :show?
|
authorize :instance, :show?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize :instance, :destroy?
|
||||||
|
|
||||||
|
Admin::DomainPurgeWorker.perform_async(@instance.domain)
|
||||||
|
|
||||||
|
log_action :destroy, @instance
|
||||||
|
redirect_to admin_instances_path, notice: I18n.t('admin.instances.destroyed_msg', domain: @instance.domain)
|
||||||
|
end
|
||||||
|
|
||||||
def clear_delivery_errors
|
def clear_delivery_errors
|
||||||
authorize :delivery, :clear_delivery_errors?
|
authorize :delivery, :clear_delivery_errors?
|
||||||
|
|
||||||
|
@ -1,52 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Admin
|
|
||||||
class PendingAccountsController < BaseController
|
|
||||||
before_action :set_accounts, only: :index
|
|
||||||
|
|
||||||
def index
|
|
||||||
@form = Form::AccountBatch.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def batch
|
|
||||||
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
@form.save
|
|
||||||
rescue ActionController::ParameterMissing
|
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
|
||||||
ensure
|
|
||||||
redirect_to admin_pending_accounts_path(current_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def approve_all
|
|
||||||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
|
|
||||||
redirect_to admin_pending_accounts_path(current_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reject_all
|
|
||||||
Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
|
|
||||||
redirect_to admin_pending_accounts_path(current_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_accounts
|
|
||||||
@accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
|
|
||||||
end
|
|
||||||
|
|
||||||
def form_account_batch_params
|
|
||||||
params.require(:form_account_batch).permit(:action, account_ids: [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def action_from_button
|
|
||||||
if params[:approve]
|
|
||||||
'approve'
|
|
||||||
elsif params[:reject]
|
|
||||||
'reject'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_params
|
|
||||||
params.slice(:page).permit(:page)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -14,20 +14,17 @@ module Admin
|
|||||||
if params[:create_and_resolve]
|
if params[:create_and_resolve]
|
||||||
@report.resolve!(current_account)
|
@report.resolve!(current_account)
|
||||||
log_action :resolve, @report
|
log_action :resolve, @report
|
||||||
|
elsif params[:create_and_unresolve]
|
||||||
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:create_and_unresolve]
|
|
||||||
@report.unresolve!
|
@report.unresolve!
|
||||||
log_action :reopen, @report
|
log_action :reopen, @report
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
|
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
|
||||||
else
|
else
|
||||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||||
@form = Form::StatusBatch.new
|
@action_logs = @report.history.includes(:target)
|
||||||
|
@form = Admin::StatusBatchAction.new
|
||||||
|
@statuses = @report.statuses.with_includes
|
||||||
|
|
||||||
render template: 'admin/reports/show'
|
render template: 'admin/reports/show'
|
||||||
end
|
end
|
||||||
@ -41,6 +38,14 @@ module Admin
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def after_create_redirect_path
|
||||||
|
if params[:create_and_resolve]
|
||||||
|
admin_reports_path
|
||||||
|
else
|
||||||
|
admin_report_path(@report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:report_note).permit(
|
params.require(:report_note).permit(
|
||||||
:content,
|
:content,
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Admin
|
|
||||||
class ReportedStatusesController < BaseController
|
|
||||||
before_action :set_report
|
|
||||||
|
|
||||||
def create
|
|
||||||
authorize :status, :update?
|
|
||||||
|
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
|
||||||
|
|
||||||
redirect_to admin_report_path(@report)
|
|
||||||
rescue ActionController::ParameterMissing
|
|
||||||
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
|
||||||
|
|
||||||
redirect_to admin_report_path(@report)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def status_params
|
|
||||||
params.require(:status).permit(:sensitive)
|
|
||||||
end
|
|
||||||
|
|
||||||
def form_status_batch_params
|
|
||||||
params.require(:form_status_batch).permit(status_ids: [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def action_from_button
|
|
||||||
if params[:nsfw_on]
|
|
||||||
'nsfw_on'
|
|
||||||
elsif params[:nsfw_off]
|
|
||||||
'nsfw_off'
|
|
||||||
elsif params[:delete]
|
|
||||||
'delete'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_report
|
|
||||||
@report = Report.find(params[:report_id])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -13,8 +13,10 @@ module Admin
|
|||||||
authorize @report, :show?
|
authorize @report, :show?
|
||||||
|
|
||||||
@report_note = @report.notes.new
|
@report_note = @report.notes.new
|
||||||
@report_notes = (@report.notes.latest + @report.history + @report.target_account.targeted_account_warnings.latest.custom).sort_by(&:created_at)
|
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
||||||
@form = Form::StatusBatch.new
|
@action_logs = @report.history.includes(:target)
|
||||||
|
@form = Admin::StatusBatchAction.new
|
||||||
|
@statuses = @report.statuses.with_includes
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_to_self
|
def assign_to_self
|
||||||
|
@ -2,71 +2,57 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class StatusesController < BaseController
|
class StatusesController < BaseController
|
||||||
helper_method :current_params
|
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
before_action :set_statuses
|
||||||
|
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :status, :index?
|
authorize :status, :index?
|
||||||
|
|
||||||
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
|
@status_batch_action = Admin::StatusBatchAction.new
|
||||||
|
|
||||||
if params[:media]
|
|
||||||
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
def batch
|
||||||
@form = Form::StatusBatch.new
|
@status_batch_action = Admin::StatusBatchAction.new(admin_status_batch_action_params.merge(current_account: current_account, report_id: params[:report_id], type: action_from_button))
|
||||||
end
|
@status_batch_action.save!
|
||||||
|
|
||||||
def show
|
|
||||||
authorize :status, :index?
|
|
||||||
|
|
||||||
@statuses = @account.statuses.where(id: params[:id])
|
|
||||||
authorize @statuses.first, :show?
|
|
||||||
|
|
||||||
@form = Form::StatusBatch.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
authorize :status, :update?
|
|
||||||
|
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
|
||||||
|
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
|
||||||
rescue ActionController::ParameterMissing
|
rescue ActionController::ParameterMissing
|
||||||
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
|
||||||
|
ensure
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
redirect_to after_create_redirect_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def form_status_batch_params
|
def admin_status_batch_action_params
|
||||||
params.require(:form_status_batch).permit(:action, status_ids: [])
|
params.require(:admin_status_batch_action).permit(status_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_create_redirect_path
|
||||||
|
if @status_batch_action.report_id.present?
|
||||||
|
admin_report_path(@status_batch_action.report_id)
|
||||||
|
else
|
||||||
|
admin_account_statuses_path(params[:account_id], current_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = Account.find(params[:account_id])
|
@account = Account.find(params[:account_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_params
|
def set_statuses
|
||||||
page = (params[:page] || 1).to_i
|
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
{
|
def filter_params
|
||||||
media: params[:media],
|
params.slice(*Admin::StatusFilter::KEYS).permit(*Admin::StatusFilter::KEYS)
|
||||||
page: page > 1 && page,
|
|
||||||
}.select { |_, value| value.present? }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_from_button
|
def action_from_button
|
||||||
if params[:nsfw_on]
|
if params[:report]
|
||||||
'nsfw_on'
|
'report'
|
||||||
elsif params[:nsfw_off]
|
elsif params[:remove_from_report]
|
||||||
'nsfw_off'
|
'remove_from_report'
|
||||||
elsif params[:delete]
|
elsif params[:delete]
|
||||||
'delete'
|
'delete'
|
||||||
end
|
end
|
||||||
|
@ -2,38 +2,12 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class TagsController < BaseController
|
class TagsController < BaseController
|
||||||
before_action :set_tag, except: [:index, :batch, :approve_all, :reject_all]
|
before_action :set_tag
|
||||||
before_action :set_usage_by_domain, except: [:index, :batch, :approve_all, :reject_all]
|
|
||||||
before_action :set_counters, except: [:index, :batch, :approve_all, :reject_all]
|
|
||||||
|
|
||||||
def index
|
|
||||||
authorize :tag, :index?
|
|
||||||
|
|
||||||
@tags = filtered_tags.page(params[:page])
|
|
||||||
@form = Form::TagBatch.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def batch
|
|
||||||
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
|
||||||
@form.save
|
|
||||||
rescue ActionController::ParameterMissing
|
|
||||||
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
|
||||||
ensure
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def approve_all
|
|
||||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'approve').save
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reject_all
|
|
||||||
Form::TagBatch.new(current_account: current_account, tag_ids: Tag.pending_review.pluck(:id), action: 'reject').save
|
|
||||||
redirect_to admin_tags_path(filter_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize @tag, :show?
|
authorize @tag, :show?
|
||||||
|
|
||||||
|
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@ -52,52 +26,8 @@ module Admin
|
|||||||
@tag = Tag.find(params[:id])
|
@tag = Tag.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_usage_by_domain
|
|
||||||
@usage_by_domain = @tag.statuses
|
|
||||||
.with_public_visibility
|
|
||||||
.excluding_silenced_accounts
|
|
||||||
.where(Status.arel_table[:id].gteq(Mastodon::Snowflake.id_at(Time.now.utc.beginning_of_day)))
|
|
||||||
.joins(:account)
|
|
||||||
.group('accounts.domain')
|
|
||||||
.reorder(statuses_count: :desc)
|
|
||||||
.pluck(Arel.sql('accounts.domain, count(*) AS statuses_count'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_counters
|
|
||||||
@accounts_today = @tag.history.first[:accounts]
|
|
||||||
@accounts_week = Redis.current.pfcount(*current_week_days.map { |day| "activity:tags:#{@tag.id}:#{day}:accounts" })
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_tags
|
|
||||||
TagFilter.new(filter_params).results
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_params
|
|
||||||
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_params
|
def tag_params
|
||||||
params.require(:tag).permit(:name, :trendable, :usable, :listable)
|
params.require(:tag).permit(:name, :trendable, :usable, :listable)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_week_days
|
|
||||||
now = Time.now.utc.beginning_of_day.to_date
|
|
||||||
|
|
||||||
(Date.commercial(now.cwyear, now.cweek)..now).map do |date|
|
|
||||||
date.to_time(:utc).beginning_of_day.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def form_tag_batch_params
|
|
||||||
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def action_from_button
|
|
||||||
if params[:approve]
|
|
||||||
'approve'
|
|
||||||
elsif params[:reject]
|
|
||||||
'reject'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
authorize :preview_card_provider, :index?
|
||||||
|
|
||||||
|
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
||||||
|
@form = Form::PreviewCardProviderBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::PreviewCardProviderBatch.new(form_preview_card_provider_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_trends_links_preview_card_providers_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_preview_card_providers
|
||||||
|
PreviewCardProviderFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *PreviewCardProviderFilter::KEYS).permit(:page, *PreviewCardProviderFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_preview_card_provider_batch_params
|
||||||
|
params.require(:form_preview_card_provider_batch).permit(:action, preview_card_provider_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
45
app/controllers/admin/trends/links_controller.rb
Normal file
45
app/controllers/admin/trends/links_controller.rb
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Trends::LinksController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
authorize :preview_card, :index?
|
||||||
|
|
||||||
|
@preview_cards = filtered_preview_cards.page(params[:page])
|
||||||
|
@form = Form::PreviewCardBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::PreviewCardBatch.new(form_preview_card_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_trends_links_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_preview_cards
|
||||||
|
PreviewCardFilter.new(filter_params.with_defaults(trending: 'all')).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *PreviewCardFilter::KEYS).permit(:page, *PreviewCardFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_preview_card_batch_params
|
||||||
|
params.require(:form_preview_card_batch).permit(:action, preview_card_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:approve_all]
|
||||||
|
'approve_all'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
elsif params[:reject_all]
|
||||||
|
'reject_all'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
41
app/controllers/admin/trends/tags_controller.rb
Normal file
41
app/controllers/admin/trends/tags_controller.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Trends::TagsController < Admin::BaseController
|
||||||
|
def index
|
||||||
|
authorize :tag, :index?
|
||||||
|
|
||||||
|
@tags = filtered_tags.page(params[:page])
|
||||||
|
@form = Form::TagBatch.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def batch
|
||||||
|
@form = Form::TagBatch.new(form_tag_batch_params.merge(current_account: current_account, action: action_from_button))
|
||||||
|
@form.save
|
||||||
|
rescue ActionController::ParameterMissing
|
||||||
|
flash[:alert] = I18n.t('admin.accounts.no_account_selected')
|
||||||
|
ensure
|
||||||
|
redirect_to admin_trends_tags_path(filter_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filtered_tags
|
||||||
|
TagFilter.new(filter_params).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def form_tag_batch_params
|
||||||
|
params.require(:form_tag_batch).permit(:action, tag_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def action_from_button
|
||||||
|
if params[:approve]
|
||||||
|
'approve'
|
||||||
|
elsif params[:reject]
|
||||||
|
'reject'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,23 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::ProofsController < Api::BaseController
|
|
||||||
include AccountOwnedConcern
|
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!
|
|
||||||
|
|
||||||
before_action :set_provider
|
|
||||||
|
|
||||||
def index
|
|
||||||
render json: @account, serializer: @provider.serializer_class
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_provider
|
|
||||||
@provider = ProofProvider.find(params[:provider]) || raise(ActiveRecord::RecordNotFound)
|
|
||||||
end
|
|
||||||
|
|
||||||
def username_param
|
|
||||||
params[:username]
|
|
||||||
end
|
|
||||||
end
|
|
@ -5,8 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
render json: []
|
||||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -49,9 +49,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def pinned_scope
|
def pinned_scope
|
||||||
return Status.none if @account.blocking?(current_account)
|
@account.pinned_statuses.permitted_for(@account, current_account)
|
||||||
|
|
||||||
@account.pinned_statuses
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def no_replies_scope
|
def no_replies_scope
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::AccountsController < Api::BaseController
|
class Api::V1::AccountsController < Api::BaseController
|
||||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :block, :unblock, :mute, :unmute]
|
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, except: [:create, :follow, :unfollow, :remove_from_followers, :block, :unblock, :mute, :unmute]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow]
|
before_action -> { doorkeeper_authorize! :follow, :'write:follows' }, only: [:follow, :unfollow, :remove_from_followers]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
before_action -> { doorkeeper_authorize! :follow, :'write:mutes' }, only: [:mute, :unmute]
|
||||||
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
before_action -> { doorkeeper_authorize! :follow, :'write:blocks' }, only: [:block, :unblock]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:create]
|
||||||
@ -53,6 +53,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remove_from_followers
|
||||||
|
RemoveFromFollowersService.new.call(current_user.account, @account)
|
||||||
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
|
end
|
||||||
|
|
||||||
def unblock
|
def unblock
|
||||||
UnblockService.new.call(current_user.account, @account)
|
UnblockService.new.call(current_user.account, @account)
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
@ -78,10 +83,14 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
forbidden if single_user_mode? || !allowed_registrations?
|
forbidden if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none'
|
Setting.registrations_mode != 'none'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
class Api::V1::Admin::AccountActionsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::AccountsController < Api::BaseController
|
class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:accounts' }, only: [:index, :show]
|
||||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:accounts' }, except: [:index, :show]
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_accounts, only: :index
|
before_action :set_accounts, only: :index
|
||||||
before_action :set_account, except: :index
|
before_action :set_account, except: :index
|
||||||
@ -94,7 +96,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_accounts
|
def set_accounts
|
||||||
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite, :ips]).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
|
25
app/controllers/api/v1/admin/dimensions_controller.rb
Normal file
25
app/controllers/api/v1/admin/dimensions_controller.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::DimensionsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_dimensions
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @dimensions, each_serializer: REST::Admin::DimensionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_dimensions
|
||||||
|
@dimensions = Admin::Metrics::Dimension.retrieve(
|
||||||
|
params[:keys],
|
||||||
|
params[:start_at],
|
||||||
|
params[:end_at],
|
||||||
|
params[:limit],
|
||||||
|
params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
24
app/controllers/api/v1/admin/measures_controller.rb
Normal file
24
app/controllers/api/v1/admin/measures_controller.rb
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::MeasuresController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_measures
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @measures, each_serializer: REST::Admin::MeasureSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_measures
|
||||||
|
@measures = Admin::Metrics::Measure.retrieve(
|
||||||
|
params[:keys],
|
||||||
|
params[:start_at],
|
||||||
|
params[:end_at],
|
||||||
|
params
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -1,13 +1,15 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::Admin::ReportsController < Api::BaseController
|
class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
include Authorization
|
include Authorization
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:reports' }, only: [:index, :show]
|
||||||
before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:reports' }, except: [:index, :show]
|
||||||
before_action :require_staff!
|
before_action :require_staff!
|
||||||
before_action :set_reports, only: :index
|
before_action :set_reports, only: :index
|
||||||
before_action :set_report, except: :index
|
before_action :set_report, except: :index
|
||||||
@ -32,6 +34,12 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
|||||||
render json: @report, serializer: REST::Admin::ReportSerializer
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
authorize @report, :update?
|
||||||
|
@report.update!(report_params)
|
||||||
|
render json: @report, serializer: REST::Admin::ReportSerializer
|
||||||
|
end
|
||||||
|
|
||||||
def assign_to_self
|
def assign_to_self
|
||||||
authorize @report, :update?
|
authorize @report, :update?
|
||||||
@report.update!(assigned_account_id: current_account.id)
|
@report.update!(assigned_account_id: current_account.id)
|
||||||
@ -74,6 +82,10 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
|||||||
ReportFilter.new(filter_params).results
|
ReportFilter.new(filter_params).results
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def report_params
|
||||||
|
params.permit(:category, rule_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
def filter_params
|
def filter_params
|
||||||
params.permit(*FILTER_PARAMS)
|
params.permit(*FILTER_PARAMS)
|
||||||
end
|
end
|
||||||
|
23
app/controllers/api/v1/admin/retention_controller.rb
Normal file
23
app/controllers/api/v1/admin/retention_controller.rb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::RetentionController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_cohorts
|
||||||
|
|
||||||
|
def create
|
||||||
|
render json: @cohorts, each_serializer: REST::Admin::CohortSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_cohorts
|
||||||
|
@cohorts = Admin::Metrics::Retention.new(
|
||||||
|
params[:start_at],
|
||||||
|
params[:end_at],
|
||||||
|
params[:frequency]
|
||||||
|
).cohorts
|
||||||
|
end
|
||||||
|
end
|
19
app/controllers/api/v1/admin/trends/tags_controller.rb
Normal file
19
app/controllers/api/v1/admin/trends/tags_controller.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Admin::Trends::TagsController < Api::BaseController
|
||||||
|
protect_from_forgery with: :exception
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :'admin:read' }
|
||||||
|
before_action :require_staff!
|
||||||
|
before_action :set_tags
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @tags, each_serializer: REST::Admin::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tags
|
||||||
|
@tags = Trends.tags.get(false, limit_param(10))
|
||||||
|
end
|
||||||
|
end
|
@ -14,22 +14,21 @@ class Api::V1::Instances::ActivityController < Api::BaseController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def activity
|
def activity
|
||||||
weeks = []
|
statuses_tracker = ActivityTracker.new('activity:statuses:local', :basic)
|
||||||
|
logins_tracker = ActivityTracker.new('activity:logins', :unique)
|
||||||
|
registrations_tracker = ActivityTracker.new('activity:accounts:local', :basic)
|
||||||
|
|
||||||
12.times do |i|
|
(0...12).map do |i|
|
||||||
day = i.weeks.ago.to_date
|
start_of_week = i.weeks.ago
|
||||||
week_id = day.cweek
|
end_of_week = start_of_week + 6.days
|
||||||
week = Date.commercial(day.cwyear, week_id)
|
|
||||||
|
|
||||||
weeks << {
|
{
|
||||||
week: week.to_time.to_i.to_s,
|
week: start_of_week.to_i.to_s,
|
||||||
statuses: Redis.current.get("activity:statuses:local:#{week_id}") || '0',
|
statuses: statuses_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
logins: Redis.current.pfcount("activity:logins:#{week_id}").to_s,
|
logins: logins_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
registrations: Redis.current.get("activity:accounts:local:#{week_id}") || '0',
|
registrations: registrations_tracker.sum(start_of_week, end_of_week).to_s,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
weeks
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_enabled_api!
|
def require_enabled_api!
|
||||||
|
21
app/controllers/api/v1/statuses/histories_controller.rb
Normal file
21
app/controllers/api/v1/statuses/histories_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::HistoriesController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||||
|
before_action :set_status
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @status.edits, each_serializer: REST::StatusEditSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
authorize @status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
21
app/controllers/api/v1/statuses/sources_controller.rb
Normal file
21
app/controllers/api/v1/statuses/sources_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Statuses::SourcesController < Api::BaseController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
|
||||||
|
before_action :set_status
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @status, serializer: REST::StatusSourceSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_status
|
||||||
|
@status = Status.find(params[:status_id])
|
||||||
|
authorize @status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
|
end
|
||||||
|
end
|
@ -60,7 +60,7 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
|
|
||||||
@status.discard
|
@status.discard
|
||||||
RemovalWorker.perform_async(@status.id, redraft: true)
|
RemovalWorker.perform_async(@status.id, { 'redraft' => true })
|
||||||
@status.account.statuses_count = @status.account.statuses_count - 1
|
@status.account.statuses_count = @status.account.statuses_count - 1
|
||||||
|
|
||||||
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
render json: @status, serializer: REST::StatusSerializer, source_requested: true
|
||||||
|
21
app/controllers/api/v1/trends/links_controller.rb
Normal file
21
app/controllers/api/v1/trends/links_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Trends::LinksController < Api::BaseController
|
||||||
|
before_action :set_links
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @links, each_serializer: REST::Trends::LinkSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_links
|
||||||
|
@links = begin
|
||||||
|
if Setting.trends
|
||||||
|
Trends.links.get(true, limit_param(10))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
21
app/controllers/api/v1/trends/tags_controller.rb
Normal file
21
app/controllers/api/v1/trends/tags_controller.rb
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Trends::TagsController < Api::BaseController
|
||||||
|
before_action :set_tags
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @tags, each_serializer: REST::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tags
|
||||||
|
@tags = begin
|
||||||
|
if Setting.trends
|
||||||
|
Trends.tags.get(true, limit_param(10))
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,15 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::V1::TrendsController < Api::BaseController
|
|
||||||
before_action :set_tags
|
|
||||||
|
|
||||||
def index
|
|
||||||
render json: @tags, each_serializer: REST::TagSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_tags
|
|
||||||
@tags = TrendingTags.get(limit_param(10))
|
|
||||||
end
|
|
||||||
end
|
|
@ -10,7 +10,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.errors.empty?
|
if resource.errors.empty?
|
||||||
resource.session_activations.destroy_all
|
resource.session_activations.destroy_all
|
||||||
resource.forget_me!
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::RegistrationsController < Devise::RegistrationsController
|
class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
include RegistrationSpamConcern
|
include RegistrationSpamConcern
|
||||||
|
|
||||||
layout :determine_layout
|
layout :determine_layout
|
||||||
@ -30,8 +29,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
super do |resource|
|
super do |resource|
|
||||||
if resource.saved_change_to_encrypted_password?
|
if resource.saved_change_to_encrypted_password?
|
||||||
resource.clear_other_sessions(current_session.session_id)
|
resource.clear_other_sessions(current_session.session_id)
|
||||||
resource.forget_me!
|
|
||||||
remember_me(resource)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -84,13 +81,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
redirect_to root_path if single_user_mode? || omniauth_only? || !allowed_registrations?
|
||||||
end
|
end
|
||||||
|
|
||||||
def allowed_registrations?
|
def allowed_registrations?
|
||||||
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
Setting.registrations_mode != 'none' || @invite&.valid_for_use?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
def invite_code
|
def invite_code
|
||||||
if params[:user]
|
if params[:user]
|
||||||
params[:user][:invite_code]
|
params[:user][:invite_code]
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::SessionsController < Devise::SessionsController
|
class Auth::SessionsController < Devise::SessionsController
|
||||||
include Devise::Controllers::Rememberable
|
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
skip_before_action :require_no_authentication, only: [:create]
|
skip_before_action :require_no_authentication, only: [:create]
|
||||||
@ -15,14 +13,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
before_action :set_instance_presenter, only: [:new]
|
before_action :set_instance_presenter, only: [:new]
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
def new
|
|
||||||
Devise.omniauth_configs.each do |provider, config|
|
|
||||||
return redirect_to(omniauth_authorize_path(resource_name, provider)) if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
super do |resource|
|
super do |resource|
|
||||||
# We only need to call this if this hasn't already been
|
# We only need to call this if this hasn't already been
|
||||||
@ -89,14 +79,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_sign_out_path_for(_resource_or_scope)
|
|
||||||
Devise.omniauth_configs.each_value do |config|
|
|
||||||
return root_path if config.strategy.redirect_at_sign_in
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_no_authentication
|
def require_no_authentication
|
||||||
super
|
super
|
||||||
|
|
||||||
@ -149,8 +131,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
clear_attempt_from_session
|
clear_attempt_from_session
|
||||||
|
|
||||||
user.update_sign_in!(request, new_sign_in: true)
|
user.update_sign_in!(new_sign_in: true)
|
||||||
remember_me(user)
|
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
flash.delete(:notice)
|
flash.delete(:notice)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ module AccountOwnedConcern
|
|||||||
before_action :set_account, if: :account_required?
|
before_action :set_account, if: :account_required?
|
||||||
before_action :check_account_approval, if: :account_required?
|
before_action :check_account_approval, if: :account_required?
|
||||||
before_action :check_account_suspension, if: :account_required?
|
before_action :check_account_suspension, if: :account_required?
|
||||||
|
before_action :check_account_confirmation, if: :account_required?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -28,6 +29,10 @@ module AccountOwnedConcern
|
|||||||
not_found if @account.local? && @account.user_pending?
|
not_found if @account.local? && @account.user_pending?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_account_confirmation
|
||||||
|
not_found if @account.local? && !@account.user_confirmed?
|
||||||
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
def check_account_suspension
|
||||||
if @account.suspended_permanently?
|
if @account.suspended_permanently?
|
||||||
permanent_suspension_response
|
permanent_suspension_response
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
module AccountableConcern
|
module AccountableConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def log_action(action, target)
|
def log_action(action, target, options = {})
|
||||||
Admin::ActionLog.create(account: current_account, action: action, target: target)
|
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,7 +57,7 @@ module TwoFactorAuthenticationConcern
|
|||||||
|
|
||||||
if valid_webauthn_credential?(user, webauthn_credential)
|
if valid_webauthn_credential?(user, webauthn_credential)
|
||||||
on_authentication_success(user, :webauthn)
|
on_authentication_success(user, :webauthn)
|
||||||
render json: { redirect_path: root_path }, status: :ok
|
render json: { redirect_path: after_sign_in_path_for(user) }, status: :ok
|
||||||
else
|
else
|
||||||
on_authentication_failure(user, :webauthn, :invalid_credential)
|
on_authentication_failure(user, :webauthn, :invalid_credential)
|
||||||
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
render json: { error: t('webauthn_credentials.invalid_credential') }, status: :unprocessable_entity
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
module UserTrackingConcern
|
module UserTrackingConcern
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
UPDATE_SIGN_IN_HOURS = 24
|
UPDATE_SIGN_IN_FREQUENCY = 24.hours.freeze
|
||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :update_user_sign_in
|
before_action :update_user_sign_in
|
||||||
@ -12,10 +12,10 @@ module UserTrackingConcern
|
|||||||
private
|
private
|
||||||
|
|
||||||
def update_user_sign_in
|
def update_user_sign_in
|
||||||
current_user.update_sign_in!(request) if user_needs_sign_in_update?
|
current_user.update_sign_in! if user_needs_sign_in_update?
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_needs_sign_in_update?
|
def user_needs_sign_in_update?
|
||||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
|
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_FREQUENCY.ago)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -27,7 +27,12 @@ class MediaController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def set_media_attachment
|
def set_media_attachment
|
||||||
@media_attachment = MediaAttachment.attached.find_by!(shortcode: params[:id] || params[:medium_id])
|
id = params[:id] || params[:medium_id]
|
||||||
|
return if id.nil?
|
||||||
|
|
||||||
|
scope = MediaAttachment.local.attached
|
||||||
|
# If id is 19 characters long, it's a shortcode, otherwise it's an identifier
|
||||||
|
@media_attachment = id.size == 19 ? scope.find_by!(shortcode: id) : scope.find_by!(id: id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_permitted_status!
|
def verify_permitted_status!
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Settings::IdentityProofsController < Settings::BaseController
|
|
||||||
before_action :check_required_params, only: :new
|
|
||||||
|
|
||||||
def index
|
|
||||||
@proofs = AccountIdentityProof.where(account: current_account).order(provider: :asc, provider_username: :asc)
|
|
||||||
@proofs.each(&:refresh!)
|
|
||||||
end
|
|
||||||
|
|
||||||
def new
|
|
||||||
@proof = current_account.identity_proofs.new(
|
|
||||||
token: params[:token],
|
|
||||||
provider: params[:provider],
|
|
||||||
provider_username: params[:provider_username]
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_account.username.casecmp(params[:username]).zero?
|
|
||||||
render layout: 'auth'
|
|
||||||
else
|
|
||||||
redirect_to settings_identity_proofs_path, alert: I18n.t('identity_proofs.errors.wrong_user', proving: params[:username], current: current_account.username)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@proof = current_account.identity_proofs.where(provider: resource_params[:provider], provider_username: resource_params[:provider_username]).first_or_initialize(resource_params)
|
|
||||||
@proof.token = resource_params[:token]
|
|
||||||
|
|
||||||
if @proof.save
|
|
||||||
PostStatusService.new.call(current_user.account, text: post_params[:status_text]) if publish_proof?
|
|
||||||
redirect_to @proof.on_success_path(params[:user_agent])
|
|
||||||
else
|
|
||||||
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
|
|
||||||
redirect_to settings_identity_proofs_path unless [:provider, :provider_username, :username, :token].all? { |k| params[k].present? }
|
|
||||||
end
|
|
||||||
|
|
||||||
def resource_params
|
|
||||||
params.require(:account_identity_proof).permit(:provider, :provider_username, :token)
|
|
||||||
end
|
|
||||||
|
|
||||||
def publish_proof?
|
|
||||||
ActiveModel::Type::Boolean.new.cast(post_params[:post_status])
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_params
|
|
||||||
params.require(:account_identity_proof).permit(:post_status, :status_text)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,9 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module WellKnown
|
|
||||||
class KeybaseProofConfigController < ActionController::Base
|
|
||||||
def show
|
|
||||||
render json: {}, serializer: ProofProvider::Keybase::ConfigSerializer, root: 'keybase_config'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -31,11 +31,15 @@ module Admin::ActionLogsHelper
|
|||||||
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
link_to truncate(record.text), edit_admin_announcement_path(record.id)
|
||||||
when 'IpBlock'
|
when 'IpBlock'
|
||||||
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
|
||||||
|
when 'Instance'
|
||||||
|
record.domain
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_target_from_history(type, attributes)
|
def log_target_from_history(type, attributes)
|
||||||
case type
|
case type
|
||||||
|
when 'User'
|
||||||
|
attributes['username']
|
||||||
when 'CustomEmoji'
|
when 'CustomEmoji'
|
||||||
attributes['shortcode']
|
attributes['shortcode']
|
||||||
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
|
||||||
@ -52,6 +56,8 @@ module Admin::ActionLogsHelper
|
|||||||
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
|
||||||
when 'IpBlock'
|
when 'IpBlock'
|
||||||
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
|
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
|
||||||
|
when 'Instance'
|
||||||
|
attributes['domain']
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,10 +1,41 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Admin::DashboardHelper
|
module Admin::DashboardHelper
|
||||||
def feature_hint(feature, enabled)
|
def relevant_account_ip(account, ip_query)
|
||||||
indicator = safe_join([enabled ? t('simple_form.yes') : t('simple_form.no'), fa_icon('power-off fw')], ' ')
|
ips = account.user.present? ? account.user.ips.to_a : []
|
||||||
class_names = enabled ? 'pull-right positive-hint' : 'pull-right neutral-hint'
|
|
||||||
|
|
||||||
safe_join([feature, content_tag(:span, indicator, class: class_names)])
|
matched_ip = begin
|
||||||
|
ip_query_addr = IPAddr.new(ip_query)
|
||||||
|
ips.find { |ip| ip_query_addr.include?(ip.ip) } || ips.first
|
||||||
|
rescue IPAddr::Error
|
||||||
|
ips.first
|
||||||
|
end
|
||||||
|
|
||||||
|
if matched_ip
|
||||||
|
link_to matched_ip.ip, admin_accounts_path(ip: matched_ip.ip)
|
||||||
|
else
|
||||||
|
'-'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relevant_account_timestamp(account)
|
||||||
|
timestamp, exact = begin
|
||||||
|
if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
|
||||||
|
[account.user_current_sign_in_at, true]
|
||||||
|
elsif account.user_current_sign_in_at
|
||||||
|
[account.user_current_sign_in_at, false]
|
||||||
|
elsif account.user_pending?
|
||||||
|
[account.user_created_at, true]
|
||||||
|
elsif account.last_status_at.present?
|
||||||
|
[account.last_status_at, true]
|
||||||
|
else
|
||||||
|
[nil, false]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return '-' if timestamp.nil?
|
||||||
|
return t('generic.today') unless exact
|
||||||
|
|
||||||
|
content_tag(:time, l(timestamp), class: 'time-ago', datetime: timestamp.iso8601, title: l(timestamp))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -6,11 +6,14 @@ module Admin::FilterHelper
|
|||||||
CustomEmojiFilter::KEYS,
|
CustomEmojiFilter::KEYS,
|
||||||
ReportFilter::KEYS,
|
ReportFilter::KEYS,
|
||||||
TagFilter::KEYS,
|
TagFilter::KEYS,
|
||||||
|
PreviewCardProviderFilter::KEYS,
|
||||||
|
PreviewCardFilter::KEYS,
|
||||||
InstanceFilter::KEYS,
|
InstanceFilter::KEYS,
|
||||||
InviteFilter::KEYS,
|
InviteFilter::KEYS,
|
||||||
RelationshipFilter::KEYS,
|
RelationshipFilter::KEYS,
|
||||||
AnnouncementFilter::KEYS,
|
AnnouncementFilter::KEYS,
|
||||||
Admin::ActionLogFilter::KEYS,
|
Admin::ActionLogFilter::KEYS,
|
||||||
|
Admin::StatusFilter::KEYS,
|
||||||
].flatten.freeze
|
].flatten.freeze
|
||||||
|
|
||||||
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
|
||||||
|
@ -50,13 +50,39 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def available_sign_up_path
|
def available_sign_up_path
|
||||||
if closed_registrations?
|
if closed_registrations? || omniauth_only?
|
||||||
'https://joinmastodon.org/#getting-started'
|
'https://joinmastodon.org/#getting-started'
|
||||||
else
|
else
|
||||||
new_user_registration_path
|
new_user_registration_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def omniauth_only?
|
||||||
|
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_to_login(name = nil, html_options = nil, &block)
|
||||||
|
target = new_user_session_path
|
||||||
|
|
||||||
|
html_options = name if block_given?
|
||||||
|
|
||||||
|
if omniauth_only? && Devise.mappings[:user].omniauthable? && User.omniauth_providers.size == 1
|
||||||
|
target = omniauth_authorize_path(:user, User.omniauth_providers[0])
|
||||||
|
html_options ||= {}
|
||||||
|
html_options[:method] = :post
|
||||||
|
end
|
||||||
|
|
||||||
|
if block_given?
|
||||||
|
link_to(target, html_options, &block)
|
||||||
|
else
|
||||||
|
link_to(name, target, html_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def provider_sign_in_link(provider)
|
||||||
|
link_to I18n.t("auth.providers.#{provider}", default: provider.to_s.chomp('_oauth2').capitalize), omniauth_authorize_path(:user, provider), class: "button button-#{provider}", method: :post
|
||||||
|
end
|
||||||
|
|
||||||
def open_deletion?
|
def open_deletion?
|
||||||
Setting.open_deletion
|
Setting.open_deletion
|
||||||
end
|
end
|
||||||
@ -137,6 +163,10 @@ module ApplicationHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def react_admin_component(name, props = {})
|
||||||
|
content_tag(:div, nil, data: { 'admin-component': name.to_s.camelcase, props: Oj.dump({ locale: I18n.locale }.merge(props)) })
|
||||||
|
end
|
||||||
|
|
||||||
def body_classes
|
def body_classes
|
||||||
output = (@body_classes || '').split(' ')
|
output = (@body_classes || '').split(' ')
|
||||||
output << "theme-#{current_theme.parameterize}"
|
output << "theme-#{current_theme.parameterize}"
|
||||||
|
@ -34,7 +34,13 @@ module JsonLdHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def as_array(value)
|
def as_array(value)
|
||||||
value.is_a?(Array) ? value : [value]
|
if value.nil?
|
||||||
|
[]
|
||||||
|
elsif value.is_a?(Array)
|
||||||
|
value
|
||||||
|
else
|
||||||
|
[value]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def value_or_id(value)
|
def value_or_id(value)
|
||||||
|
94
app/helpers/languages_helper.rb
Normal file
94
app/helpers/languages_helper.rb
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module LanguagesHelper
|
||||||
|
HUMAN_LOCALES = {
|
||||||
|
af: 'Afrikaans',
|
||||||
|
ar: 'العربية',
|
||||||
|
ast: 'Asturianu',
|
||||||
|
bg: 'Български',
|
||||||
|
bn: 'বাংলা',
|
||||||
|
br: 'Breton',
|
||||||
|
ca: 'Català',
|
||||||
|
co: 'Corsu',
|
||||||
|
cs: 'Čeština',
|
||||||
|
cy: 'Cymraeg',
|
||||||
|
da: 'Dansk',
|
||||||
|
de: 'Deutsch',
|
||||||
|
el: 'Ελληνικά',
|
||||||
|
en: 'English',
|
||||||
|
eo: 'Esperanto',
|
||||||
|
'es-AR': 'Español (Argentina)',
|
||||||
|
'es-MX': 'Español (México)',
|
||||||
|
es: 'Español',
|
||||||
|
et: 'Eesti',
|
||||||
|
eu: 'Euskara',
|
||||||
|
fa: 'فارسی',
|
||||||
|
fi: 'Suomi',
|
||||||
|
fr: 'Français',
|
||||||
|
ga: 'Gaeilge',
|
||||||
|
gd: 'Gàidhlig',
|
||||||
|
gl: 'Galego',
|
||||||
|
he: 'עברית',
|
||||||
|
hi: 'हिन्दी',
|
||||||
|
hr: 'Hrvatski',
|
||||||
|
hu: 'Magyar',
|
||||||
|
hy: 'Հայերեն',
|
||||||
|
id: 'Bahasa Indonesia',
|
||||||
|
io: 'Ido',
|
||||||
|
is: 'Íslenska',
|
||||||
|
it: 'Italiano',
|
||||||
|
ja: '日本語',
|
||||||
|
ka: 'ქართული',
|
||||||
|
kab: 'Taqbaylit',
|
||||||
|
kk: 'Қазақша',
|
||||||
|
kmr: 'Kurmancî',
|
||||||
|
kn: 'ಕನ್ನಡ',
|
||||||
|
ko: '한국어',
|
||||||
|
ku: 'سۆرانی',
|
||||||
|
lt: 'Lietuvių',
|
||||||
|
lv: 'Latviešu',
|
||||||
|
mk: 'Македонски',
|
||||||
|
ml: 'മലയാളം',
|
||||||
|
mr: 'मराठी',
|
||||||
|
ms: 'Bahasa Melayu',
|
||||||
|
nl: 'Nederlands',
|
||||||
|
nn: 'Nynorsk',
|
||||||
|
no: 'Norsk',
|
||||||
|
oc: 'Occitan',
|
||||||
|
pl: 'Polski',
|
||||||
|
'pt-BR': 'Português (Brasil)',
|
||||||
|
'pt-PT': 'Português (Portugal)',
|
||||||
|
pt: 'Português',
|
||||||
|
ro: 'Română',
|
||||||
|
ru: 'Русский',
|
||||||
|
sa: 'संस्कृतम्',
|
||||||
|
sc: 'Sardu',
|
||||||
|
si: 'සිංහල',
|
||||||
|
sk: 'Slovenčina',
|
||||||
|
sl: 'Slovenščina',
|
||||||
|
sq: 'Shqip',
|
||||||
|
'sr-Latn': 'Srpski (latinica)',
|
||||||
|
sr: 'Српски',
|
||||||
|
sv: 'Svenska',
|
||||||
|
ta: 'தமிழ்',
|
||||||
|
te: 'తెలుగు',
|
||||||
|
th: 'ไทย',
|
||||||
|
tr: 'Türkçe',
|
||||||
|
uk: 'Українська',
|
||||||
|
ur: 'اُردُو',
|
||||||
|
vi: 'Tiếng Việt',
|
||||||
|
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
||||||
|
'zh-CN': '简体中文',
|
||||||
|
'zh-HK': '繁體中文(香港)',
|
||||||
|
'zh-TW': '繁體中文(臺灣)',
|
||||||
|
zh: '中文',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def human_locale(locale)
|
||||||
|
if locale == 'und'
|
||||||
|
I18n.t('generic.none')
|
||||||
|
else
|
||||||
|
HUMAN_LOCALES[locale.to_sym] || locale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,94 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SettingsHelper
|
module SettingsHelper
|
||||||
HUMAN_LOCALES = {
|
|
||||||
af: 'Afrikaans',
|
|
||||||
ar: 'العربية',
|
|
||||||
ast: 'Asturianu',
|
|
||||||
bg: 'Български',
|
|
||||||
bn: 'বাংলা',
|
|
||||||
br: 'Breton',
|
|
||||||
ca: 'Català',
|
|
||||||
co: 'Corsu',
|
|
||||||
cs: 'Čeština',
|
|
||||||
cy: 'Cymraeg',
|
|
||||||
da: 'Dansk',
|
|
||||||
de: 'Deutsch',
|
|
||||||
el: 'Ελληνικά',
|
|
||||||
en: 'English',
|
|
||||||
eo: 'Esperanto',
|
|
||||||
'es-AR': 'Español (Argentina)',
|
|
||||||
'es-MX': 'Español (México)',
|
|
||||||
es: 'Español',
|
|
||||||
et: 'Eesti',
|
|
||||||
eu: 'Euskara',
|
|
||||||
fa: 'فارسی',
|
|
||||||
fi: 'Suomi',
|
|
||||||
fr: 'Français',
|
|
||||||
ga: 'Gaeilge',
|
|
||||||
gd: 'Gàidhlig',
|
|
||||||
gl: 'Galego',
|
|
||||||
he: 'עברית',
|
|
||||||
hi: 'हिन्दी',
|
|
||||||
hr: 'Hrvatski',
|
|
||||||
hu: 'Magyar',
|
|
||||||
hy: 'Հայերեն',
|
|
||||||
id: 'Bahasa Indonesia',
|
|
||||||
io: 'Ido',
|
|
||||||
is: 'Íslenska',
|
|
||||||
it: 'Italiano',
|
|
||||||
ja: '日本語',
|
|
||||||
ka: 'ქართული',
|
|
||||||
kab: 'Taqbaylit',
|
|
||||||
kk: 'Қазақша',
|
|
||||||
kn: 'ಕನ್ನಡ',
|
|
||||||
ko: '한국어',
|
|
||||||
ku: 'سۆرانی',
|
|
||||||
lt: 'Lietuvių',
|
|
||||||
lv: 'Latviešu',
|
|
||||||
mk: 'Македонски',
|
|
||||||
ml: 'മലയാളം',
|
|
||||||
mr: 'मराठी',
|
|
||||||
ms: 'Bahasa Melayu',
|
|
||||||
nl: 'Nederlands',
|
|
||||||
nn: 'Nynorsk',
|
|
||||||
no: 'Norsk',
|
|
||||||
oc: 'Occitan',
|
|
||||||
pl: 'Polski',
|
|
||||||
'pt-BR': 'Português (Brasil)',
|
|
||||||
'pt-PT': 'Português (Portugal)',
|
|
||||||
pt: 'Português',
|
|
||||||
ro: 'Română',
|
|
||||||
ru: 'Русский',
|
|
||||||
sa: 'संस्कृतम्',
|
|
||||||
sc: 'Sardu',
|
|
||||||
si: 'සිංහල',
|
|
||||||
sk: 'Slovenčina',
|
|
||||||
sl: 'Slovenščina',
|
|
||||||
sq: 'Shqip',
|
|
||||||
'sr-Latn': 'Srpski (latinica)',
|
|
||||||
sr: 'Српски',
|
|
||||||
sv: 'Svenska',
|
|
||||||
ta: 'தமிழ்',
|
|
||||||
te: 'తెలుగు',
|
|
||||||
th: 'ไทย',
|
|
||||||
tr: 'Türkçe',
|
|
||||||
uk: 'Українська',
|
|
||||||
ur: 'اُردُو',
|
|
||||||
vi: 'Tiếng Việt',
|
|
||||||
zgh: 'ⵜⴰⵎⴰⵣⵉⵖⵜ',
|
|
||||||
'zh-CN': '简体中文',
|
|
||||||
'zh-HK': '繁體中文(香港)',
|
|
||||||
'zh-TW': '繁體中文(臺灣)',
|
|
||||||
zh: '中文',
|
|
||||||
}.freeze
|
|
||||||
|
|
||||||
def human_locale(locale)
|
|
||||||
HUMAN_LOCALES[locale]
|
|
||||||
end
|
|
||||||
|
|
||||||
def filterable_languages
|
def filterable_languages
|
||||||
LanguageDetector.instance.language_names.select(&HUMAN_LOCALES.method(:key?))
|
LanguageDetector.instance.language_names.select(&LanguagesHelper::HUMAN_LOCALES.method(:key?))
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash_to_object(hash)
|
def hash_to_object(hash)
|
||||||
|
@ -39,6 +39,7 @@ export const THUMBNAIL_UPLOAD_PROGRESS = 'THUMBNAIL_UPLOAD_PROGRESS';
|
|||||||
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR';
|
||||||
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY';
|
||||||
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
||||||
|
export const COMPOSE_SUGGESTION_IGNORE = 'COMPOSE_SUGGESTION_IGNORE';
|
||||||
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
export const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE';
|
||||||
|
|
||||||
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
export const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE';
|
||||||
@ -271,12 +272,15 @@ export function uploadCompose(files) {
|
|||||||
if (status === 200) {
|
if (status === 200) {
|
||||||
dispatch(uploadComposeSuccess(data, f));
|
dispatch(uploadComposeSuccess(data, f));
|
||||||
} else if (status === 202) {
|
} else if (status === 202) {
|
||||||
|
let tryCount = 1;
|
||||||
const poll = () => {
|
const poll = () => {
|
||||||
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
dispatch(uploadComposeSuccess(response.data, f));
|
dispatch(uploadComposeSuccess(response.data, f));
|
||||||
} else if (response.status === 206) {
|
} else if (response.status === 206) {
|
||||||
setTimeout(() => poll(), 1000);
|
let retryAfter = (Math.log2(tryCount) || 1) * 1000;
|
||||||
|
tryCount += 1;
|
||||||
|
setTimeout(() => poll(), retryAfter);
|
||||||
}
|
}
|
||||||
}).catch(error => dispatch(uploadComposeFail(error)));
|
}).catch(error => dispatch(uploadComposeFail(error)));
|
||||||
};
|
};
|
||||||
@ -554,6 +558,9 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||||||
startPosition = position;
|
startPosition = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that
|
||||||
|
// the suggestions are dismissed and the cursor moves forward.
|
||||||
|
if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_SUGGESTION_SELECT,
|
type: COMPOSE_SUGGESTION_SELECT,
|
||||||
position: startPosition,
|
position: startPosition,
|
||||||
@ -561,6 +568,15 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
|
|||||||
completion,
|
completion,
|
||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: COMPOSE_SUGGESTION_IGNORE,
|
||||||
|
position: startPosition,
|
||||||
|
token,
|
||||||
|
completion,
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import api from '../api';
|
|
||||||
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST';
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS';
|
|
||||||
export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => {
|
|
||||||
dispatch(fetchAccountIdentityProofsRequest(accountId));
|
|
||||||
|
|
||||||
api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`)
|
|
||||||
.then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data)))
|
|
||||||
.catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsRequest = id => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS,
|
|
||||||
accountId,
|
|
||||||
identity_proofs,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchAccountIdentityProofsFail = (accountId, err) => ({
|
|
||||||
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
|
|
||||||
accountId,
|
|
||||||
err,
|
|
||||||
skipNotFound: true,
|
|
||||||
});
|
|
@ -54,9 +54,10 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
normalStatus.poll = status.poll.id;
|
normalStatus.poll = status.poll.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only calculate these values when status first encountered
|
// Only calculate these values when status first encountered and
|
||||||
// Otherwise keep the ones already in the reducer
|
// when the underlying values change. Otherwise keep the ones
|
||||||
if (normalOldStatus) {
|
// already in the reducer
|
||||||
|
if (normalOldStatus && normalOldStatus.get('content') === normalStatus.content && normalOldStatus.get('spoiler_text') === normalStatus.spoiler_text) {
|
||||||
normalStatus.search_index = normalOldStatus.get('search_index');
|
normalStatus.search_index = normalOldStatus.get('search_index');
|
||||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
normalStatus.contentHtml = normalOldStatus.get('contentHtml');
|
||||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
|
||||||
@ -73,7 +74,7 @@ export function normalizeStatus(status, normalOldStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const spoilerText = normalStatus.spoiler_text || '';
|
const spoilerText = normalStatus.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');
|
||||||
const emojiMap = makeEmojiMap(normalStatus);
|
const emojiMap = makeEmojiMap(normalStatus);
|
||||||
|
|
||||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
|
@ -134,6 +134,9 @@ export function deleteStatusFail(id, error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateStatus = status => dispatch =>
|
||||||
|
dispatch(importFetchedStatus(status));
|
||||||
|
|
||||||
export function fetchContext(id) {
|
export function fetchContext(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchContextRequest(id));
|
dispatch(fetchContextRequest(id));
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
fetchAnnouncements,
|
fetchAnnouncements,
|
||||||
updateAnnouncements,
|
updateAnnouncements,
|
||||||
@ -75,6 +76,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||||||
case 'update':
|
case 'update':
|
||||||
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
|
||||||
break;
|
break;
|
||||||
|
case 'status.update':
|
||||||
|
dispatch(updateStatus(JSON.parse(data.payload)));
|
||||||
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
|
116
app/javascript/mastodon/components/admin/Counter.js
Normal file
116
app/javascript/mastodon/components/admin/Counter.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { FormattedNumber } from 'react-intl';
|
||||||
|
import { Sparklines, SparklinesCurve } from 'react-sparklines';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
|
const percIncrease = (a, b) => {
|
||||||
|
let percent;
|
||||||
|
|
||||||
|
if (b !== 0) {
|
||||||
|
if (a !== 0) {
|
||||||
|
percent = (b - a) / a;
|
||||||
|
} else {
|
||||||
|
percent = 1;
|
||||||
|
}
|
||||||
|
} else if (b === 0 && a === 0) {
|
||||||
|
percent = 0;
|
||||||
|
} else {
|
||||||
|
percent = - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return percent;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Counter extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
measure: PropTypes.string.isRequired,
|
||||||
|
start_at: PropTypes.string.isRequired,
|
||||||
|
end_at: PropTypes.string.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
href: PropTypes.string,
|
||||||
|
params: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { measure, start_at, end_at, params } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/measures', { keys: [measure], start_at, end_at, [measure]: params }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { label, href } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className='sparkline__value__total'><Skeleton width={43} /></span>
|
||||||
|
<span className='sparkline__value__change'><Skeleton width={43} /></span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const measure = data[0];
|
||||||
|
const percentChange = percIncrease(measure.previous_total * 1, measure.total * 1);
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<React.Fragment>
|
||||||
|
<span className='sparkline__value__total'><FormattedNumber value={measure.total} /></span>
|
||||||
|
<span className={classNames('sparkline__value__change', { positive: percentChange > 0, negative: percentChange < 0 })}>{percentChange > 0 && '+'}<FormattedNumber value={percentChange} style='percent' /></span>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const inner = (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className='sparkline__value'>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sparkline__label'>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='sparkline__graph'>
|
||||||
|
{!loading && (
|
||||||
|
<Sparklines width={259} height={55} data={data[0].data.map(x => x.value * 1)}>
|
||||||
|
<SparklinesCurve />
|
||||||
|
</Sparklines>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href) {
|
||||||
|
return (
|
||||||
|
<a href={href} className='sparkline'>
|
||||||
|
{inner}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className='sparkline'>
|
||||||
|
{inner}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
93
app/javascript/mastodon/components/admin/Dimension.js
Normal file
93
app/javascript/mastodon/components/admin/Dimension.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { FormattedNumber } from 'react-intl';
|
||||||
|
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
|
export default class Dimension extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dimension: PropTypes.string.isRequired,
|
||||||
|
start_at: PropTypes.string.isRequired,
|
||||||
|
end_at: PropTypes.string.isRequired,
|
||||||
|
limit: PropTypes.number.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
params: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { start_at, end_at, dimension, limit, params } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/dimensions', { keys: [dimension], start_at, end_at, limit, [dimension]: params }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { label, limit } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Array.from(Array(limit)).map((_, i) => (
|
||||||
|
<tr className='dimension__item' key={i}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<Skeleton width={100} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
<Skeleton width={60} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const sum = data[0].data.reduce((sum, cur) => sum + (cur.value * 1), 0);
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{data[0].data.map(item => (
|
||||||
|
<tr className='dimension__item' key={item.key}>
|
||||||
|
<td className='dimension__item__key'>
|
||||||
|
<span className={`dimension__item__indicator dimension__item__indicator--${roundTo10(((item.value * 1) / sum) * 100)}`} />
|
||||||
|
<span title={item.key}>{item.human_key}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className='dimension__item__value'>
|
||||||
|
{typeof item.human_value !== 'undefined' ? item.human_value : <FormattedNumber value={item.value} />}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dimension'>
|
||||||
|
<h4>{label}</h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
159
app/javascript/mastodon/components/admin/ReportReasonSelector.js
Normal file
159
app/javascript/mastodon/components/admin/ReportReasonSelector.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
other: { id: 'report.categories.other', defaultMessage: 'Other' },
|
||||||
|
spam: { id: 'report.categories.spam', defaultMessage: 'Spam' },
|
||||||
|
violation: { id: 'report.categories.violation', defaultMessage: 'Content violates one or more server rules' },
|
||||||
|
});
|
||||||
|
|
||||||
|
class Category extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
onSelect: PropTypes.func,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { id, disabled, onSelect } = this.props;
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
onSelect(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { id, text, disabled, selected, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
|
||||||
|
{selected && <input type='hidden' name='report[category]' value={id} />}
|
||||||
|
|
||||||
|
<div className='report-reason-selector__category__label'>
|
||||||
|
<span className={classNames('poll__input', { active: selected, disabled })} />
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(selected && children) && (
|
||||||
|
<div className='report-reason-selector__category__rules'>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class Rule extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
text: PropTypes.string.isRequired,
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
onToggle: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { id, disabled, onToggle } = this.props;
|
||||||
|
|
||||||
|
if (!disabled) {
|
||||||
|
onToggle(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { id, text, disabled, selected } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
|
||||||
|
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
|
||||||
|
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default @injectIntl
|
||||||
|
class ReportReasonSelector extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
category: PropTypes.string.isRequired,
|
||||||
|
rule_ids: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
category: this.props.category,
|
||||||
|
rule_ids: this.props.rule_ids || [],
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
api().get('/api/v1/instance').then(res => {
|
||||||
|
this.setState({
|
||||||
|
rules: res.data.rules,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_save = () => {
|
||||||
|
const { id, disabled } = this.props;
|
||||||
|
const { category, rule_ids } = this.state;
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api().put(`/api/v1/admin/reports/${id}`, {
|
||||||
|
category,
|
||||||
|
rule_ids,
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelect = id => {
|
||||||
|
this.setState({ category: id }, () => this._save());
|
||||||
|
};
|
||||||
|
|
||||||
|
handleToggle = id => {
|
||||||
|
const { rule_ids } = this.state;
|
||||||
|
|
||||||
|
if (rule_ids.includes(id)) {
|
||||||
|
this.setState({ rule_ids: rule_ids.filter(x => x !== id ) }, () => this._save());
|
||||||
|
} else {
|
||||||
|
this.setState({ rule_ids: [...rule_ids, id] }, () => this._save());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { disabled, intl } = this.props;
|
||||||
|
const { rules, category, rule_ids } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='report-reason-selector'>
|
||||||
|
<Category id='other' text={intl.formatMessage(messages.other)} selected={category === 'other'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
|
<Category id='spam' text={intl.formatMessage(messages.spam)} selected={category === 'spam'} onSelect={this.handleSelect} disabled={disabled} />
|
||||||
|
<Category id='violation' text={intl.formatMessage(messages.violation)} selected={category === 'violation'} onSelect={this.handleSelect} disabled={disabled}>
|
||||||
|
{rules.map(rule => <Rule key={rule.id} id={rule.id} text={rule.text} selected={rule_ids.includes(rule.id)} onToggle={this.handleToggle} disabled={disabled} />)}
|
||||||
|
</Category>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
151
app/javascript/mastodon/components/admin/Retention.js
Normal file
151
app/javascript/mastodon/components/admin/Retention.js
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { FormattedMessage, FormattedNumber, FormattedDate } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { roundTo10 } from 'mastodon/utils/numbers';
|
||||||
|
|
||||||
|
const dateForCohort = cohort => {
|
||||||
|
switch(cohort.frequency) {
|
||||||
|
case 'day':
|
||||||
|
return <FormattedDate value={cohort.period} month='long' day='2-digit' />;
|
||||||
|
default:
|
||||||
|
return <FormattedDate value={cohort.period} month='long' year='numeric' />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class Retention extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
start_at: PropTypes.string,
|
||||||
|
end_at: PropTypes.string,
|
||||||
|
frequency: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { start_at, end_at, frequency } = this.props;
|
||||||
|
|
||||||
|
api().post('/api/v1/admin/retention', { start_at, end_at, frequency }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
const { frequency } = this.props;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<table className='retention__table'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div className='retention__table__date retention__table__label'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort' defaultMessage='Sign-up month' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<th>
|
||||||
|
<div className='retention__table__number retention__table__label'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.cohort_size' defaultMessage='New users' />
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => (
|
||||||
|
<th key={retention.date}>
|
||||||
|
<div className='retention__table__number retention__table__label'>
|
||||||
|
{i + 1}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__date retention__table__average'>
|
||||||
|
<FormattedMessage id='admin.dashboard.retention.average' defaultMessage='Average' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__size'>
|
||||||
|
<FormattedNumber value={data.reduce((sum, cohort, i) => sum + ((cohort.data[0].value * 1) - sum) / (i + 1), 0)} maximumFractionDigits={0} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{data[0].data.slice(1).map((retention, i) => {
|
||||||
|
const average = data.reduce((sum, cohort, k) => cohort.data[i + 1] ? sum + (cohort.data[i + 1].rate - sum)/(k + 1) : sum, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className={classNames('retention__table__box', 'retention__table__average', `retention__table__box--${roundTo10(average * 100)}`)}>
|
||||||
|
<FormattedNumber value={average} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{data.slice(0, -1).map(cohort => (
|
||||||
|
<tr key={cohort.period}>
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__date'>
|
||||||
|
{dateForCohort(cohort)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div className='retention__table__size'>
|
||||||
|
<FormattedNumber value={cohort.data[0].value} />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{cohort.data.slice(1).map(retention => (
|
||||||
|
<td key={retention.date}>
|
||||||
|
<div className={classNames('retention__table__box', `retention__table__box--${roundTo10(retention.rate * 100)}`)}>
|
||||||
|
<FormattedNumber value={retention.rate} style='percent' />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = null;
|
||||||
|
switch(frequency) {
|
||||||
|
case 'day':
|
||||||
|
title = <FormattedMessage id='admin.dashboard.daily_retention' defaultMessage='User retention rate by day after sign-up' />;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
title = <FormattedMessage id='admin.dashboard.monthly_retention' defaultMessage='User retention rate by month after sign-up' />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='retention'>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
73
app/javascript/mastodon/components/admin/Trends.js
Normal file
73
app/javascript/mastodon/components/admin/Trends.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Hashtag from 'mastodon/components/hashtag';
|
||||||
|
|
||||||
|
export default class Trends extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
limit: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { limit } = this.props;
|
||||||
|
|
||||||
|
api().get('/api/v1/admin/trends/tags', { params: { limit } }).then(res => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
data: res.data,
|
||||||
|
});
|
||||||
|
}).catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { limit } = this.props;
|
||||||
|
const { loading, data } = this.state;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
{Array.from(Array(limit)).map((_, i) => (
|
||||||
|
<Hashtag key={i} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<div>
|
||||||
|
{data.map(hashtag => (
|
||||||
|
<Hashtag
|
||||||
|
key={hashtag.name}
|
||||||
|
name={hashtag.name}
|
||||||
|
href={`/admin/tags/${hashtag.id}`}
|
||||||
|
people={hashtag.history[0].accounts * 1 + hashtag.history[1].accounts * 1}
|
||||||
|
uses={hashtag.history[0].uses * 1 + hashtag.history[1].uses * 1}
|
||||||
|
history={hashtag.history.reverse().map(day => day.uses)}
|
||||||
|
className={classNames(hashtag.requires_review && 'trends__item--requires-review', !hashtag.trendable && !hashtag.requires_review && 'trends__item--disabled')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='trends trends--compact'>
|
||||||
|
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,6 +6,8 @@ import PropTypes from 'prop-types';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
|
import Skeleton from 'mastodon/components/skeleton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
class SilentErrorBoundary extends React.Component {
|
class SilentErrorBoundary extends React.Component {
|
||||||
|
|
||||||
@ -47,45 +49,38 @@ const accountsCountRenderer = (displayNumber, pluralReady) => (
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Hashtag = ({ hashtag }) => (
|
export const ImmutableHashtag = ({ hashtag }) => (
|
||||||
<div className='trends__item'>
|
<Hashtag
|
||||||
<div className='trends__item__name'>
|
name={hashtag.get('name')}
|
||||||
<Permalink
|
|
||||||
href={hashtag.get('url')}
|
href={hashtag.get('url')}
|
||||||
to={`/tags/${hashtag.get('name')}`}
|
to={`/tags/${hashtag.get('name')}`}
|
||||||
>
|
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
|
||||||
#<span>{hashtag.get('name')}</span>
|
uses={hashtag.getIn(['history', 0, 'uses']) * 1 + hashtag.getIn(['history', 1, 'uses']) * 1}
|
||||||
|
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ImmutableHashtag.propTypes = {
|
||||||
|
hashtag: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Hashtag = ({ name, href, to, people, uses, history, className }) => (
|
||||||
|
<div className={classNames('trends__item', className)}>
|
||||||
|
<div className='trends__item__name'>
|
||||||
|
<Permalink href={href} to={to}>
|
||||||
|
{name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<ShortNumber
|
{typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
|
||||||
value={
|
|
||||||
hashtag.getIn(['history', 0, 'accounts']) * 1 +
|
|
||||||
hashtag.getIn(['history', 1, 'accounts']) * 1
|
|
||||||
}
|
|
||||||
renderer={accountsCountRenderer}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__current'>
|
<div className='trends__item__current'>
|
||||||
<ShortNumber
|
{typeof uses !== 'undefined' ? <ShortNumber value={uses} /> : <Skeleton width={42} height={36} />}
|
||||||
value={
|
|
||||||
hashtag.getIn(['history', 0, 'uses']) * 1 +
|
|
||||||
hashtag.getIn(['history', 1, 'uses']) * 1
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='trends__item__sparkline'>
|
<div className='trends__item__sparkline'>
|
||||||
<SilentErrorBoundary>
|
<SilentErrorBoundary>
|
||||||
<Sparklines
|
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
|
||||||
width={50}
|
|
||||||
height={28}
|
|
||||||
data={hashtag
|
|
||||||
.get('history')
|
|
||||||
.reverse()
|
|
||||||
.map((day) => day.get('uses'))
|
|
||||||
.toArray()}
|
|
||||||
>
|
|
||||||
<SparklinesCurve style={{ fill: 'none' }} />
|
<SparklinesCurve style={{ fill: 'none' }} />
|
||||||
</Sparklines>
|
</Sparklines>
|
||||||
</SilentErrorBoundary>
|
</SilentErrorBoundary>
|
||||||
@ -94,7 +89,13 @@ const Hashtag = ({ hashtag }) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
Hashtag.propTypes = {
|
Hashtag.propTypes = {
|
||||||
hashtag: ImmutablePropTypes.map.isRequired,
|
name: PropTypes.string,
|
||||||
|
href: PropTypes.string,
|
||||||
|
to: PropTypes.string,
|
||||||
|
people: PropTypes.number,
|
||||||
|
uses: PropTypes.number,
|
||||||
|
history: PropTypes.arrayOf(PropTypes.number),
|
||||||
|
className: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Hashtag;
|
export default Hashtag;
|
||||||
|
@ -12,8 +12,18 @@ import RelativeTimestamp from './relative_timestamp';
|
|||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
closed: {
|
||||||
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer', description: 'Tooltip of the "voted" checkmark in polls' },
|
id: 'poll.closed',
|
||||||
|
defaultMessage: 'Closed',
|
||||||
|
},
|
||||||
|
voted: {
|
||||||
|
id: 'poll.voted',
|
||||||
|
defaultMessage: 'You voted for this answer',
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
id: 'poll.votes',
|
||||||
|
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||||
@ -148,9 +158,16 @@ class Poll extends ImmutablePureComponent {
|
|||||||
data-index={optionIndex}
|
data-index={optionIndex}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showResults && <span className='poll__number'>
|
{showResults && (
|
||||||
|
<span
|
||||||
|
className='poll__number'
|
||||||
|
title={intl.formatMessage(messages.votes, {
|
||||||
|
votes: option.get('votes_count'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
{Math.round(percent)}%
|
{Math.round(percent)}%
|
||||||
</span>}
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className='poll__option__text translate'
|
className='poll__option__text translate'
|
||||||
|
11
app/javascript/mastodon/components/skeleton.js
Normal file
11
app/javascript/mastodon/components/skeleton.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const Skeleton = ({ width, height }) => <span className='skeleton' style={{ width, height }}>‌</span>;
|
||||||
|
|
||||||
|
Skeleton.propTypes = {
|
||||||
|
width: PropTypes.number,
|
||||||
|
height: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Skeleton;
|
@ -81,6 +81,7 @@ const messages = defineMessages({
|
|||||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||||
|
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpandedQuoteToggle, identity, media, router, contextType = 'home') => {
|
export const quote = (status, muted, quoteMuted, handleQuoteClick, handleExpandedQuoteToggle, identity, media, router, contextType = 'home') => {
|
||||||
@ -234,7 +235,11 @@ class Status extends ImmutablePureComponent {
|
|||||||
this.handleHotkeyOpen();
|
this.handleHotkeyOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAccountClick = e => {
|
handlePrependAccountClick = e => {
|
||||||
|
this.handleAccountClick(e, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAccountClick = (e, proper = true) => {
|
||||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -243,7 +248,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleHotkeyOpenProfile();
|
this._openProfile(proper);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleQuoteClick = () => {
|
handleQuoteClick = () => {
|
||||||
@ -345,8 +350,12 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleHotkeyOpenProfile = () => {
|
handleHotkeyOpenProfile = () => {
|
||||||
|
this._openProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
_openProfile = (proper = true) => {
|
||||||
const { router } = this.context;
|
const { router } = this.context;
|
||||||
const status = this._properStatus();
|
const status = proper ? this._properStatus() : this.props.status;
|
||||||
|
|
||||||
if (!router) {
|
if (!router) {
|
||||||
return;
|
return;
|
||||||
@ -449,7 +458,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></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> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} 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>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -595,7 +604,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
<a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
<a onClick={this.handleClick} href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||||
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
<span className='status__visibility-icon'><Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></span>
|
||||||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { hour12: false, year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{identity(status, account, otherAccounts, false)}
|
{identity(status, account, otherAccounts, false)}
|
||||||
|
@ -240,6 +240,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
@ -257,7 +258,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||||
|
|
||||||
if (writtenByMe && publicStatus) {
|
if (writtenByMe && pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,7 +306,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
if (isStaff) {
|
if (isStaff) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
26
app/javascript/mastodon/containers/admin_component.js
Normal file
26
app/javascript/mastodon/containers/admin_component.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { getLocale } from '../locales';
|
||||||
|
|
||||||
|
const { localeData, messages } = getLocale();
|
||||||
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
export default class AdminComponent extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locale, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
|
{children}
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,7 +7,7 @@ import { getLocale } from 'mastodon/locales';
|
|||||||
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
import { getScrollbarWidth } from 'mastodon/utils/scrollbar';
|
||||||
import MediaGallery from 'mastodon/components/media_gallery';
|
import MediaGallery from 'mastodon/components/media_gallery';
|
||||||
import Poll from 'mastodon/components/poll';
|
import Poll from 'mastodon/components/poll';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
import ModalRoot from 'mastodon/components/modal_root';
|
import ModalRoot from 'mastodon/components/modal_root';
|
||||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
||||||
import Video from 'mastodon/features/video';
|
import Video from 'mastodon/features/video';
|
||||||
|
@ -123,7 +123,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, domain, identity_proofs } = this.props;
|
const { account, intl, domain } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
@ -297,20 +297,8 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className='account__header__extra'>
|
<div className='account__header__extra'>
|
||||||
<div className='account__header__bio'>
|
<div className='account__header__bio'>
|
||||||
{(fields.size > 0 || identity_proofs.size > 0) && (
|
{fields.size > 0 && (
|
||||||
<div className='account__header__fields'>
|
<div className='account__header__fields'>
|
||||||
{identity_proofs.map((proof, i) => (
|
|
||||||
<dl key={i}>
|
|
||||||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
|
||||||
|
|
||||||
<dd className='verified'>
|
|
||||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
|
||||||
<Icon id='check' className='verified__mark' />
|
|
||||||
</span></a>
|
|
||||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
))}
|
|
||||||
{fields.map((pair, i) => (
|
{fields.map((pair, i) => (
|
||||||
<dl key={i}>
|
<dl key={i}>
|
||||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' />
|
||||||
|
@ -11,7 +11,6 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
identity_proofs: ImmutablePropTypes.list,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
@ -92,7 +91,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, hideTabs, identity_proofs } = this.props;
|
const { account, hideTabs } = this.props;
|
||||||
|
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
return null;
|
return null;
|
||||||
@ -104,7 +103,6 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<InnerHeader
|
<InnerHeader
|
||||||
account={account}
|
account={account}
|
||||||
identity_proofs={identity_proofs}
|
|
||||||
onFollow={this.handleFollow}
|
onFollow={this.handleFollow}
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
|
@ -21,7 +21,6 @@ import { openModal } from '../../../actions/modal';
|
|||||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { unfollowModal } from '../../../initial_state';
|
import { unfollowModal } from '../../../initial_state';
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
@ -34,7 +33,6 @@ const makeMapStateToProps = () => {
|
|||||||
const mapStateToProps = (state, { accountId }) => ({
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
account: getAccount(state, accountId),
|
account: getAccount(state, accountId),
|
||||||
domain: state.getIn(['meta', 'domain']),
|
domain: state.getIn(['meta', 'domain']),
|
||||||
identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -12,7 +12,6 @@ import ColumnBackButton from '../../components/column_back_button';
|
|||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { fetchAccountIdentityProofs } from '../../actions/identity_proofs';
|
|
||||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import TimelineHint from 'mastodon/components/timeline_hint';
|
import TimelineHint from 'mastodon/components/timeline_hint';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
@ -80,7 +79,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
const { accountId, withReplies, dispatch } = this.props;
|
const { accountId, withReplies, dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(fetchAccount(accountId));
|
dispatch(fetchAccount(accountId));
|
||||||
dispatch(fetchAccountIdentityProofs(accountId));
|
|
||||||
|
|
||||||
if (!withReplies) {
|
if (!withReplies) {
|
||||||
dispatch(expandAccountFeaturedTimeline(accountId));
|
dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
|
@ -60,6 +60,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
onPickEmoji: PropTypes.func.isRequired,
|
onPickEmoji: PropTypes.func.isRequired,
|
||||||
showSearch: PropTypes.bool,
|
showSearch: PropTypes.bool,
|
||||||
anyMedia: PropTypes.bool,
|
anyMedia: PropTypes.bool,
|
||||||
|
isInReply: PropTypes.bool,
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -149,7 +150,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
if (this.props.focusDate !== prevProps.focusDate) {
|
if (this.props.focusDate !== prevProps.focusDate) {
|
||||||
let selectionEnd, selectionStart;
|
let selectionEnd, selectionStart;
|
||||||
|
|
||||||
if (this.props.preselectDate !== prevProps.preselectDate) {
|
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
|
||||||
selectionEnd = this.props.text.length;
|
selectionEnd = this.props.text.length;
|
||||||
selectionStart = this.props.text.search(/\s/) + 1;
|
selectionStart = this.props.text.search(/\s/) + 1;
|
||||||
} else if (typeof this.props.caretPosition === 'number') {
|
} else if (typeof this.props.caretPosition === 'number') {
|
||||||
|
@ -5,7 +5,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
|||||||
import AccountContainer from '../../../containers/account_container';
|
import AccountContainer from '../../../containers/account_container';
|
||||||
import StatusContainer from '../../../containers/status_container';
|
import StatusContainer from '../../../containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Hashtag from '../../../components/hashtag';
|
import { ImmutableHashtag as Hashtag } from '../../../components/hashtag';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import { searchEnabled } from '../../../initial_state';
|
import { searchEnabled } from '../../../initial_state';
|
||||||
import LoadMore from 'mastodon/components/load_more';
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
|
@ -25,6 +25,7 @@ const mapStateToProps = state => ({
|
|||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
|
||||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
|
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Hashtag from 'mastodon/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
export default class Trends extends ImmutablePureComponent {
|
export default class Trends extends ImmutablePureComponent {
|
||||||
|
@ -33,8 +33,8 @@ class ColumnSettings extends React.PureComponent {
|
|||||||
tags (mode) {
|
tags (mode) {
|
||||||
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
let tags = this.props.settings.getIn(['tags', mode]) || [];
|
||||||
|
|
||||||
if (tags.toJSON) {
|
if (tags.toJS) {
|
||||||
return tags.toJSON();
|
return tags.toJS();
|
||||||
} else {
|
} else {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
@ -11,21 +11,22 @@ const mapStateToProps = (state, { columnId }) => {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { settings: columns.get(index).get('params') };
|
return {
|
||||||
|
settings: columns.get(index).get('params'),
|
||||||
|
onLoad (value) {
|
||||||
|
return api(() => state).get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
||||||
|
return (response.data.hashtags || []).map((tag) => {
|
||||||
|
return { value: tag.name, label: `#${tag.name}` };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { columnId }) => ({
|
const mapDispatchToProps = (dispatch, { columnId }) => ({
|
||||||
onChange (key, value) {
|
onChange (key, value) {
|
||||||
dispatch(changeColumnParams(columnId, key, value));
|
dispatch(changeColumnParams(columnId, key, value));
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad (value) {
|
|
||||||
return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
|
|
||||||
return (response.data.hashtags || []).map((tag) => {
|
|
||||||
return { value: tag.name, label: `#${tag.name}` };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);
|
||||||
|
@ -26,7 +26,8 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
||||||
|
|
||||||
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||||
|
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||||
@ -57,11 +58,11 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||||
<FormattedMessage id='notifications.column_settings.unread_markers.category' defaultMessage='Unread notification markers' />
|
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={filterShowStr} />
|
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -71,7 +72,7 @@ export default class ColumnSettings extends React.PureComponent {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterShowStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -196,6 +196,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
const { status, relationship, intl } = this.props;
|
const { status, relationship, intl } = this.props;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
const writtenByMe = status.getIn(['account', 'id']) === me;
|
const writtenByMe = status.getIn(['account', 'id']) === me;
|
||||||
@ -209,7 +210,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
if (writtenByMe) {
|
||||||
if (publicStatus) {
|
if (pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
@ -252,7 +253,7 @@ class ActionBar extends React.PureComponent {
|
|||||||
if (isStaff) {
|
if (isStaff) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
|
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses?id=${status.get('id')}` });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import { mapStateToProps, quote } from '../../../components/status';
|
|||||||
import StatusContent from '../../../components/status_content';
|
import StatusContent from '../../../components/status_content';
|
||||||
import MediaGallery from '../../../components/media_gallery';
|
import MediaGallery from '../../../components/media_gallery';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
|
import { injectIntl, defineMessages, FormattedDate, FormattedMessage } from 'react-intl';
|
||||||
import Card from './card';
|
import Card from './card';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Video from '../../video';
|
import Video from '../../video';
|
||||||
@ -135,6 +135,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
let reblogIcon = 'retweet';
|
let reblogIcon = 'retweet';
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
|
let edited = '';
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
if (this.props.measureHeight) {
|
||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
@ -277,6 +278,15 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.get('edited_at')) {
|
||||||
|
edited = (
|
||||||
|
<React.Fragment>
|
||||||
|
<React.Fragment> · </React.Fragment>
|
||||||
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(status.get('edited_at'), { hour12: false, month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={outerStyle}>
|
<div style={outerStyle}>
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
|
||||||
@ -291,7 +301,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
</a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user