Jay Logan 2020-04-08 22:18:04 -05:00
commit 10733881f3
460 changed files with 6382 additions and 3233 deletions

View File

@ -6,6 +6,7 @@ aliases:
- image: circleci/ruby:2.7-buster-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
BUNDLE_PATH: ./vendor/bundle/
DB_HOST: localhost
DB_USER: root
RAILS_ENV: test
@ -138,9 +139,10 @@ jobs:
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
steps:
- *attach_workspace
@ -157,9 +159,10 @@ jobs:
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps
@ -168,9 +171,10 @@ jobs:
docker:
- image: circleci/ruby:2.6-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps
@ -179,9 +183,10 @@ jobs:
docker:
- image: circleci/ruby:2.5-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
<<: *test_steps

View File

@ -1 +1 @@
2.6.5
2.6.6

View File

@ -3,6 +3,79 @@ Changelog
All notable changes to this project will be documented in this file.
## [v3.1.3] - 2020-04-05
### Added
- Add ability to filter audit log in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/13381))
- Add titles to warning presets in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/13252))
- Add option to include resolved DNS records when blacklisting e-mail domains in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/13254))
- Add ability to delete files uploaded for settings in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13192))
- Add sorting by username, creation and last activity in admin UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13076))
- Add explanation as to why unlocked accounts may have follow requests in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13385))
- Add link to bookmarks to dropdown in web UI ([mayaeh](https://github.com/tootsuite/mastodon/pull/13273))
- Add support for links to statuses in announcements to be opened in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13212), [ThibG](https://github.com/tootsuite/mastodon/pull/13250))
- Add tooltips to audio/video player buttons in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13203))
- Add submit button to the top of preferences pages ([guigeekz](https://github.com/tootsuite/mastodon/pull/13068))
- Add specific rate limits for posting, following and reporting ([Gargron](https://github.com/tootsuite/mastodon/pull/13172), [Gargron](https://github.com/tootsuite/mastodon/pull/13390))
- 300 posts every 3 hours
- 400 follows or follow requests every 24 hours
- 400 reports every 24 hours
- Add federation support for the "hide network" preference ([ThibG](https://github.com/tootsuite/mastodon/pull/11673))
- Add `--skip-media-remove` option to `tootctl statuses remove` ([tateisu](https://github.com/tootsuite/mastodon/pull/13080))
### Changed
- **Change design of polls in web UI** ([Sasha-Sorokin](https://github.com/tootsuite/mastodon/pull/13257), [ThibG](https://github.com/tootsuite/mastodon/pull/13313))
- Change status click areas in web UI to be bigger ([ariasuni](https://github.com/tootsuite/mastodon/pull/13327))
- **Change `tootctl media remove-orphans` to work for all classes** ([Gargron](https://github.com/tootsuite/mastodon/pull/13316))
- **Change local media attachments to perform heavy processing asynchronously** ([Gargron](https://github.com/tootsuite/mastodon/pull/13210))
- Change video uploads to always be converted to H264/MP4 ([Gargron](https://github.com/tootsuite/mastodon/pull/13220), [ThibG](https://github.com/tootsuite/mastodon/pull/13239), [ThibG](https://github.com/tootsuite/mastodon/pull/13242))
- Change video uploads to enforce certain limits ([Gargron](https://github.com/tootsuite/mastodon/pull/13218))
- Dimensions smaller than 1920x1200px
- Frame rate at most 60fps
- Change the tooltip "Toggle visibility" to "Hide media" in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13199))
- Change description of privacy levels to be more intuitive in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13197))
- Change GIF label to be displayed even when autoplay is enabled in web UI ([koyuawsmbrtn](https://github.com/tootsuite/mastodon/pull/13209))
- Change the string "Hide everything from …" to "Block domain …" in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13178), [mayaeh](https://github.com/tootsuite/mastodon/pull/13221))
- Change wording of media display preferences to be more intuitive ([ariasuni](https://github.com/tootsuite/mastodon/pull/13198))
### Deprecated
- `POST /api/v1/media``POST /api/v2/media` ([Gargron](https://github.com/tootsuite/mastodon/pull/13210))
### Fixed
- Fix `tootctl media remove-orphans` ignoring `PAPERCLIP_ROOT_PATH` ([Gargron](https://github.com/tootsuite/mastodon/pull/13375))
- Fix returning results when searching for URL with non-zero offset ([Gargron](https://github.com/tootsuite/mastodon/pull/13377))
- Fix pinning a column in web UI sometimes redirecting out of web UI ([Gargron](https://github.com/tootsuite/mastodon/pull/13376))
- Fix background jobs not using locks like they are supposed to ([Gargron](https://github.com/tootsuite/mastodon/pull/13361))
- Fix content warning being unnecessarily cleared when hiding content warning input in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13348))
- Fix "Show more" not switching to "Show less" on public pages ([ThibG](https://github.com/tootsuite/mastodon/pull/13174))
- Fix import overwrite option not being selectable ([noellabo](https://github.com/tootsuite/mastodon/pull/13347))
- Fix wrong color for ellipsis in boost confirmation dialog in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13355))
- Fix unnecessary unfollowing when importing follows with overwrite option ([noellabo](https://github.com/tootsuite/mastodon/pull/13350))
- Fix 404 and 410 API errors being silently discarded in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13279))
- Fix OCR not working on Safari because of unsupported worker-src CSP ([ThibG](https://github.com/tootsuite/mastodon/pull/13323))
- Fix media not being marked sensitive when a content warning is set with no text ([ThibG](https://github.com/tootsuite/mastodon/pull/13277))
- Fix crash after deleting announcements in web UI ([codesections](https://github.com/tootsuite/mastodon/pull/13283), [ThibG](https://github.com/tootsuite/mastodon/pull/13312))
- Fix bookmarks not being searchable ([Kjwon15](https://github.com/tootsuite/mastodon/pull/13271), [noellabo](https://github.com/tootsuite/mastodon/pull/13293))
- Fix reported accounts not being whitelisted from further spam checks when resolving a spam check report ([ThibG](https://github.com/tootsuite/mastodon/pull/13289))
- Fix web UI crash in single-column mode on prehistoric browsers ([ThibG](https://github.com/tootsuite/mastodon/pull/13267))
- Fix some timeouts when searching for URLs ([ThibG](https://github.com/tootsuite/mastodon/pull/13253))
- Fix detailed view of direct messages displaying a 0 boost count in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13244))
- Fix regression in “Edit media” modal in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13243))
- Fix public posts from silenced accounts not being changed to unlisted visibility ([ThibG](https://github.com/tootsuite/mastodon/pull/13096))
- Fix error when searching for URLs that contain the mention syntax ([ThibG](https://github.com/tootsuite/mastodon/pull/13151))
- Fix text area above/right of emoji picker being accidentally clickable in web UI ([ariasuni](https://github.com/tootsuite/mastodon/pull/13148))
- Fix too large announcements not being scrollable in web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13211))
- Fix `tootctl media remove-orphans` crashing when encountering invalid media ([ThibG](https://github.com/tootsuite/mastodon/pull/13170))
- Fix installation failing when Redis password contains special characters ([ThibG](https://github.com/tootsuite/mastodon/pull/13156))
- Fix announcements with fully-qualified mentions to local users crashing web UI ([ThibG](https://github.com/tootsuite/mastodon/pull/13164))
### Security
- Fix re-sending of e-mail confirmation not being rate limited ([Gargron](https://github.com/tootsuite/mastodon/pull/13360))
## [v3.1.2] - 2020-02-27
### Added

View File

@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
SHELL ["bash", "-c"]
# Install Node v12 (LTS)
ENV NODE_VER="12.14.0"
ENV NODE_VER="12.16.1"
RUN ARCH= && \
dpkgArch="$(dpkg --print-architecture)" && \
case "${dpkgArch##*-}" in \
@ -38,8 +38,8 @@ RUN apt update && \
make -j$(nproc) > /dev/null && \
make install_bin install_include install_lib
# Install ruby
ENV RUBY_VER="2.6.5"
# Install Ruby
ENV RUBY_VER="2.6.6"
ENV CPPFLAGS="-I/opt/jemalloc/include"
ENV LDFLAGS="-L/opt/jemalloc/lib/"
RUN apt update && \

32
Gemfile
View File

@ -1,12 +1,12 @@
# frozen_string_literal: true
source 'https://rubygems.org'
ruby '>= 2.4.0', '< 3.0.0'
ruby '>= 2.5.0', '< 3.0.0'
gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.4'
gem 'rails', '~> 5.2.4.2'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20'
gem 'rack', '~> 2.2.2'
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7'
gem 'aws-sdk-s3', '~> 1.60', require: false
gem 'aws-sdk-s3', '~> 1.61', require: false
gem 'fog-core', '<= 2.1.0'
gem 'fog-openstack', '~> 0.3', require: false
gem 'paperclip', '~> 6.0'
@ -35,7 +35,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.6'
gem 'cld3', '~> 3.3.0'
gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
@ -48,8 +48,8 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
gem 'discard', '~> 1.1'
gem 'doorkeeper', '~> 5.2'
gem 'discard', '~> 1.2'
gem 'doorkeeper', '~> 5.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'goldfinger', '~> 2.1'
@ -69,7 +69,7 @@ gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.10'
gem 'ox', '~> 2.12'
gem 'ox', '~> 2.13'
gem 'parslet'
gem 'parallel', '~> 1.19'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
@ -84,7 +84,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.1'
gem 'sidekiq', '~> 5.2'
gem 'sidekiq', '~> 6.0'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
gem 'sidekiq-bulk', '~>0.2.0'
@ -92,9 +92,9 @@ gem 'simple-navigation', '~> 4.1'
gem 'simple_form', '~> 5.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.2.0'
gem 'strong_migrations', '~> 0.5'
gem 'strong_migrations', '~> 0.6'
gem 'tty-command', '~> 0.9', require: false
gem 'tty-prompt', '~> 0.20', require: false
gem 'tty-prompt', '~> 0.21', require: false
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2019'
gem 'webpacker', '~> 4.2'
@ -112,7 +112,7 @@ group :development, :test do
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-byebug', '~> 3.8'
gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.9'
gem 'rspec-rails', '~> 4.0'
end
group :production, :test do
@ -122,19 +122,19 @@ end
group :test do
gem 'capybara', '~> 3.31'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.10'
gem 'faker', '~> 2.11'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.18', require: false
gem 'webmock', '~> 3.8'
gem 'parallel_tests', '~> 2.30'
gem 'parallel_tests', '~> 2.32'
end
group :development do
gem 'active_record_query_trace', '~> 1.7'
gem 'annotate', '~> 3.0'
gem 'better_errors', '~> 2.5'
gem 'better_errors', '~> 2.6'
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 6.1'
gem 'letter_opener', '~> 1.7'
@ -142,10 +142,10 @@ group :development do
gem 'memory_profiler'
gem 'rubocop', '~> 0.79', require: false
gem 'rubocop-rails', '~> 2.4', require: false
gem 'brakeman', '~> 4.7', require: false
gem 'brakeman', '~> 4.8', require: false
gem 'bundler-audit', '~> 0.6', require: false
gem 'capistrano', '~> 3.11'
gem 'capistrano', '~> 3.12'
gem 'capistrano-rails', '~> 1.4'
gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0'

View File

@ -31,25 +31,25 @@ GIT
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.4.1)
actionpack (= 5.2.4.1)
actioncable (5.2.4.2)
actionpack (= 5.2.4.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.4.1)
actionpack (= 5.2.4.1)
actionview (= 5.2.4.1)
activejob (= 5.2.4.1)
actionmailer (5.2.4.2)
actionpack (= 5.2.4.2)
actionview (= 5.2.4.2)
activejob (= 5.2.4.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.4.1)
actionview (= 5.2.4.1)
activesupport (= 5.2.4.1)
actionpack (5.2.4.2)
actionview (= 5.2.4.2)
activesupport (= 5.2.4.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.4.1)
activesupport (= 5.2.4.1)
actionview (5.2.4.2)
activesupport (= 5.2.4.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -60,20 +60,20 @@ GEM
case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.7)
activejob (5.2.4.1)
activesupport (= 5.2.4.1)
activejob (5.2.4.2)
activesupport (= 5.2.4.2)
globalid (>= 0.3.6)
activemodel (5.2.4.1)
activesupport (= 5.2.4.1)
activerecord (5.2.4.1)
activemodel (= 5.2.4.1)
activesupport (= 5.2.4.1)
activemodel (5.2.4.2)
activesupport (= 5.2.4.2)
activerecord (5.2.4.2)
activemodel (= 5.2.4.2)
activesupport (= 5.2.4.2)
arel (>= 9.0)
activestorage (5.2.4.1)
actionpack (= 5.2.4.1)
activerecord (= 5.2.4.1)
activestorage (5.2.4.2)
actionpack (= 5.2.4.2)
activerecord (= 5.2.4.2)
marcel (~> 0.3.1)
activesupport (5.2.4.1)
activesupport (5.2.4.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -92,23 +92,23 @@ GEM
av (0.9.0)
cocaine (~> 0.5.3)
aws-eventstream (1.0.3)
aws-partitions (1.261.0)
aws-sdk-core (3.86.0)
aws-partitions (1.286.0)
aws-sdk-core (3.92.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.27.0)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.1)
aws-sdk-s3 (1.61.1)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-sigv4 (1.1.1)
aws-eventstream (~> 1.0, >= 1.0.2)
bcrypt (3.1.12)
better_errors (2.5.1)
better_errors (2.6.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@ -116,10 +116,10 @@ GEM
debug_inspector (>= 0.0.1)
blurhash (0.1.4)
ffi (~> 1.10.0)
bootsnap (1.4.5)
bootsnap (1.4.6)
msgpack (~> 1.0)
brakeman (4.7.2)
browser (3.0.3)
brakeman (4.8.0)
browser (4.0.0)
builder (3.2.4)
bullet (6.1.0)
activesupport (>= 3.0.0)
@ -128,7 +128,7 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.1.1)
capistrano (3.11.2)
capistrano (3.12.1)
airbrussh (>= 1.0.0)
i18n
rake (>= 10.0.0)
@ -160,13 +160,13 @@ GEM
elasticsearch (>= 2.0.0)
elasticsearch-dsl
chunky_png (1.3.11)
cld3 (3.2.6)
cld3 (3.3.0)
ffi (>= 1.1.0, < 1.12.0)
climate_control (0.2.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coderay (1.1.2)
concurrent-ruby (1.1.5)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@ -190,12 +190,12 @@ GEM
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.3)
discard (1.1.0)
discard (1.2.0)
activerecord (>= 4.2, < 7)
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.2.3)
doorkeeper (5.3.1)
railties (>= 5)
dotenv (2.7.5)
dotenv-rails (2.7.5)
@ -214,11 +214,11 @@ GEM
encryptor (3.0.0)
equatable (0.6.1)
erubi (1.9.0)
et-orbi (1.1.6)
et-orbi (1.2.3)
tzinfo
excon (0.71.0)
fabrication (2.21.0)
faker (2.10.1)
faker (2.11.0)
i18n (>= 1.6, < 2)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
@ -241,8 +241,8 @@ GEM
fog-json (>= 1.0)
ipaddress (>= 0.8)
formatador (0.2.5)
fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6)
fugit (1.3.3)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.1)
fuubar (2.5.0)
rspec-core (~> 3.0)
@ -265,8 +265,8 @@ GEM
railties (>= 4.0.1)
hamster (3.0.0)
concurrent-ruby (~> 1.0)
hashdiff (1.0.0)
hashie (3.6.0)
hashdiff (1.0.1)
hashie (4.1.0)
highline (2.0.3)
hiredis (0.6.3)
hkdf (0.3.0)
@ -287,7 +287,7 @@ GEM
rainbow (>= 2.0.0)
i18n (1.8.2)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.30)
i18n-tasks (0.9.31)
activesupport (>= 4.0.2)
ast (>= 2.1.0)
erubi
@ -299,19 +299,19 @@ GEM
terminal-table (>= 1.5.1)
idn-ruby (0.1.0)
ipaddress (0.8.3)
iso-639 (0.2.8)
iso-639 (0.3.5)
jaro_winkler (1.5.4)
jmespath (1.4.0)
json (2.3.0)
json-canonicalization (0.2.0)
json-ld (3.1.0)
json-ld (3.1.2)
htmlentities (~> 4.3)
json-canonicalization (~> 0.1)
json-canonicalization (~> 0.2)
link_header (~> 0.0, >= 0.0.8)
multi_json (~> 1.14)
rack (~> 2.0)
rdf (~> 3.1)
json-ld-preloaded (3.1.0)
json-ld-preloaded (3.1.2)
json-ld (~> 3.1)
rdf (~> 3.1)
jsonapi-renderer (0.2.2)
@ -361,11 +361,11 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.1009)
mimemagic (0.3.3)
mimemagic (0.3.4)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
msgpack (1.3.1)
msgpack (1.3.3)
multi_json (1.14.1)
multipart-post (2.1.1)
necromancer (0.5.1)
@ -374,7 +374,7 @@ GEM
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
nio4r (2.5.2)
nokogiri (1.10.8)
nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
nokogumbo (2.0.1)
nokogiri (~> 1.8, >= 1.8.4)
@ -383,9 +383,9 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.10.1)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
oj (3.10.5)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-cas (1.1.1)
addressable (~> 2.3)
@ -395,7 +395,7 @@ GEM
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
orm_adapter (0.5.0)
ox (2.12.1)
ox (2.13.2)
paperclip (6.0.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
@ -406,15 +406,15 @@ GEM
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.19.1)
parallel_tests (2.30.1)
parallel_tests (2.32.0)
parallel
parser (2.7.0.2)
parser (2.7.0.5)
ast (~> 2.4.0)
parslet (1.8.2)
pastel (0.7.3)
equatable (~> 0.6)
tty-color (~> 0.5)
pg (1.2.2)
pg (1.2.3)
pghero (2.4.1)
activerecord (>= 5)
pkg-config (1.4.1)
@ -445,24 +445,24 @@ GEM
rack (>= 1.0, < 3)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-protection (2.0.7)
rack-protection (2.0.8.1)
rack
rack-proxy (0.6.5)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.4.1)
actioncable (= 5.2.4.1)
actionmailer (= 5.2.4.1)
actionpack (= 5.2.4.1)
actionview (= 5.2.4.1)
activejob (= 5.2.4.1)
activemodel (= 5.2.4.1)
activerecord (= 5.2.4.1)
activestorage (= 5.2.4.1)
activesupport (= 5.2.4.1)
rails (5.2.4.2)
actioncable (= 5.2.4.2)
actionmailer (= 5.2.4.2)
actionpack (= 5.2.4.2)
actionview (= 5.2.4.2)
activejob (= 5.2.4.2)
activemodel (= 5.2.4.2)
activerecord (= 5.2.4.2)
activestorage (= 5.2.4.2)
activesupport (= 5.2.4.2)
bundler (>= 1.3.0)
railties (= 5.2.4.1)
railties (= 5.2.4.2)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@ -478,9 +478,9 @@ GEM
railties (>= 5.0, < 6)
rails-settings-cached (0.6.6)
rails (>= 4.2.0)
railties (5.2.4.1)
actionpack (= 5.2.4.1)
activesupport (= 5.2.4.1)
railties (5.2.4.2)
actionpack (= 5.2.4.2)
activesupport (= 5.2.4.2)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
@ -523,26 +523,26 @@ GEM
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.1)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (3.9.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0)
rspec-support (3.9.0)
rspec-support (3.9.2)
rubocop (0.79.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
@ -556,38 +556,40 @@ GEM
ruby-progressbar (1.10.1)
ruby-saml (1.9.0)
nokogiri (>= 1.5.10)
rufus-scheduler (3.5.2)
fugit (~> 1.1, >= 1.1.5)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)
safe_yaml (1.0.5)
sanitize (5.1.0)
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
sidekiq (5.2.7)
connection_pool (~> 2.2, >= 2.2.2)
rack (>= 1.5.0)
rack-protection (>= 1.5.0)
redis (>= 3.3.5, < 5)
sidekiq (6.0.4)
connection_pool (>= 2.2.2)
rack (>= 2.0.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
sidekiq-bulk (0.2.0)
sidekiq
sidekiq-scheduler (3.0.0)
sidekiq-scheduler (3.0.1)
e2mmap
redis (>= 3, < 5)
rufus-scheduler (~> 3.2)
sidekiq (>= 3)
thwait
tilt (>= 1.4.0)
sidekiq-unique-jobs (6.0.18)
sidekiq-unique-jobs (6.0.21)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 4.0, < 7.0)
thor (~> 0)
simple-navigation (4.1.0)
activesupport (>= 2.3.2)
simple_form (5.0.1)
simple_form (5.0.2)
actionpack (>= 5.0)
activemodel (>= 5.0)
simplecov (0.18.2)
simplecov (0.18.5)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov-html (0.12.0)
simplecov-html (0.12.2)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -595,7 +597,7 @@ GEM
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.20.0)
sshkit (1.21.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.15)
@ -603,7 +605,7 @@ GEM
stoplight (2.2.0)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.5.1)
strong_migrations (0.6.2)
activerecord (>= 5)
temple (0.8.2)
terminal-table (1.8.0)
@ -614,11 +616,11 @@ GEM
thread_safe (0.3.6)
thwait (0.1.0)
tilt (2.0.10)
tty-color (0.5.0)
tty-color (0.5.1)
tty-command (0.9.0)
pastel (~> 0.7.0)
tty-cursor (0.7.0)
tty-prompt (0.20.0)
tty-cursor (0.7.1)
tty-prompt (0.21.0)
necromancer (~> 0.5.0)
pastel (~> 0.7.0)
tty-reader (~> 0.7.0)
@ -626,10 +628,10 @@ GEM
tty-cursor (~> 0.7)
tty-screen (~> 0.7)
wisper (~> 2.0.0)
tty-screen (0.7.0)
tty-screen (0.7.1)
twitter-text (1.14.7)
unf (~> 0.1.0)
tzinfo (1.2.6)
tzinfo (1.2.7)
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
@ -640,7 +642,7 @@ GEM
uniform_notifier (1.13.0)
warden (1.2.8)
rack (>= 2.0.6)
webmock (3.8.0)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -666,35 +668,35 @@ DEPENDENCIES
active_record_query_trace (~> 1.7)
addressable (~> 2.7)
annotate (~> 3.0)
aws-sdk-s3 (~> 1.60)
better_errors (~> 2.5)
aws-sdk-s3 (~> 1.61)
better_errors (~> 2.6)
binding_of_caller (~> 0.7)
blurhash (~> 0.1)
bootsnap (~> 1.4)
brakeman (~> 4.7)
brakeman (~> 4.8)
browser
bullet (~> 6.1)
bundler-audit (~> 0.6)
capistrano (~> 3.11)
capistrano (~> 3.12)
capistrano-rails (~> 1.4)
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 3.31)
charlock_holmes (~> 0.7.7)
chewy (~> 5.1)
cld3 (~> 3.2.6)
cld3 (~> 3.3.0)
climate_control (~> 0.2)
concurrent-ruby
connection_pool
devise (~> 4.7)
devise-two-factor (~> 3.1)
devise_pam_authenticatable2 (~> 9.2)
discard (~> 1.1)
doorkeeper (~> 5.2)
discard (~> 1.2)
doorkeeper (~> 5.3)
dotenv-rails (~> 2.7)
e2mmap (~> 0.1.0)
fabrication (~> 2.21)
faker (~> 2.10)
faker (~> 2.11)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -732,11 +734,11 @@ DEPENDENCIES
omniauth (~> 1.9)
omniauth-cas (~> 1.1)
omniauth-saml (~> 1.10)
ox (~> 2.12)
ox (~> 2.13)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.19)
parallel_tests (~> 2.30)
parallel_tests (~> 2.32)
parslet
pg (~> 1.2)
pghero (~> 2.4)
@ -751,7 +753,7 @@ DEPENDENCIES
rack (~> 2.2.2)
rack-attack (~> 6.2)
rack-cors (~> 1.1)
rails (~> 5.2.4)
rails (~> 5.2.4.2)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1)
rails-settings-cached (~> 0.6)
@ -761,13 +763,13 @@ DEPENDENCIES
redis-namespace (~> 1.7)
redis-rails (~> 5.0)
rqrcode (~> 1.1)
rspec-rails (~> 3.9)
rspec-rails (~> 4.0)
rspec-sidekiq (~> 3.0)
rubocop (~> 0.79)
rubocop-rails (~> 2.4)
ruby-progressbar (~> 1.10)
sanitize (~> 5.1)
sidekiq (~> 5.2)
sidekiq (~> 6.0)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
sidekiq-unique-jobs (~> 6.0)
@ -779,11 +781,11 @@ DEPENDENCIES
stackprof
stoplight (~> 2.2.0)
streamio-ffmpeg (~> 3.0)
strong_migrations (~> 0.5)
strong_migrations (~> 0.6)
thor (~> 0.20)
thwait (~> 0.1.0)
tty-command (~> 0.9)
tty-prompt (~> 0.20)
tty-prompt (~> 0.21)
twitter-text (~> 1.14)
tzinfo-data (~> 1.2019)
webmock (~> 3.8)

2
Vagrantfile vendored
View File

@ -91,7 +91,7 @@ VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "ubuntu/xenial64"
config.vm.box = "ubuntu/bionic64"
config.vm.provider :virtualbox do |vb|
vb.name = "mastodon"

View File

@ -47,6 +47,11 @@ class StatusesIndex < Chewy::Index
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
crutch :bookmarks do |collection|
data = ::Bookmark.where(status_id: collection.map(&:id)).where(account: Account.local).pluck(:status_id, :account_id)
data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
end
root date_detection: false do
field :id, type: 'long'
field :account_id, type: 'long'

View File

@ -6,7 +6,7 @@ class AccountFollowController < ApplicationController
before_action :authenticate_user!
def create
FollowService.new.call(current_user.account, @account.acct)
FollowService.new.call(current_user.account, @account, with_rate_limit: true)
redirect_to account_path(@account)
end
end

View File

@ -2,8 +2,18 @@
module Admin
class ActionLogsController < BaseController
def index
@action_logs = Admin::ActionLog.page(params[:page])
before_action :set_action_logs
def index; end
private
def set_action_logs
@action_logs = Admin::ActionLogFilter.new(filter_params).results.page(params[:page])
end
def filter_params
params.slice(:page, *Admin::ActionLogFilter::KEYS).permit(:page, *Admin::ActionLogFilter::KEYS)
end
end
end

View File

@ -6,12 +6,12 @@ module Admin
def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page])
@email_domain_blocks = EmailDomainBlock.where(parent_id: nil).includes(:children).order(id: :desc).page(params[:page])
end
def new
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new
@email_domain_block = EmailDomainBlock.new(domain: params[:_domain])
end
def create
@ -21,6 +21,28 @@ module Admin
if @email_domain_block.save
log_action :create, @email_domain_block
if @email_domain_block.with_dns_records?
hostnames = []
ips = []
Resolv::DNS.open do |dns|
dns.timeouts = 1
hostnames = dns.getresources(@email_domain_block.domain, Resolv::DNS::Resource::IN::MX).to_a.map { |e| e.exchange.to_s }
([@email_domain_block.domain] + hostnames).uniq.each do |hostname|
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::A).to_a.map { |e| e.address.to_s })
ips.concat(dns.getresources(hostname, Resolv::DNS::Resource::IN::AAAA).to_a.map { |e| e.address.to_s })
end
end
(hostnames + ips).each do |hostname|
another_email_domain_block = EmailDomainBlock.new(domain: hostname, parent: @email_domain_block)
log_action :create, another_email_domain_block if another_email_domain_block.save
end
end
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else
render :new
@ -41,7 +63,7 @@ module Admin
end
def resource_params
params.require(:email_domain_block).permit(:domain)
params.require(:email_domain_block).permit(:domain, :with_dns_records)
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
module Admin
class SiteUploadsController < BaseController
before_action :set_site_upload
def destroy
authorize :settings, :destroy?
@site_upload.destroy!
redirect_to edit_admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private
def set_site_upload
@site_upload = SiteUpload.find(params[:id])
end
end
end

View File

@ -7,7 +7,7 @@ module Admin
def index
authorize :account_warning_preset, :index?
@warning_presets = AccountWarningPreset.all
@warning_presets = AccountWarningPreset.alphabetic
@warning_preset = AccountWarningPreset.new
end
@ -19,7 +19,7 @@ module Admin
if @warning_preset.save
redirect_to admin_warning_presets_path
else
@warning_presets = AccountWarningPreset.all
@warning_presets = AccountWarningPreset.alphabetic
render :index
end
end
@ -52,7 +52,7 @@ module Admin
end
def warning_preset_params
params.require(:account_warning_preset).permit(:text)
params.require(:account_warning_preset).permit(:title, :text)
end
end
end

View File

@ -44,6 +44,10 @@ class Api::BaseController < ApplicationController
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
end
rescue_from Mastodon::RateLimitExceededError do
render json: { error: I18n.t('errors.429') }, status: 429
end
rescue_from ActionController::ParameterMissing do |e|
render json: { error: e.to_s }, status: 400
end

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
before_action :set_account
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
@ -27,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
end
def hide_results?
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
(@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
before_action :set_account
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
@ -27,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end
def hide_results?
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
(@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
before_action :require_user!
before_action :set_account
respond_to :json
def index
@proofs = @account.identity_proofs.active
render json: @proofs, each_serializer: REST::IdentityProofSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::Accounts::ListsController < Api::BaseController
before_action :require_user!
before_action :set_account
respond_to :json
def index
@lists = @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Accounts::PinsController < Api::BaseController
before_action :require_user!
before_action :set_account
respond_to :json
def create
AccountPin.create!(account: current_account, target_account: @account)
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:follows' }
before_action :require_user!
respond_to :json
def index
accounts = Account.where(id: account_ids).select('id')
# .where doesn't guarantee that our results are in the same order

View File

@ -4,8 +4,6 @@ class Api::V1::Accounts::SearchController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user!
respond_to :json
def show
@accounts = account_search
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -6,8 +6,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
respond_to :json
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -14,7 +14,7 @@ class Api::V1::AccountsController < Api::BaseController
skip_before_action :require_authenticated_user!, only: :create
respond_to :json
override_rate_limit_headers :follow, family: :follows
def show
render json: @account, serializer: REST::AccountSerializer
@ -31,7 +31,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def follow
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs))
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }

View File

@ -3,8 +3,6 @@
class Api::V1::Apps::CredentialsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
respond_to :json
def show
render json: doorkeeper_token.application, serializer: REST::ApplicationSerializer, fields: %i(name website vapid_key)
end

View File

@ -5,8 +5,6 @@ class Api::V1::BlocksController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::BookmarksController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -9,8 +9,6 @@ class Api::V1::ConversationsController < Api::BaseController
before_action :set_conversation, except: :index
after_action :insert_pagination_headers, only: :index
respond_to :json
def index
@conversations = paginated_conversations
render json: @conversations, each_serializer: REST::ConversationSerializer

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::V1::CustomEmojisController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers
def index

View File

@ -8,8 +8,6 @@ class Api::V1::DomainBlocksController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers, only: :show
respond_to :json
def show
@blocks = load_domain_blocks
render json: @blocks.map(&:domain)

View File

@ -5,8 +5,6 @@ class Api::V1::EndorsementsController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -5,8 +5,6 @@ class Api::V1::FavouritesController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -2,12 +2,9 @@
class Api::V1::FeaturedTags::SuggestionsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action :require_user!
before_action :set_most_used_tags, only: :index
respond_to :json
def index
render json: @most_used_tags, each_serializer: REST::TagSerializer
end

View File

@ -7,8 +7,6 @@ class Api::V1::FiltersController < Api::BaseController
before_action :set_filters, only: :index
before_action :set_filter, only: [:show, :update, :destroy]
respond_to :json
def index
render json: @filters, each_serializer: REST::FilterSerializer
end

View File

@ -6,8 +6,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
def show
expires_in 1.day, public: true
render_with_cache json: :activity, expires_in: 1.day

View File

@ -6,8 +6,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
respond_to :json
def index
expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains }

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?

View File

@ -3,27 +3,42 @@
class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user!
respond_to :json
before_action :set_media_attachment, except: [:create]
before_action :check_processing, except: [:create]
def create
@media = current_account.media_attachments.create!(media_params)
render json: @media, serializer: REST::MediaAttachmentSerializer
@media_attachment = current_account.media_attachments.create!(media_attachment_params)
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
rescue Paperclip::Error
render json: processing_error, status: 500
end
def show
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
def update
@media = current_account.media_attachments.where(status_id: nil).find(params[:id])
@media.update!(media_params)
render json: @media, serializer: REST::MediaAttachmentSerializer
@media_attachment.update!(media_attachment_params)
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: status_code_for_media_attachment
end
private
def media_params
def status_code_for_media_attachment
@media_attachment.not_processed? ? 206 : 200
end
def set_media_attachment
@media_attachment = current_account.media_attachments.unattached.find(params[:id])
end
def check_processing
render json: processing_error, status: 422 if @media_attachment.processing_failed?
end
def media_attachment_params
params.permit(:file, :description, :focus)
end

View File

@ -5,8 +5,6 @@ class Api::V1::MutesController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers
respond_to :json
def index
@data = @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -6,8 +6,6 @@ class Api::V1::NotificationsController < Api::BaseController
before_action :require_user!
after_action :insert_pagination_headers, only: :index
respond_to :json
DEFAULT_NOTIFICATIONS_LIMIT = 15
def index

View File

@ -7,8 +7,6 @@ class Api::V1::Polls::VotesController < Api::BaseController
before_action :require_user!
before_action :set_poll
respond_to :json
def create
VoteService.new.call(current_account, @poll, vote_params[:choices])
render json: @poll, serializer: REST::PollSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::PollsController < Api::BaseController
before_action :set_poll
before_action :refresh_poll
respond_to :json
def show
render json: @poll, serializer: REST::PollSerializer, include_results: true
end

View File

@ -4,8 +4,6 @@ class Api::V1::PreferencesController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }
before_action :require_user!
respond_to :json
def index
render json: current_account, serializer: REST::PreferencesSerializer
end

View File

@ -4,7 +4,7 @@ class Api::V1::ReportsController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
before_action :require_user!
respond_to :json
override_rate_limit_headers :create, family: :reports
def create
@report = ReportService.new.call(

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController
before_action :require_user!
before_action :set_status
respond_to :json
def create
current_account.bookmarks.find_or_create_by!(account: current_account, status: @status)
render json: @status, serializer: REST::StatusSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
before_action :set_status
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController
before_action :require_user!
before_action :set_status
respond_to :json
def create
FavouriteService.new.call(current_account, @status)
render json: @status, serializer: REST::StatusSerializer

View File

@ -8,8 +8,6 @@ class Api::V1::Statuses::MutesController < Api::BaseController
before_action :set_status
before_action :set_conversation
respond_to :json
def create
current_account.mute_conversation!(@conversation)
@mutes_map = { @conversation.id => true }

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::PinsController < Api::BaseController
before_action :require_user!
before_action :set_status
respond_to :json
def create
StatusPin.create!(account: current_account, status: @status)
distribute_add_activity!

View File

@ -7,8 +7,6 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
before_action :set_status
after_action :insert_pagination_headers
respond_to :json
def index
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer

View File

@ -7,10 +7,11 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
before_action :require_user!
before_action :set_reblog
respond_to :json
override_rate_limit_headers :create, family: :statuses
def create
@status = ReblogService.new.call(current_account, @reblog, reblog_params)
render json: @status, serializer: REST::StatusSerializer
end

View File

@ -7,8 +7,9 @@ class Api::V1::StatusesController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :destroy]
before_action :require_user!, except: [:show, :context]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
respond_to :json
override_rate_limit_headers :create, family: :statuses
# This API was originally unlimited, pagination cannot be introduced without
# breaking backwards-compatibility. Arbitrarily high number to cover most
@ -36,7 +37,7 @@ class Api::V1::StatusesController < Api::BaseController
def create
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
thread: @thread,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
@ -45,7 +46,8 @@ class Api::V1::StatusesController < Api::BaseController
application: doorkeeper_token.application,
poll: status_params[:poll],
content_type: status_params[:content_type],
idempotency: request.headers['Idempotency-Key'])
idempotency: request.headers['Idempotency-Key'],
with_rate_limit: true)
render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end
@ -69,6 +71,12 @@ class Api::V1::StatusesController < Api::BaseController
raise ActiveRecord::RecordNotFound
end
def set_thread
@thread = status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id])
rescue ActiveRecord::RecordNotFound
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end
def status_params
params.permit(
:status,

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::V1::StreamingController < Api::BaseController
respond_to :json
def index
if Rails.configuration.x.streaming_api_base_url != request.host
redirect_to streaming_api_url, status: 301

View File

@ -7,8 +7,6 @@ class Api::V1::SuggestionsController < Api::BaseController
before_action :require_user!
before_action :set_accounts
respond_to :json
def index
render json: @accounts, each_serializer: REST::AccountSerializer
end

View File

@ -5,8 +5,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses

View File

@ -4,8 +4,6 @@ class Api::V1::Timelines::PublicController < Api::BaseController
before_action :require_user!, only: [:show], if: :require_auth?
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -4,8 +4,6 @@ class Api::V1::Timelines::TagController < Api::BaseController
before_action :load_tag
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)

View File

@ -3,8 +3,6 @@
class Api::V1::TrendsController < Api::BaseController
before_action :set_tags
respond_to :json
def index
render json: @tags, each_serializer: REST::TagSerializer
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
class Api::V2::MediaController < Api::V1::MediaController
def create
@media_attachment = current_account.media_attachments.create!({ delay_processing: true }.merge(media_attachment_params))
render json: @media_attachment, serializer: REST::MediaAttachmentSerializer, status: 202
rescue Paperclip::Errors::NotIdentifiedByImageMagickError
render json: file_type_error, status: 422
rescue Paperclip::Error
render json: processing_error, status: 500
end
end

View File

@ -8,8 +8,6 @@ class Api::V2::SearchController < Api::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:search' }
before_action :require_user!
respond_to :json
def index
@search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::Web::EmbedsController < Api::Web::BaseController
respond_to :json
before_action :require_user!
def create

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
respond_to :json
before_action :require_user!
def create

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::Web::SettingsController < Api::Web::BaseController
respond_to :json
before_action :require_user!
def update

View File

@ -30,6 +30,7 @@ class ApplicationController < ActionController::Base
rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
rescue_from Mastodon::RaceConditionError, with: :service_unavailable
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :require_functional!, if: :user_signed_in?
@ -181,6 +182,10 @@ class ApplicationController < ActionController::Base
respond_with_error(503)
end
def too_many_requests
respond_with_error(429)
end
def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
end

View File

@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController
end
def create
if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource)
if @resource.is_a?(Account) && FollowService.new.call(current_account, @resource, with_rate_limit: true)
render :success
else
render :error

View File

@ -3,6 +3,20 @@
module RateLimitHeaders
extend ActiveSupport::Concern
class_methods do
def override_rate_limit_headers(method_name, options = {})
around_action(only: method_name, if: :current_account) do |_controller, block|
begin
block.call
ensure
rate_limiter = RateLimiter.new(current_account, options)
rate_limit_headers = rate_limiter.to_headers
response.headers.merge!(rate_limit_headers) unless response.headers['X-RateLimit-Remaining'].present? && rate_limit_headers['X-RateLimit-Remaining'].to_i > response.headers['X-RateLimit-Remaining'].to_i
end
end
end
end
included do
before_action :set_rate_limit_headers, if: :rate_limited_request?
end
@ -44,7 +58,7 @@ module RateLimitHeaders
end
def api_throttle_data
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] - v[:count] }
request.env['rack.attack.throttle_data'][most_limited_type]
end

View File

@ -29,7 +29,8 @@ class FollowerAccountsController < ApplicationController
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
content_type: 'application/activity+json',
fields: restrict_fields_to
end
end
end
@ -72,4 +73,12 @@ class FollowerAccountsController < ApplicationController
)
end
end
def restrict_fields_to
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
end
end
end

View File

@ -29,7 +29,8 @@ class FollowingAccountsController < ApplicationController
render json: collection_presenter,
serializer: ActivityPub::CollectionSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
content_type: 'application/activity+json',
fields: restrict_fields_to
end
end
end
@ -72,4 +73,12 @@ class FollowingAccountsController < ApplicationController
)
end
end
def restrict_fields_to
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
end
end
end

View File

@ -29,6 +29,6 @@ class Settings::ImportsController < Settings::BaseController
end
def import_params
params.require(:import).permit(:data, :type)
params.require(:import).permit(:data, :type, :mode)
end
end

View File

@ -9,79 +9,8 @@ module Admin::ActionLogsHelper
end
end
def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain')
elsif log.target_type == 'CustomEmoji' && log.action == :update
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
elsif log.target_type == 'Announcement' && log.action == :update
log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day')
end
end
def log_extra_attributes(hash)
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
end
def log_change(val)
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
end
def icon_for_log(log)
case log.target_type
when 'Account', 'User'
'user'
when 'CustomEmoji'
'file'
when 'Report'
'flag'
when 'DomainBlock'
'lock'
when 'DomainAllow'
'plus-circle'
when 'EmailDomainBlock'
'envelope'
when 'Status'
'pencil'
when 'AccountWarning'
'warning'
when 'Announcement'
'bullhorn'
end
end
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral'
when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
else
''
end
end
private
def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
end
def linkable_log_target(record)
case record.class.name
when 'Account'
@ -99,7 +28,7 @@ module Admin::ActionLogsHelper
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
when 'Announcement'
link_to "##{record.id}", edit_admin_announcement_path(record.id)
link_to truncate(record.text), edit_admin_announcement_path(record.id)
end
end
@ -118,7 +47,7 @@ module Admin::ActionLogsHelper
I18n.t('admin.action_logs.deleted_status')
end
when 'Announcement'
"##{attributes['id']}"
truncate(attributes['text'])
end
end
end

View File

@ -10,6 +10,7 @@ module Admin::FilterHelper
InviteFilter::KEYS,
RelationshipFilter::KEYS,
AnnouncementFilter::KEYS,
Admin::ActionLogFilter::KEYS,
].flatten.freeze
def filter_link_to(text, link_to_params, link_class_params = link_to_params)

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Admin::SettingsHelper
def site_upload_delete_hint(hint, var)
upload = SiteUpload.find_by(var: var.to_s)
return hint unless upload
link = link_to t('admin.site_uploads.delete'), admin_site_upload_path(upload), data: { method: :delete }
safe_join([hint, link], '<br/>'.html_safe)
end
end

View File

@ -1,6 +1,6 @@
// This file will be loaded on admin pages, regardless of theme.
import { delegate } from 'rails-ujs';
import { delegate } from '@rails/ujs';
import ready from '../mastodon/ready';
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
@ -32,6 +32,10 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
});
});
delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
target.form.submit();
});
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');

View File

@ -3,7 +3,7 @@
import createHistory from 'history/createBrowserHistory';
import ready from '../mastodon/ready';
const { delegate } = require('rails-ujs');
const { delegate } = require('@rails/ujs');
const { length } = require('stringz');
delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
@ -14,20 +14,6 @@ delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
return false;
});
delegate(document, '.status__content__spoiler-link', 'click', function() {
const contentEl = this.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') {
contentEl.style.display = 'none';
this.parentNode.style.marginBottom = 0;
} else {
contentEl.style.display = 'block';
this.parentNode.style.marginBottom = null;
}
return false;
});
delegate(document, '.modal-button', 'click', e => {
e.preventDefault();

View File

@ -1,7 +1,7 @@
// This file will be loaded on settings pages, regardless of theme.
import escapeTextContentForBrowser from 'escape-html';
const { delegate } = require('rails-ujs');
const { delegate } = require('@rails/ujs');
import emojify from '../mastodon/features/emoji/emoji';
delegate(document, '#account_display_name', 'input', ({ target }) => {

View File

@ -370,6 +370,7 @@ export function fetchFollowersFail(id, error) {
type: FOLLOWERS_FETCH_FAIL,
id,
error,
skipNotFound: true,
};
};
@ -456,6 +457,7 @@ export function fetchFollowingFail(id, error) {
type: FOLLOWING_FETCH_FAIL,
id,
error,
skipNotFound: true,
};
};
@ -545,6 +547,7 @@ export function fetchRelationshipsFail(error) {
type: RELATIONSHIPS_FETCH_FAIL,
error,
skipLoading: true,
skipNotFound: true,
};
};

View File

@ -34,11 +34,11 @@ export function showAlert(title = messages.unexpectedTitle, message = messages.u
};
};
export function showAlertForError(error) {
export function showAlertForError(error, skipNotFound = false) {
if (error.response) {
const { data, status, statusText, headers } = error.response;
if (status === 404 || status === 410) {
if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}

View File

@ -259,12 +259,31 @@ export function uploadCompose(files) {
// Account for disparity in size of original image and resized data
total += file.size - f.size;
return api(getState).post('/api/v1/media', data, {
return api(getState).post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
}).then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (status === 202) {
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, f));
} else if (response.status === 206) {
setTimeout(() => poll(), 1000);
}
}).catch(error => dispatch(uploadComposeFail(error)));
};
poll();
}
});
}).catch(error => dispatch(uploadComposeFail(error)));
};
};

View File

@ -27,4 +27,5 @@ export const fetchAccountIdentityProofsFail = (accountId, err) => ({
type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL,
accountId,
err,
skipNotFound: true,
});

View File

@ -165,6 +165,7 @@ export function expandTimelineFail(timeline, error, isLoadingMore) {
timeline,
error,
skipLoading: !isLoadingMore,
skipNotFound: timeline.startsWith('account:'),
};
};

View File

@ -5,7 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
export default @injectIntl

View File

@ -45,7 +45,7 @@ export default class IntersectionObserverArticle extends React.Component {
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection
this.handleIntersection,
);
this.componentMounted = true;

View File

@ -23,7 +23,7 @@ const messages = defineMessages({
id: 'status.sensitive_toggle',
},
toggle_visible: {
defaultMessage: 'Toggle visibility',
defaultMessage: 'Hide media',
id: 'media_gallery.toggle_visible',
},
warning: {

View File

@ -127,15 +127,7 @@ class Poll extends ImmutablePureComponent {
return (
<li key={option.get('title')}>
{showResults && (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
}
</Motion>
)}
<label className={classNames('poll__text', { selectable: !showResults })}>
<label className={classNames('poll__option', { selectable: !showResults })}>
<input
name='vote-options'
type={poll.get('multiple') ? 'checkbox' : 'radio'}
@ -157,12 +149,26 @@ class Poll extends ImmutablePureComponent {
/>
)}
{showResults && <span className='poll__number'>
{!!voted && <Icon id='check' className='poll__vote__mark' title={intl.formatMessage(messages.voted)} />}
{Math.round(percent)}%
</span>}
<span dangerouslySetInnerHTML={{ __html: titleEmojified }} />
<span
className='poll__option__text'
dangerouslySetInnerHTML={{ __html: titleEmojified }}
/>
{!!voted && <span className='poll__voted'>
<Icon id='check' className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
</span>}
</label>
{showResults && (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
}
</Motion>
)}
</li>
);
}

View File

@ -6,7 +6,7 @@ import Domain from '../components/domain';
import { openModal } from '../actions/modal';
const messages = defineMessages({
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
});
const makeMapStateToProps = () => {

View File

@ -30,8 +30,8 @@ const messages = defineMessages({
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
@ -40,7 +40,7 @@ const messages = defineMessages({
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
@ -136,7 +136,7 @@ class Header extends ImmutablePureComponent {
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>);
info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
}
if (me !== account.get('id')) {

View File

@ -199,8 +199,8 @@ class Audio extends React.PureComponent {
<div className='video-player__controls active'>
<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;
@ -221,7 +221,7 @@ class Audio extends React.PureComponent {
</div>
<div className='video-player__buttons right'>
<button type='button' aria-label={intl.formatMessage(messages.download)}>
<button type='button' title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)}>
<a className='video-player__download__icon' href={this.props.src} download>
<Icon id={'download'} fixedWidth />
</a>

View File

@ -66,7 +66,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
</Column>

View File

@ -81,18 +81,6 @@ class ComposeForm extends ImmutablePureComponent {
this.props.onChange(e.target.value);
}
handleKeyDown = ({ ctrlKey, keyCode, metaKey, altKey }) => {
// We submit the status on control/meta + enter.
if (keyCode === 13 && (ctrlKey || metaKey)) {
this.handleSubmit();
}
// Submit the status with secondary visibility on alt + enter.
if (keyCode === 13 && altKey) {
this.handleSecondarySubmit();
}
}
handleSubmit = (overriddenVisibility = null) => {
const { textarea: { value }, uploadForm } = this;
const {
@ -123,7 +111,7 @@ class ComposeForm extends ImmutablePureComponent {
// Submit unless there are media with missing descriptions
if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) {
const firstWithoutDescription = media.find(item => !item.get('description'));
onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'));
onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'), overriddenVisibility);
} else if (onSubmit) {
if (onChangeVisibility && overriddenVisibility) {
onChangeVisibility(overriddenVisibility);
@ -171,10 +159,20 @@ class ComposeForm extends ImmutablePureComponent {
}
// When the escape key is released, we focus the UI.
handleKeyUp = ({ key }) => {
handleKeyUp = ({ key, ctrlKey, keyCode, metaKey, altKey }) => {
if (key === 'Escape') {
document.querySelector('.ui').parentElement.focus();
}
// We submit the status on control/meta + enter.
if (keyCode === 13 && (ctrlKey || metaKey)) {
this.handleSubmit();
}
// Submit the status with secondary visibility on alt + enter.
if (keyCode === 13 && altKey) {
this.handleSecondarySubmit();
}
}
// Sets a reference to the textarea.
@ -307,7 +305,6 @@ class ComposeForm extends ImmutablePureComponent {
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={spoilerText}
onChange={this.handleChangeSpoiler}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
disabled={!spoiler}
ref={this.handleRefSpoilerText}
@ -328,9 +325,9 @@ class ComposeForm extends ImmutablePureComponent {
disabled={isSubmitting}
value={this.props.text}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onSuggestionsFetchRequested={onFetchSuggestions}
onSuggestionsClearRequested={onClearSuggestions}
onSuggestionSelected={this.onSuggestionSelected}

View File

@ -34,7 +34,7 @@ const messages = defineMessages({
id: 'content-type.change',
},
direct_long: {
defaultMessage: 'Post to mentioned users only',
defaultMessage: 'Visible for mentioned users only',
id: 'privacy.direct.long',
},
direct_short: {
@ -66,7 +66,7 @@ const messages = defineMessages({
id: 'compose.content-type.plain',
},
private_long: {
defaultMessage: 'Post to followers only',
defaultMessage: 'Visible for followers only',
id: 'privacy.private.long',
},
private_short: {
@ -74,7 +74,7 @@ const messages = defineMessages({
id: 'privacy.private.short',
},
public_long: {
defaultMessage: 'Post to public timelines',
defaultMessage: 'Visible for all, shown in public timelines',
id: 'privacy.public.long',
},
public_short: {
@ -94,7 +94,7 @@ const messages = defineMessages({
id: 'advanced_options.threaded_mode.short',
},
unlisted_long: {
defaultMessage: 'Do not show in public timelines',
defaultMessage: 'Visible for all, but not in public timelines',
id: 'privacy.unlisted.long',
},
unlisted_short: {

View File

@ -62,7 +62,7 @@ class Option extends React.PureComponent {
return (
<li>
<label className='poll__text editable'>
<label className='poll__option editable'>
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
<AutosuggestInput
@ -143,6 +143,7 @@ class PollForm extends ImmutablePureComponent {
<option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
</select>
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
<select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>

View File

@ -114,11 +114,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(changeComposeVisibility(value));
},
onMediaDescriptionConfirm(routerHistory, mediaId) {
onMediaDescriptionConfirm(routerHistory, mediaId, overriddenVisibility = null) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.missingDescriptionMessage),
confirm: intl.formatMessage(messages.missingDescriptionConfirm),
onConfirm: () => dispatch(submitCompose(routerHistory)),
onConfirm: () => {
if (overriddenVisibility) {
dispatch(changeComposeVisibility(overriddenVisibility));
};
dispatch(submitCompose(routerHistory));
},
secondary: intl.formatMessage(messages.missingDescriptionEdit),
onSecondary: () => dispatch(openModal('FOCAL_POINT', { id: mediaId })),
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)),

View File

@ -195,7 +195,7 @@ class Conversation extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className='conversation__avatar'>
<div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
<AvatarComposite accounts={accounts} size={48} />
</div>

View File

@ -13,8 +13,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
const messages = defineMessages({
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
const mapStateToProps = state => ({
@ -54,7 +54,7 @@ class Blocks extends ImmutablePureComponent {
);
}
const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no blocked domains yet.' />;
return (
<Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
@ -67,7 +67,7 @@ class Blocks extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{domains.map(domain =>
<DomainContainer key={domain} domain={domain} />
<DomainContainer key={domain} domain={domain} />,
)}
</ScrollableList>
</Column>

View File

@ -88,7 +88,7 @@ class Favourites extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
</Column>

View File

@ -11,6 +11,7 @@ import { fetchFollowRequests, expandFollowRequests } from 'flavours/glitch/actio
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { me } from 'flavours/glitch/util/initial_state';
const messages = defineMessages({
heading: { id: 'column.follow_requests', defaultMessage: 'Follow requests' },
@ -19,6 +20,8 @@ const messages = defineMessages({
const mapStateToProps = state => ({
accountIds: state.getIn(['user_lists', 'follow_requests', 'items']),
hasMore: !!state.getIn(['user_lists', 'follow_requests', 'next']),
locked: !!state.getIn(['accounts', me, 'locked']),
domain: state.getIn(['meta', 'domain']),
});
export default @connect(mapStateToProps)
@ -30,6 +33,8 @@ class FollowRequests extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired,
hasMore: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
locked: PropTypes.bool,
domain: PropTypes.string,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
};
@ -43,7 +48,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, accountIds, hasMore, multiColumn } = this.props;
const { intl, accountIds, hasMore, multiColumn, locked, domain } = this.props;
if (!accountIds) {
return (
@ -54,6 +59,15 @@ class FollowRequests extends ImmutablePureComponent {
}
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
const unlockedPrependMessage = locked ? null : (
<div className='follow_requests-unlocked_explanation'>
<FormattedMessage
id='follow_requests.unlocked_explanation'
defaultMessage='Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.'
values={{ domain: domain }}
/>
</div>
);
return (
<Column bindToDocument={!multiColumn} name='follow-requests' icon='user-plus' heading={intl.formatMessage(messages.heading)}>
@ -65,9 +79,10 @@ class FollowRequests extends ImmutablePureComponent {
hasMore={hasMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
prepend={unlockedPrependMessage}
>
{accountIds.map(id =>
<AccountAuthorizeContainer key={id} id={id} />
<AccountAuthorizeContainer key={id} id={id} />,
)}
</ScrollableList>
</Column>

View File

@ -105,7 +105,7 @@ class Followers extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
</Column>

View File

@ -105,7 +105,7 @@ class Following extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />
<AccountContainer key={id} id={id} withNote={false} />,
)}
</ScrollableList>
</Column>

View File

@ -95,6 +95,10 @@ class Content extends ImmutablePureComponent {
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else {
let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url'));
if (status) {
link.addEventListener('click', this.onStatusClick.bind(this, status), false);
}
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');
}
@ -120,6 +124,13 @@ class Content extends ImmutablePureComponent {
}
}
onStatusClick = (status, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/statuses/${status.get('id')}`);
}
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
@ -367,6 +378,14 @@ class Announcements extends ImmutablePureComponent {
index: 0,
};
static getDerivedStateFromProps(props, state) {
if (props.announcements.size > 0 && state.index >= props.announcements.size) {
return { index: props.announcements.size - 1 };
} else {
return null;
}
}
componentDidMount () {
this._markAnnouncementAsRead();
}

View File

@ -73,7 +73,7 @@ class Lists extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />,
)}
</ScrollableList>
</Column>

View File

@ -66,7 +66,7 @@ class Mutes extends ImmutablePureComponent {
bindToDocument={!multiColumn}
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />
<AccountContainer key={id} id={id} />,
)}
</ScrollableList>
</Column>

Some files were not shown because too many files have changed in this diff Show More