mirror of
https://framagit.org/tykayn/mastodon.git
synced 2023-08-25 08:33:12 +02:00
Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `Gemfile.lock`: Not a real conflict, upstream updated dependencies that were too close to glitch-soc-only ones in the file. - `app/controllers/oauth/authorized_applications_controller.rb`: Upstream changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/controllers/settings/base_controller.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/controllers/settings/sessions_controller.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/models/user.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc not preventing moved accounts from logging in. Ported upstream changes while keeping the ability for moved accounts to log in. - `app/policies/status_policy.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's local-only toots. Ported upstream changes. - `app/serializers/rest/account_serializer.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's ability to hide followers count. Ported upstream changes. - `app/services/process_mentions_service.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's local-only toots. Ported upstream changes. - `package.json`: Not a real conflict, upstream updated dependencies that were too close to glitch-soc-only ones in the file.
This commit is contained in:
commit
a7aedebc31
18
Gemfile
18
Gemfile
@ -5,10 +5,10 @@ ruby '>= 2.5.0', '< 3.0.0'
|
|||||||
|
|
||||||
gem 'pkg-config', '~> 1.4'
|
gem 'pkg-config', '~> 1.4'
|
||||||
|
|
||||||
gem 'puma', '~> 4.3'
|
gem 'puma', '~> 5.0'
|
||||||
gem 'rails', '~> 5.2.4.3'
|
gem 'rails', '~> 5.2.4.4'
|
||||||
gem 'sprockets', '~> 3.7.2'
|
gem 'sprockets', '~> 3.7.2'
|
||||||
gem 'thor', '~> 0.20'
|
gem 'thor', '~> 1.0'
|
||||||
gem 'rack', '~> 2.2.3'
|
gem 'rack', '~> 2.2.3'
|
||||||
|
|
||||||
gem 'thwait', '~> 0.2.0'
|
gem 'thwait', '~> 0.2.0'
|
||||||
@ -20,7 +20,7 @@ gem 'makara', '~> 0.4'
|
|||||||
gem 'pghero', '~> 2.7'
|
gem 'pghero', '~> 2.7'
|
||||||
gem 'dotenv-rails', '~> 2.7'
|
gem 'dotenv-rails', '~> 2.7'
|
||||||
|
|
||||||
gem 'aws-sdk-s3', '~> 1.79', require: false
|
gem 'aws-sdk-s3', '~> 1.81', 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 'paperclip', '~> 6.0'
|
gem 'paperclip', '~> 6.0'
|
||||||
@ -123,26 +123,26 @@ end
|
|||||||
group :test do
|
group :test do
|
||||||
gem 'capybara', '~> 3.33'
|
gem 'capybara', '~> 3.33'
|
||||||
gem 'climate_control', '~> 0.2'
|
gem 'climate_control', '~> 0.2'
|
||||||
gem 'faker', '~> 2.13'
|
gem 'faker', '~> 2.14'
|
||||||
gem 'microformats', '~> 4.2'
|
gem 'microformats', '~> 4.2'
|
||||||
gem 'rails-controller-testing', '~> 1.0'
|
gem 'rails-controller-testing', '~> 1.0'
|
||||||
gem 'rspec-sidekiq', '~> 3.1'
|
gem 'rspec-sidekiq', '~> 3.1'
|
||||||
gem 'simplecov', '~> 0.19', require: false
|
gem 'simplecov', '~> 0.19', require: false
|
||||||
gem 'webmock', '~> 3.8'
|
gem 'webmock', '~> 3.9'
|
||||||
gem 'parallel_tests', '~> 3.2'
|
gem 'parallel_tests', '~> 3.3'
|
||||||
gem 'rspec_junit_formatter', '~> 0.4'
|
gem 'rspec_junit_formatter', '~> 0.4'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
gem 'active_record_query_trace', '~> 1.7'
|
gem 'active_record_query_trace', '~> 1.7'
|
||||||
gem 'annotate', '~> 3.1'
|
gem 'annotate', '~> 3.1'
|
||||||
gem 'better_errors', '~> 2.7'
|
gem 'better_errors', '~> 2.8'
|
||||||
gem 'binding_of_caller', '~> 0.7'
|
gem 'binding_of_caller', '~> 0.7'
|
||||||
gem 'bullet', '~> 6.1'
|
gem 'bullet', '~> 6.1'
|
||||||
gem 'letter_opener', '~> 1.7'
|
gem 'letter_opener', '~> 1.7'
|
||||||
gem 'letter_opener_web', '~> 1.4'
|
gem 'letter_opener_web', '~> 1.4'
|
||||||
gem 'memory_profiler'
|
gem 'memory_profiler'
|
||||||
gem 'rubocop', '~> 0.90', require: false
|
gem 'rubocop', '~> 0.91', require: false
|
||||||
gem 'rubocop-rails', '~> 2.8', require: false
|
gem 'rubocop-rails', '~> 2.8', require: false
|
||||||
gem 'brakeman', '~> 4.9', require: false
|
gem 'brakeman', '~> 4.9', require: false
|
||||||
gem 'bundler-audit', '~> 0.7', require: false
|
gem 'bundler-audit', '~> 0.7', require: false
|
||||||
|
144
Gemfile.lock
144
Gemfile.lock
@ -16,25 +16,25 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (5.2.4.3)
|
actioncable (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailer (5.2.4.3)
|
actionmailer (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activejob (= 5.2.4.3)
|
activejob (= 5.2.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 (5.2.4.3)
|
actionpack (5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
rack (~> 2.0, >= 2.0.8)
|
rack (~> 2.0, >= 2.0.8)
|
||||||
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.0.2)
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||||
actionview (5.2.4.3)
|
actionview (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
@ -45,20 +45,20 @@ GEM
|
|||||||
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.7)
|
active_record_query_trace (1.7)
|
||||||
activejob (5.2.4.3)
|
activejob (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (5.2.4.3)
|
activemodel (5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
activerecord (5.2.4.3)
|
activerecord (5.2.4.4)
|
||||||
activemodel (= 5.2.4.3)
|
activemodel (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
arel (>= 9.0)
|
arel (>= 9.0)
|
||||||
activestorage (5.2.4.3)
|
activestorage (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
activerecord (= 5.2.4.3)
|
activerecord (= 5.2.4.4)
|
||||||
marcel (~> 0.3.1)
|
marcel (~> 0.3.1)
|
||||||
activesupport (5.2.4.3)
|
activesupport (5.2.4.4)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
minitest (~> 5.1)
|
minitest (~> 5.1)
|
||||||
@ -79,23 +79,23 @@ GEM
|
|||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
awrence (1.1.1)
|
awrence (1.1.1)
|
||||||
aws-eventstream (1.1.0)
|
aws-eventstream (1.1.0)
|
||||||
aws-partitions (1.365.0)
|
aws-partitions (1.373.0)
|
||||||
aws-sdk-core (3.105.0)
|
aws-sdk-core (3.107.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.37.0)
|
aws-sdk-kms (1.38.0)
|
||||||
aws-sdk-core (~> 3, >= 3.99.0)
|
aws-sdk-core (~> 3, >= 3.99.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.79.1)
|
aws-sdk-s3 (1.81.0)
|
||||||
aws-sdk-core (~> 3, >= 3.104.3)
|
aws-sdk-core (~> 3, >= 3.104.3)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sigv4 (1.2.2)
|
aws-sigv4 (1.2.2)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
bcrypt (3.1.16)
|
bcrypt (3.1.16)
|
||||||
better_errors (2.7.1)
|
better_errors (2.8.1)
|
||||||
coderay (>= 1.0.0)
|
coderay (>= 1.0.0)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
@ -160,13 +160,12 @@ GEM
|
|||||||
cose (1.0.0)
|
cose (1.0.0)
|
||||||
cbor (~> 0.5.9)
|
cbor (~> 0.5.9)
|
||||||
openssl-signature_algorithm (~> 0.4.0)
|
openssl-signature_algorithm (~> 0.4.0)
|
||||||
crack (0.4.3)
|
crack (0.4.4)
|
||||||
safe_yaml (~> 1.0.0)
|
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.7.1)
|
css_parser (1.7.1)
|
||||||
addressable
|
addressable
|
||||||
debug_inspector (0.0.3)
|
debug_inspector (0.0.3)
|
||||||
devise (4.7.2)
|
devise (4.7.3)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@ -210,7 +209,7 @@ GEM
|
|||||||
tzinfo
|
tzinfo
|
||||||
excon (0.76.0)
|
excon (0.76.0)
|
||||||
fabrication (2.21.1)
|
fabrication (2.21.1)
|
||||||
faker (2.13.0)
|
faker (2.14.0)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
faraday (1.0.1)
|
faraday (1.0.1)
|
||||||
multipart-post (>= 1.2, < 3)
|
multipart-post (>= 1.2, < 3)
|
||||||
@ -233,7 +232,7 @@ GEM
|
|||||||
fog-json (>= 1.0)
|
fog-json (>= 1.0)
|
||||||
ipaddress (>= 0.8)
|
ipaddress (>= 0.8)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fugit (1.3.8)
|
fugit (1.3.9)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.3)
|
raabro (~> 1.3)
|
||||||
fuubar (2.5.0)
|
fuubar (2.5.0)
|
||||||
@ -363,7 +362,7 @@ GEM
|
|||||||
net-scp (3.0.0)
|
net-scp (3.0.0)
|
||||||
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.3)
|
nio4r (2.5.4)
|
||||||
nokogiri (1.10.10)
|
nokogiri (1.10.10)
|
||||||
mini_portile2 (~> 2.4.0)
|
mini_portile2 (~> 2.4.0)
|
||||||
nokogumbo (2.0.2)
|
nokogumbo (2.0.2)
|
||||||
@ -387,7 +386,7 @@ 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.13.3)
|
ox (2.13.4)
|
||||||
paperclip (6.0.0)
|
paperclip (6.0.0)
|
||||||
activemodel (>= 4.2.0)
|
activemodel (>= 4.2.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
@ -398,7 +397,7 @@ GEM
|
|||||||
av (~> 0.9.0)
|
av (~> 0.9.0)
|
||||||
paperclip (>= 2.5.2)
|
paperclip (>= 2.5.2)
|
||||||
parallel (1.19.2)
|
parallel (1.19.2)
|
||||||
parallel_tests (3.2.0)
|
parallel_tests (3.3.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.7.1.4)
|
parser (2.7.1.4)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
@ -406,11 +405,11 @@ GEM
|
|||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
pghero (2.7.0)
|
pghero (2.7.2)
|
||||||
activerecord (>= 5)
|
activerecord (>= 5)
|
||||||
pkg-config (1.4.2)
|
pkg-config (1.4.3)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
premailer (1.13.1)
|
premailer (1.14.2)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.6.0)
|
css_parser (>= 1.6.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
@ -427,7 +426,7 @@ 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 (4.3.6)
|
puma (5.0.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.1.0)
|
pundit (2.1.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -441,18 +440,18 @@ GEM
|
|||||||
rack
|
rack
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails (5.2.4.3)
|
rails (5.2.4.4)
|
||||||
actioncable (= 5.2.4.3)
|
actioncable (= 5.2.4.4)
|
||||||
actionmailer (= 5.2.4.3)
|
actionmailer (= 5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
actionview (= 5.2.4.3)
|
actionview (= 5.2.4.4)
|
||||||
activejob (= 5.2.4.3)
|
activejob (= 5.2.4.4)
|
||||||
activemodel (= 5.2.4.3)
|
activemodel (= 5.2.4.4)
|
||||||
activerecord (= 5.2.4.3)
|
activerecord (= 5.2.4.4)
|
||||||
activestorage (= 5.2.4.3)
|
activestorage (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
bundler (>= 1.3.0)
|
bundler (>= 1.3.0)
|
||||||
railties (= 5.2.4.3)
|
railties (= 5.2.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,9 +467,9 @@ GEM
|
|||||||
railties (>= 5.0, < 6)
|
railties (>= 5.0, < 6)
|
||||||
rails-settings-cached (0.6.6)
|
rails-settings-cached (0.6.6)
|
||||||
rails (>= 4.2.0)
|
rails (>= 4.2.0)
|
||||||
railties (5.2.4.3)
|
railties (5.2.4.4)
|
||||||
actionpack (= 5.2.4.3)
|
actionpack (= 5.2.4.4)
|
||||||
activesupport (= 5.2.4.3)
|
activesupport (= 5.2.4.4)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
thor (>= 0.19.0, < 2.0)
|
thor (>= 0.19.0, < 2.0)
|
||||||
@ -482,7 +481,7 @@ GEM
|
|||||||
rdf-normalize (0.4.0)
|
rdf-normalize (0.4.0)
|
||||||
rdf (~> 3.1)
|
rdf (~> 3.1)
|
||||||
redcarpet (3.5.0)
|
redcarpet (3.5.0)
|
||||||
redis (4.2.1)
|
redis (4.2.2)
|
||||||
redis-actionpack (5.2.0)
|
redis-actionpack (5.2.0)
|
||||||
actionpack (>= 5, < 7)
|
actionpack (>= 5, < 7)
|
||||||
redis-rack (>= 2.1.0, < 3)
|
redis-rack (>= 2.1.0, < 3)
|
||||||
@ -501,7 +500,7 @@ GEM
|
|||||||
redis-store (>= 1.2, < 2)
|
redis-store (>= 1.2, < 2)
|
||||||
redis-store (1.9.0)
|
redis-store (1.9.0)
|
||||||
redis (>= 4, < 5)
|
redis (>= 4, < 5)
|
||||||
regexp_parser (1.7.1)
|
regexp_parser (1.8.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)
|
||||||
@ -536,18 +535,18 @@ GEM
|
|||||||
rspec-support (3.9.3)
|
rspec-support (3.9.3)
|
||||||
rspec_junit_formatter (0.4.1)
|
rspec_junit_formatter (0.4.1)
|
||||||
rspec-core (>= 2, < 4, != 2.12.0)
|
rspec-core (>= 2, < 4, != 2.12.0)
|
||||||
rubocop (0.90.0)
|
rubocop (0.91.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 2.7.1.1)
|
parser (>= 2.7.1.1)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.7)
|
regexp_parser (>= 1.7)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 0.3.0, < 1.0)
|
rubocop-ast (>= 0.4.0, < 1.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 2.0)
|
unicode-display_width (>= 1.4.0, < 2.0)
|
||||||
rubocop-ast (0.3.0)
|
rubocop-ast (0.4.2)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
rubocop-rails (2.8.0)
|
rubocop-rails (2.8.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 0.87.0)
|
rubocop (>= 0.87.0)
|
||||||
@ -556,7 +555,6 @@ GEM
|
|||||||
nokogiri (>= 1.5.10)
|
nokogiri (>= 1.5.10)
|
||||||
rufus-scheduler (3.6.0)
|
rufus-scheduler (3.6.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safe_yaml (1.0.5)
|
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (5.2.1)
|
sanitize (5.2.1)
|
||||||
@ -565,7 +563,7 @@ GEM
|
|||||||
nokogumbo (~> 2.0)
|
nokogumbo (~> 2.0)
|
||||||
securecompare (1.0.0)
|
securecompare (1.0.0)
|
||||||
semantic_range (2.3.0)
|
semantic_range (2.3.0)
|
||||||
sidekiq (6.1.1)
|
sidekiq (6.1.2)
|
||||||
connection_pool (>= 2.2.2)
|
connection_pool (>= 2.2.2)
|
||||||
rack (~> 2.0)
|
rack (~> 2.0)
|
||||||
redis (>= 4.2.0)
|
redis (>= 4.2.0)
|
||||||
@ -594,7 +592,7 @@ 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.1)
|
sprockets-rails (3.2.2)
|
||||||
actionpack (>= 4.0)
|
actionpack (>= 4.0)
|
||||||
activesupport (>= 4.0)
|
activesupport (>= 4.0)
|
||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
@ -613,7 +611,7 @@ GEM
|
|||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||||
terrapin (0.6.0)
|
terrapin (0.6.0)
|
||||||
climate_control (>= 0.0.3, < 1.0)
|
climate_control (>= 0.0.3, < 1.0)
|
||||||
thor (0.20.3)
|
thor (1.0.1)
|
||||||
thread_safe (0.3.6)
|
thread_safe (0.3.6)
|
||||||
thwait (0.2.0)
|
thwait (0.2.0)
|
||||||
e2mmap
|
e2mmap
|
||||||
@ -654,7 +652,7 @@ GEM
|
|||||||
safety_net_attestation (~> 0.4.0)
|
safety_net_attestation (~> 0.4.0)
|
||||||
securecompare (~> 1.0)
|
securecompare (~> 1.0)
|
||||||
tpm-key_attestation (~> 0.9.0)
|
tpm-key_attestation (~> 0.9.0)
|
||||||
webmock (3.8.3)
|
webmock (3.9.1)
|
||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@ -681,8 +679,8 @@ DEPENDENCIES
|
|||||||
active_record_query_trace (~> 1.7)
|
active_record_query_trace (~> 1.7)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
annotate (~> 3.1)
|
annotate (~> 3.1)
|
||||||
aws-sdk-s3 (~> 1.79)
|
aws-sdk-s3 (~> 1.81)
|
||||||
better_errors (~> 2.7)
|
better_errors (~> 2.8)
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
blurhash (~> 0.1)
|
blurhash (~> 0.1)
|
||||||
bootsnap (~> 1.4)
|
bootsnap (~> 1.4)
|
||||||
@ -711,7 +709,7 @@ DEPENDENCIES
|
|||||||
e2mmap (~> 0.1.0)
|
e2mmap (~> 0.1.0)
|
||||||
ed25519 (~> 1.2)
|
ed25519 (~> 1.2)
|
||||||
fabrication (~> 2.21)
|
fabrication (~> 2.21)
|
||||||
faker (~> 2.13)
|
faker (~> 2.14)
|
||||||
fast_blank (~> 1.0)
|
fast_blank (~> 1.0)
|
||||||
fastimage
|
fastimage
|
||||||
fog-core (<= 2.1.0)
|
fog-core (<= 2.1.0)
|
||||||
@ -752,7 +750,7 @@ DEPENDENCIES
|
|||||||
paperclip (~> 6.0)
|
paperclip (~> 6.0)
|
||||||
paperclip-av-transcoder (~> 0.6)
|
paperclip-av-transcoder (~> 0.6)
|
||||||
parallel (~> 1.19)
|
parallel (~> 1.19)
|
||||||
parallel_tests (~> 3.2)
|
parallel_tests (~> 3.3)
|
||||||
parslet
|
parslet
|
||||||
pg (~> 1.2)
|
pg (~> 1.2)
|
||||||
pghero (~> 2.7)
|
pghero (~> 2.7)
|
||||||
@ -762,12 +760,12 @@ DEPENDENCIES
|
|||||||
private_address_check (~> 0.5)
|
private_address_check (~> 0.5)
|
||||||
pry-byebug (~> 3.9)
|
pry-byebug (~> 3.9)
|
||||||
pry-rails (~> 0.3)
|
pry-rails (~> 0.3)
|
||||||
puma (~> 4.3)
|
puma (~> 5.0)
|
||||||
pundit (~> 2.1)
|
pundit (~> 2.1)
|
||||||
rack (~> 2.2.3)
|
rack (~> 2.2.3)
|
||||||
rack-attack (~> 6.3)
|
rack-attack (~> 6.3)
|
||||||
rack-cors (~> 1.1)
|
rack-cors (~> 1.1)
|
||||||
rails (~> 5.2.4.3)
|
rails (~> 5.2.4.4)
|
||||||
rails-controller-testing (~> 1.0)
|
rails-controller-testing (~> 1.0)
|
||||||
rails-i18n (~> 5.1)
|
rails-i18n (~> 5.1)
|
||||||
rails-settings-cached (~> 0.6)
|
rails-settings-cached (~> 0.6)
|
||||||
@ -780,7 +778,7 @@ DEPENDENCIES
|
|||||||
rspec-rails (~> 4.0)
|
rspec-rails (~> 4.0)
|
||||||
rspec-sidekiq (~> 3.1)
|
rspec-sidekiq (~> 3.1)
|
||||||
rspec_junit_formatter (~> 0.4)
|
rspec_junit_formatter (~> 0.4)
|
||||||
rubocop (~> 0.90)
|
rubocop (~> 0.91)
|
||||||
rubocop-rails (~> 2.8)
|
rubocop-rails (~> 2.8)
|
||||||
ruby-progressbar (~> 1.10)
|
ruby-progressbar (~> 1.10)
|
||||||
sanitize (~> 5.2)
|
sanitize (~> 5.2)
|
||||||
@ -797,12 +795,12 @@ DEPENDENCIES
|
|||||||
stoplight (~> 2.2.1)
|
stoplight (~> 2.2.1)
|
||||||
streamio-ffmpeg (~> 3.0)
|
streamio-ffmpeg (~> 3.0)
|
||||||
strong_migrations (~> 0.7)
|
strong_migrations (~> 0.7)
|
||||||
thor (~> 0.20)
|
thor (~> 1.0)
|
||||||
thwait (~> 0.2.0)
|
thwait (~> 0.2.0)
|
||||||
tty-prompt (~> 0.22)
|
tty-prompt (~> 0.22)
|
||||||
twitter-text (~> 1.14)
|
twitter-text (~> 1.14)
|
||||||
tzinfo-data (~> 1.2020)
|
tzinfo-data (~> 1.2020)
|
||||||
webauthn (~> 3.0.0.alpha1)
|
webauthn (~> 3.0.0.alpha1)
|
||||||
webmock (~> 3.8)
|
webmock (~> 3.9)
|
||||||
webpacker (~> 5.2)
|
webpacker (~> 5.2)
|
||||||
webpush
|
webpush
|
||||||
|
@ -7,6 +7,7 @@ class AccountsController < ApplicationController
|
|||||||
include AccountControllerConcern
|
include AccountControllerConcern
|
||||||
include SignatureAuthentication
|
include SignatureAuthentication
|
||||||
|
|
||||||
|
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ class AccountsController < ApplicationController
|
|||||||
|
|
||||||
format.json do
|
format.json do
|
||||||
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
|
||||||
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to
|
render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -154,12 +155,4 @@ class AccountsController < ApplicationController
|
|||||||
def params_slice(*keys)
|
def params_slice(*keys)
|
||||||
params.slice(*keys).permit(*keys)
|
params.slice(*keys).permit(*keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_fields_to
|
|
||||||
if signed_request_account.present? || public_fetch_mode?
|
|
||||||
# Return all fields
|
|
||||||
else
|
|
||||||
%i(id type preferred_username inbox public_key endpoints)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -57,9 +57,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
|||||||
def set_statuses
|
def set_statuses
|
||||||
return unless page_requested?
|
return unless page_requested?
|
||||||
|
|
||||||
@statuses = @account.statuses.permitted_for(@account, signed_request_account)
|
|
||||||
@statuses = cache_collection_paginated_by_id(
|
@statuses = cache_collection_paginated_by_id(
|
||||||
@statuses,
|
@account.statuses.permitted_for(@account, signed_request_account),
|
||||||
Status,
|
Status,
|
||||||
LIMIT,
|
LIMIT,
|
||||||
params_slice(:max_id, :min_id, :since_id)
|
params_slice(:max_id, :min_id, :since_id)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
class AccountsController < BaseController
|
||||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
before_action :set_account, except: [:index]
|
||||||
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]
|
||||||
|
|
||||||
@ -14,49 +14,58 @@ module Admin
|
|||||||
def show
|
def show
|
||||||
authorize @account, :show?
|
authorize @account, :show?
|
||||||
|
|
||||||
|
@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.targeted_account_warnings.latest.custom
|
||||||
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def memorialize
|
def memorialize
|
||||||
authorize @account, :memorialize?
|
authorize @account, :memorialize?
|
||||||
@account.memorialize!
|
@account.memorialize!
|
||||||
log_action :memorialize, @account
|
log_action :memorialize, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
authorize @account.user, :enable?
|
authorize @account.user, :enable?
|
||||||
@account.user.enable!
|
@account.user.enable!
|
||||||
log_action :enable, @account.user
|
log_action :enable, @account.user
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def approve
|
def approve
|
||||||
authorize @account.user, :approve?
|
authorize @account.user, :approve?
|
||||||
@account.user.approve!
|
@account.user.approve!
|
||||||
redirect_to admin_pending_accounts_path
|
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
SuspendAccountService.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
|
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @account, :destroy?
|
||||||
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsilence
|
def unsilence
|
||||||
authorize @account, :unsilence?
|
authorize @account, :unsilence?
|
||||||
@account.unsilence!
|
@account.unsilence!
|
||||||
log_action :unsilence, @account
|
log_action :unsilence, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuspend
|
def unsuspend
|
||||||
authorize @account, :unsuspend?
|
authorize @account, :unsuspend?
|
||||||
@account.unsuspend!
|
@account.unsuspend!
|
||||||
|
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||||
log_action :unsuspend, @account
|
log_action :unsuspend, @account
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def redownload
|
def redownload
|
||||||
@ -65,7 +74,7 @@ module Admin
|
|||||||
@account.update!(last_webfingered_at: nil)
|
@account.update!(last_webfingered_at: nil)
|
||||||
ResolveAccountService.new.call(@account)
|
ResolveAccountService.new.call(@account)
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_avatar
|
def remove_avatar
|
||||||
@ -76,7 +85,7 @@ module Admin
|
|||||||
|
|
||||||
log_action :remove_avatar, @account.user
|
log_action :remove_avatar, @account.user
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_header
|
def remove_header
|
||||||
@ -87,7 +96,7 @@ module Admin
|
|||||||
|
|
||||||
log_action :remove_header, @account.user
|
log_action :remove_header, @account.user
|
||||||
|
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController
|
|||||||
def require_user!
|
def require_user!
|
||||||
if !current_user
|
if !current_user
|
||||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||||
elsif current_user.disabled?
|
|
||||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
|
||||||
elsif !current_user.confirmed?
|
elsif !current_user.confirmed?
|
||||||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
||||||
elsif !current_user.approved?
|
elsif !current_user.approved?
|
||||||
render json: { error: 'Your login is currently pending approval' }, status: 403
|
render json: { error: 'Your login is currently pending approval' }, status: 403
|
||||||
|
elsif !current_user.functional?
|
||||||
|
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||||
else
|
else
|
||||||
set_user_activity
|
set_user_activity
|
||||||
end
|
end
|
||||||
|
@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_featured_tags
|
def set_featured_tags
|
||||||
@featured_tags = @account.featured_tags
|
@featured_tags = @account.suspended? ? @account.featured_tags : []
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hide_results?
|
def hide_results?
|
||||||
(@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
@account.suspended? || (@account.hides_followers? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
|
@ -25,7 +25,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hide_results?
|
def hide_results?
|
||||||
(@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
@account.suspended? || (@account.hides_following? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@proofs = @account.identity_proofs.active
|
@proofs = @account.suspended? ? [] : @account.identity_proofs.active
|
||||||
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
render json: @proofs, each_serializer: REST::IdentityProofSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ class Api::V1::Accounts::ListsController < Api::BaseController
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@lists = @account.lists.where(account: current_account)
|
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account)
|
||||||
render json: @lists, each_serializer: REST::ListSerializer
|
render json: @lists, each_serializer: REST::ListSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
accounts = Account.where(id: account_ids).select('id')
|
accounts = Account.without_suspended.where(id: account_ids).select('id')
|
||||||
# .where doesn't guarantee that our results are in the same order
|
# .where doesn't guarantee that our results are in the same order
|
||||||
# we requested them, so return the "right" order to the requestor.
|
# we requested them, so return the "right" order to the requestor.
|
||||||
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
@accounts = accounts.index_by(&:id).values_at(*account_ids).compact
|
||||||
|
@ -18,7 +18,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def load_statuses
|
def load_statuses
|
||||||
cached_account_statuses
|
@account.suspended? ? [] : cached_account_statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
def cached_account_statuses
|
def cached_account_statuses
|
||||||
|
@ -9,7 +9,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
before_action :require_user!, except: [:show, :create]
|
before_action :require_user!, except: [:show, :create]
|
||||||
before_action :set_account, except: [:create]
|
before_action :set_account, except: [:create]
|
||||||
before_action :check_account_suspension, only: [:show]
|
|
||||||
before_action :check_enabled_registrations, only: [:create]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
@ -31,9 +30,8 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account, reblogs: truthy_param?(:reblogs), with_rate_limit: true)
|
follow = FollowService.new.call(current_user.account, @account, reblogs: params.key?(:reblogs) ? truthy_param?(:reblogs) : nil, notify: params.key?(:notify) ? truthy_param?(:notify) : nil, with_rate_limit: true)
|
||||||
|
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: follow.show_reblogs?, notify: follow.notify? } }, requested_map: { @account.id => false } }
|
||||||
options = @account.locked? || current_user.account.silenced? ? {} : { following_map: { @account.id => { reblogs: truthy_param?(:reblogs) } }, requested_map: { @account.id => false } }
|
|
||||||
|
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
@ -73,10 +71,6 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_account_suspension
|
|
||||||
gone if @account.suspended?
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
params.permit(:username, :email, :password, :agreement, :locale, :reason)
|
||||||
end
|
end
|
||||||
|
@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def reject
|
def reject
|
||||||
authorize @account.user, :reject?
|
authorize @account.user, :reject?
|
||||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||||
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize @account, :destroy?
|
||||||
|
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||||||
def unsuspend
|
def unsuspend
|
||||||
authorize @account, :unsuspend?
|
authorize @account, :unsuspend?
|
||||||
@account.unsuspend!
|
@account.unsuspend!
|
||||||
|
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||||
log_action :unsuspend, @account
|
log_action :unsuspend, @account
|
||||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||||
end
|
end
|
||||||
|
@ -18,6 +18,8 @@ class Api::V1::BlocksController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_blocks
|
def paginated_blocks
|
||||||
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
@paginated_blocks ||= Block.eager_load(target_account: :account_stat)
|
||||||
|
.joins(:target_account)
|
||||||
|
.merge(Account.without_suspended)
|
||||||
.where(account: current_account)
|
.where(account: current_account)
|
||||||
.paginate_by_max_id(
|
.paginate_by_max_id(
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
|
@ -25,7 +25,7 @@ class Api::V1::EndorsementsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def endorsed_accounts
|
def endorsed_accounts
|
||||||
current_account.endorsed_accounts.includes(:account_stat)
|
current_account.endorsed_accounts.includes(:account_stat).without_suspended
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
@ -13,7 +13,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||||||
|
|
||||||
def authorize
|
def authorize
|
||||||
AuthorizeFollowService.new.call(account, current_account)
|
AuthorizeFollowService.new.call(account, current_account)
|
||||||
NotifyService.new.call(current_account, Follow.find_by(account: account, target_account: current_account))
|
NotifyService.new.call(current_account, :follow, Follow.find_by(account: account, target_account: current_account))
|
||||||
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ class Api::V1::FollowRequestsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:follow_requests, :account_stat).references(:follow_requests)
|
Account.without_suspended.includes(:follow_requests, :account_stat).references(:follow_requests)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_follow_requests
|
def paginated_follow_requests
|
||||||
|
@ -37,9 +37,9 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||||||
|
|
||||||
def load_accounts
|
def load_accounts
|
||||||
if unlimited?
|
if unlimited?
|
||||||
@list.accounts.includes(:account_stat).all
|
@list.accounts.without_suspended.includes(:account_stat).all
|
||||||
else
|
else
|
||||||
@list.accounts.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
@list.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -27,6 +27,8 @@ class Api::V1::MutesController < Api::BaseController
|
|||||||
|
|
||||||
def paginated_mutes
|
def paginated_mutes
|
||||||
@paginated_mutes ||= Mute.eager_load(:target_account)
|
@paginated_mutes ||= Mute.eager_load(:target_account)
|
||||||
|
.joins(:target_account)
|
||||||
|
.merge(Account.without_suspended)
|
||||||
.where(account: current_account)
|
.where(account: current_account)
|
||||||
.paginate_by_max_id(
|
.paginate_by_max_id(
|
||||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
|
@ -14,7 +14,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.find(params[:id])
|
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||||
render json: @notification, serializer: REST::NotificationSerializer
|
render json: @notification, serializer: REST::NotificationSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.browserable(exclude_types, from_account)
|
current_account.notifications.without_suspended.browserable(exclude_types, from_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
|
@ -52,6 +52,6 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
|
|||||||
def data_params
|
def data_params
|
||||||
return {} if params[:data].blank?
|
return {} if params[:data].blank?
|
||||||
|
|
||||||
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -22,6 +22,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
|
|||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account
|
Account
|
||||||
|
.without_suspended
|
||||||
.includes(:favourites, :account_stat)
|
.includes(:favourites, :account_stat)
|
||||||
.references(:favourites)
|
.references(:favourites)
|
||||||
.where(favourites: { status_id: @status.id })
|
.where(favourites: { status_id: @status.id })
|
||||||
|
@ -21,7 +21,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_accounts
|
def default_accounts
|
||||||
Account.includes(:statuses, :account_stat).references(:statuses)
|
Account.without_suspended.includes(:statuses, :account_stat).references(:statuses)
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginated_statuses
|
def paginated_statuses
|
||||||
|
@ -22,6 +22,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
reblog: alerts_enabled,
|
reblog: alerts_enabled,
|
||||||
mention: alerts_enabled,
|
mention: alerts_enabled,
|
||||||
poll: alerts_enabled,
|
poll: alerts_enabled,
|
||||||
|
status: alerts_enabled,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,6 +58,6 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def data_params
|
def data_params
|
||||||
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll])
|
@data_params ||= params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll, :status])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,7 +5,6 @@ module ExportControllerConcern
|
|||||||
|
|
||||||
included do
|
included do
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :require_not_suspended!
|
|
||||||
before_action :load_export
|
before_action :load_export
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
@ -30,8 +29,4 @@ module ExportControllerConcern
|
|||||||
def export_filename
|
def export_filename
|
||||||
"#{controller_name}.csv"
|
"#{controller_name}.csv"
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -6,6 +6,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
before_action :require_not_suspended!, only: :destroy
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
@ -30,4 +31,8 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
|||||||
def set_pack
|
def set_pack
|
||||||
use_pack 'settings'
|
use_pack 'settings'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::AliasesController < Settings::BaseController
|
class Settings::AliasesController < Settings::BaseController
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :require_not_suspended!
|
||||||
before_action :set_aliases, except: :destroy
|
before_action :set_aliases, except: :destroy
|
||||||
before_action :set_alias, only: :destroy
|
before_action :set_alias, only: :destroy
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ApplicationsController < Settings::BaseController
|
class Settings::ApplicationsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
before_action :set_application, only: [:show, :update, :destroy, :regenerate]
|
||||||
before_action :prepare_scopes, only: [:create, :update]
|
before_action :prepare_scopes, only: [:create, :update]
|
||||||
|
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
class Settings::BaseController < ApplicationController
|
class Settings::BaseController < ApplicationController
|
||||||
before_action :set_pack
|
before_action :set_pack
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
before_action :set_body_classes
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
@ -18,4 +21,8 @@ class Settings::BaseController < ApplicationController
|
|||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_not_suspended!
|
||||||
|
forbidden if current_account.suspended?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::DeletesController < Settings::BaseController
|
class Settings::DeletesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :check_enabled_deletion
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
before_action :check_enabled_deletion
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::DeleteConfirmation.new
|
@confirmation = Form::DeleteConfirmation.new
|
||||||
end
|
end
|
||||||
@ -46,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||||||
|
|
||||||
def destroy_account!
|
def destroy_account!
|
||||||
current_account.suspend!
|
current_account.suspend!
|
||||||
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
|
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||||
sign_out
|
sign_out
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class BlockedAccountsController < ApplicationController
|
class BlockedAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class BlockedDomainsController < ApplicationController
|
class BlockedDomainsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class FollowingAccountsController < ApplicationController
|
class FollowingAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class ListsController < ApplicationController
|
class ListsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
module Exports
|
module Exports
|
||||||
class MutedAccountsController < ApplicationController
|
class MutedAccountsController < BaseController
|
||||||
include ExportControllerConcern
|
include ExportControllerConcern
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -3,11 +3,6 @@
|
|||||||
class Settings::ExportsController < Settings::BaseController
|
class Settings::ExportsController < Settings::BaseController
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@ -16,8 +11,6 @@ class Settings::ExportsController < Settings::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
raise Mastodon::NotPermittedError unless user_signed_in?
|
|
||||||
|
|
||||||
backup = nil
|
backup = nil
|
||||||
|
|
||||||
RedisLock.acquire(lock_options) do |lock|
|
RedisLock.acquire(lock_options) do |lock|
|
||||||
@ -37,8 +30,4 @@ class Settings::ExportsController < Settings::BaseController
|
|||||||
def lock_options
|
def lock_options
|
||||||
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
{ redis: Redis.current, key: "backup:#{current_user.id}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::FeaturedTagsController < Settings::BaseController
|
class Settings::FeaturedTagsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_featured_tags, only: :index
|
before_action :set_featured_tags, only: :index
|
||||||
before_action :set_featured_tag, except: [:index, :create]
|
before_action :set_featured_tag, except: [:index, :create]
|
||||||
before_action :set_recently_used_tags, only: :index
|
before_action :set_recently_used_tags, only: :index
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::IdentityProofsController < Settings::BaseController
|
class Settings::IdentityProofsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :check_required_params, only: :new
|
before_action :check_required_params, only: :new
|
||||||
before_action :check_enabled, only: :new
|
before_action :check_enabled, only: :new
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ImportsController < Settings::BaseController
|
class Settings::ImportsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::Migration::RedirectsController < Settings::BaseController
|
class Settings::Migration::RedirectsController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
|
||||||
def new
|
def new
|
||||||
@redirect = Form::Redirect.new
|
@redirect = Form::Redirect.new
|
||||||
end
|
end
|
||||||
@ -38,8 +35,4 @@ class Settings::Migration::RedirectsController < Settings::BaseController
|
|||||||
def resource_params
|
def resource_params
|
||||||
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
params.require(:form_redirect).permit(:acct, :current_password, :current_username)
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::MigrationsController < Settings::BaseController
|
class Settings::MigrationsController < Settings::BaseController
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_not_suspended!
|
before_action :require_not_suspended!
|
||||||
before_action :set_migrations
|
before_action :set_migrations
|
||||||
before_action :set_cooldown
|
before_action :set_cooldown
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@migration = current_account.migrations.build
|
@migration = current_account.migrations.build
|
||||||
end
|
end
|
||||||
@ -44,8 +41,4 @@ class Settings::MigrationsController < Settings::BaseController
|
|||||||
def on_cooldown?
|
def on_cooldown?
|
||||||
@cooldown.present?
|
@cooldown.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_not_suspended!
|
|
||||||
forbidden if current_account.suspended?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
module Settings
|
module Settings
|
||||||
class PicturesController < BaseController
|
class PicturesController < BaseController
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
before_action :set_picture
|
before_action :set_picture
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::PreferencesController < Settings::BaseController
|
class Settings::PreferencesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Settings::ProfilesController < Settings::BaseController
|
class Settings::ProfilesController < Settings::BaseController
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Intentionally does not inherit from BaseController
|
class Settings::SessionsController < Settings::BaseController
|
||||||
class Settings::SessionsController < ApplicationController
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_session, only: :destroy
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_not_suspended!
|
||||||
|
before_action :set_session, only: :destroy
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@session.destroy!
|
@session.destroy!
|
||||||
flash[:notice] = I18n.t('sessions.revoke_success')
|
flash[:notice] = I18n.t('sessions.revoke_success')
|
||||||
|
@ -5,14 +5,11 @@ module Settings
|
|||||||
class ConfirmationsController < BaseController
|
class ConfirmationsController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_challenge!
|
before_action :require_challenge!
|
||||||
before_action :ensure_otp_secret
|
before_action :ensure_otp_secret
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def new
|
def new
|
||||||
prepare_two_factor_form
|
prepare_two_factor_form
|
||||||
end
|
end
|
||||||
|
@ -5,14 +5,11 @@ module Settings
|
|||||||
class OtpAuthenticationController < BaseController
|
class OtpAuthenticationController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :verify_otp_not_enabled, only: [:show]
|
before_action :verify_otp_not_enabled, only: [:show]
|
||||||
before_action :require_challenge!, only: [:create]
|
before_action :require_challenge!, only: [:create]
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@confirmation = Form::TwoFactorConfirmation.new
|
@confirmation = Form::TwoFactorConfirmation.new
|
||||||
end
|
end
|
||||||
|
@ -5,13 +5,10 @@ module Settings
|
|||||||
class RecoveryCodesController < BaseController
|
class RecoveryCodesController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_challenge!, on: :create
|
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
|
before_action :require_challenge!, on: :create
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@recovery_codes = current_user.generate_otp_backup_codes!
|
@recovery_codes = current_user.generate_otp_backup_codes!
|
||||||
current_user.save!
|
current_user.save!
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
module Settings
|
module Settings
|
||||||
module TwoFactorAuthentication
|
module TwoFactorAuthentication
|
||||||
class WebauthnCredentialsController < BaseController
|
class WebauthnCredentialsController < BaseController
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_otp_enabled
|
before_action :require_otp_enabled
|
||||||
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
||||||
|
|
||||||
|
@ -4,14 +4,11 @@ module Settings
|
|||||||
class TwoFactorAuthenticationMethodsController < BaseController
|
class TwoFactorAuthenticationMethodsController < BaseController
|
||||||
include ChallengableConcern
|
include ChallengableConcern
|
||||||
|
|
||||||
layout 'admin'
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :require_challenge!, only: :disable
|
before_action :require_challenge!, only: :disable
|
||||||
before_action :require_otp_enabled
|
before_action :require_otp_enabled
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def disable
|
def disable
|
||||||
|
@ -109,14 +109,14 @@ export function fetchAccountFail(id, error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function followAccount(id, reblogs = true) {
|
export function followAccount(id, options = { reblogs: true }) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
|
||||||
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
const locked = getState().getIn(['accounts', id, 'locked'], false);
|
||||||
|
|
||||||
dispatch(followAccountRequest(id, locked));
|
dispatch(followAccountRequest(id, locked));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => {
|
||||||
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
dispatch(followAccountSuccess(response.data, alreadyFollowing));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(followAccountFail(error, locked));
|
dispatch(followAccountFail(error, locked));
|
||||||
|
@ -3,6 +3,9 @@ import { debounce } from 'lodash';
|
|||||||
import compareId from '../compare_id';
|
import compareId from '../compare_id';
|
||||||
import { showAlertForError } from './alerts';
|
import { showAlertForError } from './alerts';
|
||||||
|
|
||||||
|
export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST';
|
||||||
|
export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS';
|
||||||
|
export const MARKERS_FETCH_FAIL = 'MARKERS_FETCH_FAIL';
|
||||||
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
|
export const MARKERS_SUBMIT_SUCCESS = 'MARKERS_SUBMIT_SUCCESS';
|
||||||
|
|
||||||
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
||||||
@ -57,8 +60,8 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
|
|||||||
const _buildParams = (state) => {
|
const _buildParams = (state) => {
|
||||||
const params = {};
|
const params = {};
|
||||||
|
|
||||||
const lastHomeId = state.getIn(['timelines', 'home', 'items', 0]);
|
const lastHomeId = state.getIn(['timelines', 'home', 'items']).find(item => item !== null);
|
||||||
const lastNotificationId = state.getIn(['notifications', 'items', 0, 'id']);
|
const lastNotificationId = state.getIn(['notifications', 'lastReadId']);
|
||||||
|
|
||||||
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
if (lastHomeId && compareId(lastHomeId, state.getIn(['markers', 'home'])) > 0) {
|
||||||
params.home = {
|
params.home = {
|
||||||
@ -100,3 +103,39 @@ export function submitMarkersSuccess({ home, notifications }) {
|
|||||||
export function submitMarkers() {
|
export function submitMarkers() {
|
||||||
return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
|
return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const fetchMarkers = () => (dispatch, getState) => {
|
||||||
|
const params = { timeline: ['notifications'] };
|
||||||
|
|
||||||
|
dispatch(fetchMarkersRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/markers', { params }).then(response => {
|
||||||
|
dispatch(fetchMarkersSuccess(response.data));
|
||||||
|
}).catch(error => {
|
||||||
|
dispatch(fetchMarkersFail(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMarkersRequest() {
|
||||||
|
return {
|
||||||
|
type: MARKERS_FETCH_REQUEST,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMarkersSuccess(markers) {
|
||||||
|
return {
|
||||||
|
type: MARKERS_FETCH_SUCCESS,
|
||||||
|
markers,
|
||||||
|
skipLoading: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function fetchMarkersFail(error) {
|
||||||
|
return {
|
||||||
|
type: MARKERS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
skipLoading: true,
|
||||||
|
skipAlert: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -33,6 +33,8 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
|||||||
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT';
|
||||||
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
@ -59,7 +61,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
|
|||||||
|
|
||||||
let filtered = false;
|
let filtered = false;
|
||||||
|
|
||||||
if (notification.type === 'mention') {
|
if (['mention', 'status'].includes(notification.type)) {
|
||||||
const dropRegex = filters[0];
|
const dropRegex = filters[0];
|
||||||
const regex = filters[1];
|
const regex = filters[1];
|
||||||
const searchIndex = searchTextFromRawStatus(notification.status);
|
const searchIndex = searchTextFromRawStatus(notification.status);
|
||||||
|
38
app/javascript/mastodon/actions/picture_in_picture.js
Normal file
38
app/javascript/mastodon/actions/picture_in_picture.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
export const PICTURE_IN_PICTURE_DEPLOY = 'PICTURE_IN_PICTURE_DEPLOY';
|
||||||
|
export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef MediaProps
|
||||||
|
* @property {string} src
|
||||||
|
* @property {boolean} muted
|
||||||
|
* @property {number} volume
|
||||||
|
* @property {number} currentTime
|
||||||
|
* @property {string} poster
|
||||||
|
* @property {string} backgroundColor
|
||||||
|
* @property {string} foregroundColor
|
||||||
|
* @property {string} accentColor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} statusId
|
||||||
|
* @param {string} accountId
|
||||||
|
* @param {string} playerType
|
||||||
|
* @param {MediaProps} props
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
|
||||||
|
type: PICTURE_IN_PICTURE_DEPLOY,
|
||||||
|
statusId,
|
||||||
|
accountId,
|
||||||
|
playerType,
|
||||||
|
props,
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @return {object}
|
||||||
|
*/
|
||||||
|
export const removePictureInPicture = () => ({
|
||||||
|
type: PICTURE_IN_PICTURE_REMOVE,
|
||||||
|
});
|
@ -5,10 +5,21 @@ import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
|||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { reduceMotion } from 'mastodon/initial_state';
|
import { reduceMotion } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
const obfuscatedCount = count => {
|
||||||
|
if (count < 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (count <= 1) {
|
||||||
|
return count;
|
||||||
|
} else {
|
||||||
|
return '1+';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default class AnimatedNumber extends React.PureComponent {
|
export default class AnimatedNumber extends React.PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
value: PropTypes.number.isRequired,
|
value: PropTypes.number.isRequired,
|
||||||
|
obfuscate: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -36,11 +47,11 @@ export default class AnimatedNumber extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { value } = this.props;
|
const { value, obfuscate } = this.props;
|
||||||
const { direction } = this.state;
|
const { direction } = this.state;
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
return <FormattedNumber value={value} />;
|
return obfuscate ? obfuscatedCount(value) : <FormattedNumber value={value} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = [{
|
const styles = [{
|
||||||
@ -54,7 +65,7 @@ export default class AnimatedNumber extends React.PureComponent {
|
|||||||
{items => (
|
{items => (
|
||||||
<span className='animated-number'>
|
<span className='animated-number'>
|
||||||
{items.map(({ key, data, style }) => (
|
{items.map(({ key, data, style }) => (
|
||||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}><FormattedNumber value={data} /></span>
|
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
@ -66,17 +66,31 @@ export default class ErrorBoundary extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { hasError, copied } = this.state;
|
const { hasError, copied, errorMessage } = this.state;
|
||||||
|
|
||||||
if (!hasError) {
|
if (!hasError) {
|
||||||
return this.props.children;
|
return this.props.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const likelyBrowserAddonIssue = errorMessage && errorMessage.includes('NotFoundError');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='error-boundary'>
|
<div className='error-boundary'>
|
||||||
<div>
|
<div>
|
||||||
<p className='error-boundary__error'><FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' /></p>
|
<p className='error-boundary__error'>
|
||||||
<p><FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' /></p>
|
{ likelyBrowserAddonIssue ? (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.explanation_addons' defaultMessage='This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.explanation' defaultMessage='Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.' />
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ likelyBrowserAddonIssue ? (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.next_steps_addons' defaultMessage='Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id='error.unexpected_crash.next_steps' defaultMessage='Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.' />
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
<p className='error-boundary__footer'>Mastodon v{version} · <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='errors.unexpected_crash.report_issue' defaultMessage='Report issue' /></a> · <button onClick={this.handleCopyStackTrace} className={copied ? 'copied' : ''}><FormattedMessage id='errors.unexpected_crash.copy_stacktrace' defaultMessage='Copy stacktrace to clipboard' /></button></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||||
|
|
||||||
export default class IconButton extends React.PureComponent {
|
export default class IconButton extends React.PureComponent {
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ export default class IconButton extends React.PureComponent {
|
|||||||
animate: PropTypes.bool,
|
animate: PropTypes.bool,
|
||||||
overlay: PropTypes.bool,
|
overlay: PropTypes.bool,
|
||||||
tabIndex: PropTypes.string,
|
tabIndex: PropTypes.string,
|
||||||
|
counter: PropTypes.number,
|
||||||
|
obfuscateCount: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -97,6 +100,8 @@ export default class IconButton extends React.PureComponent {
|
|||||||
pressed,
|
pressed,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
title,
|
title,
|
||||||
|
counter,
|
||||||
|
obfuscateCount,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -113,6 +118,10 @@ export default class IconButton extends React.PureComponent {
|
|||||||
overlayed: overlay,
|
overlayed: overlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof counter !== 'undefined') {
|
||||||
|
style.width = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
@ -128,7 +137,7 @@ export default class IconButton extends React.PureComponent {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Icon id={icon} fixedWidth aria-hidden='true' />
|
<Icon id={icon} fixedWidth aria-hidden='true' /> {typeof counter !== 'undefined' && <span className='icon-button__counter'><AnimatedNumber value={counter} obfuscate={obfuscateCount} /></span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
||||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
||||||
import { is } from 'immutable';
|
|
||||||
|
|
||||||
// Diff these props in the "rendered" state
|
|
||||||
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
|
|
||||||
// Diff these props in the "unrendered" state
|
// Diff these props in the "unrendered" state
|
||||||
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
|
||||||
|
|
||||||
@ -33,9 +30,12 @@ export default class IntersectionObserverArticle extends React.Component {
|
|||||||
// If we're going from rendered to unrendered (or vice versa) then update
|
// If we're going from rendered to unrendered (or vice versa) then update
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Otherwise, diff based on props
|
// If we are and remain hidden, diff based on props
|
||||||
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
|
if (isUnrendered) {
|
||||||
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
|
return !updateOnPropsForUnrendered.every(prop => nextProps[prop] === this.props[prop]);
|
||||||
|
}
|
||||||
|
// Else, assume the children have changed
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
export default @connect()
|
||||||
|
class PictureInPicturePlaceholder extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
width: PropTypes.number,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
width: this.props.width,
|
||||||
|
height: this.props.width && (this.props.width / (16/9)),
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(removePictureInPicture());
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.node = c;
|
||||||
|
|
||||||
|
if (this.node) {
|
||||||
|
this._setDimensions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setDimensions () {
|
||||||
|
const width = this.node.offsetWidth;
|
||||||
|
const height = width / (16/9);
|
||||||
|
|
||||||
|
this.setState({ width, height });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleResize = debounce(() => {
|
||||||
|
if (this.node) {
|
||||||
|
this._setDimensions();
|
||||||
|
}
|
||||||
|
}, 250, {
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { height } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
|
||||||
|
<Icon id='window-restore' />
|
||||||
|
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -17,6 +17,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import { displayMedia } from '../initial_state';
|
import { displayMedia } from '../initial_state';
|
||||||
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
@ -95,6 +96,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
|
deployPictureInPicture: PropTypes.func,
|
||||||
|
usingPiP: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
@ -104,6 +107,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
'account',
|
'account',
|
||||||
'muted',
|
'muted',
|
||||||
'hidden',
|
'hidden',
|
||||||
|
'unread',
|
||||||
|
'usingPiP',
|
||||||
];
|
];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -205,6 +210,13 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDeployPictureInPicture = (type, mediaProps) => {
|
||||||
|
const { deployPictureInPicture } = this.props;
|
||||||
|
const status = this._properStatus();
|
||||||
|
|
||||||
|
deployPictureInPicture(status, type, mediaProps);
|
||||||
|
}
|
||||||
|
|
||||||
handleHotkeyReply = e => {
|
handleHotkeyReply = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onReply(this._properStatus(), this.context.router.history);
|
this.props.onReply(this._properStatus(), this.context.router.history);
|
||||||
@ -265,7 +277,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar, prepend, rebloggedByText;
|
let statusAvatar, prepend, rebloggedByText;
|
||||||
|
|
||||||
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey } = this.props;
|
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, usingPiP } = this.props;
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
@ -336,7 +348,9 @@ class Status extends ImmutablePureComponent {
|
|||||||
status = status.get('reblog');
|
status = status.get('reblog');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (usingPiP) {
|
||||||
|
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
|
||||||
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (this.props.muted) {
|
if (this.props.muted) {
|
||||||
media = (
|
media = (
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
@ -361,6 +375,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
width={this.props.cachedMediaWidth}
|
width={this.props.cachedMediaWidth}
|
||||||
height={110}
|
height={110}
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
@ -382,6 +397,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
cacheWidth={this.props.cacheMediaWidth}
|
cacheWidth={this.props.cacheMediaWidth}
|
||||||
|
deployPictureInPicture={this.handleDeployPictureInPicture}
|
||||||
visible={this.state.showMedia}
|
visible={this.state.showMedia}
|
||||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||||
/>
|
/>
|
||||||
@ -438,10 +454,10 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
|
||||||
{prepend}
|
{prepend}
|
||||||
|
|
||||||
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
|
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
|
||||||
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
|
||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener noreferrer'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
|
@ -43,16 +43,6 @@ const messages = defineMessages({
|
|||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const obfuscatedCount = count => {
|
|
||||||
if (count < 0) {
|
|
||||||
return 0;
|
|
||||||
} else if (count <= 1) {
|
|
||||||
return count;
|
|
||||||
} else {
|
|
||||||
return '1+';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { status }) => ({
|
const mapStateToProps = (state, { status }) => ({
|
||||||
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
|
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
|
||||||
});
|
});
|
||||||
@ -329,9 +319,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='status__action-bar'>
|
<div className='status__action-bar'>
|
||||||
<div className='status__action-bar__counter'><IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /><span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span></div>
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
|
||||||
|
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
|
@ -37,6 +37,7 @@ import { initMuteModal } from '../actions/mutes';
|
|||||||
import { initBlockModal } from '../actions/blocks';
|
import { initBlockModal } from '../actions/blocks';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
|
import { deployPictureInPicture } from '../actions/picture_in_picture';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { boostModal, deleteModal } from '../initial_state';
|
import { boostModal, deleteModal } from '../initial_state';
|
||||||
import { showAlertForError } from '../actions/alerts';
|
import { showAlertForError } from '../actions/alerts';
|
||||||
@ -56,6 +57,7 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
status: getStatus(state, props),
|
status: getStatus(state, props),
|
||||||
|
usingPiP: state.get('picture_in_picture').statusId === props.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
@ -207,6 +209,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
dispatch(unblockDomain(domain));
|
dispatch(unblockDomain(domain));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deployPictureInPicture (status, type, mediaProps) {
|
||||||
|
dispatch(deployPictureInPicture(status.get('id'), status.getIn(['account', 'id']), type, mediaProps));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
import Avatar from 'mastodon/components/avatar';
|
import Avatar from 'mastodon/components/avatar';
|
||||||
import { counterRenderer } from 'mastodon/components/common_counter';
|
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||||
import ShortNumber from 'mastodon/components/short_number';
|
import ShortNumber from 'mastodon/components/short_number';
|
||||||
@ -35,6 +36,8 @@ const messages = defineMessages({
|
|||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||||
|
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||||
|
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
|
|||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
onDirect: PropTypes.func.isRequired,
|
onDirect: PropTypes.func.isRequired,
|
||||||
onReport: PropTypes.func.isRequired,
|
|
||||||
onReblogToggle: PropTypes.func.isRequired,
|
onReblogToggle: PropTypes.func.isRequired,
|
||||||
|
onNotifyToggle: PropTypes.func.isRequired,
|
||||||
|
onReport: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
onBlockDomain: PropTypes.func.isRequired,
|
onBlockDomain: PropTypes.func.isRequired,
|
||||||
onUnblockDomain: PropTypes.func.isRequired,
|
onUnblockDomain: PropTypes.func.isRequired,
|
||||||
@ -140,8 +144,11 @@ class Header extends ImmutablePureComponent {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const suspended = account.get('suspended');
|
||||||
|
|
||||||
let info = [];
|
let info = [];
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
|
let bellBtn = '';
|
||||||
let lockedIcon = '';
|
let lockedIcon = '';
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
@ -171,6 +178,10 @@ class Header extends ImmutablePureComponent {
|
|||||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||||
|
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||||
actionBtn = '';
|
actionBtn = '';
|
||||||
}
|
}
|
||||||
@ -268,7 +279,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||||
<div className='account__header__image'>
|
<div className='account__header__image'>
|
||||||
<div className='account__header__info'>
|
<div className='account__header__info'>
|
||||||
{info}
|
{!suspended && info}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||||
@ -282,11 +293,14 @@ class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
<div className='spacer' />
|
<div className='spacer' />
|
||||||
|
|
||||||
|
{!suspended && (
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{actionBtn}
|
{actionBtn}
|
||||||
|
{bellBtn}
|
||||||
|
|
||||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='account__header__tabs__name'>
|
<div className='account__header__tabs__name'>
|
||||||
@ -298,7 +312,7 @@ 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 || identity_proofs.size > 0) && (
|
||||||
<div className='account__header__fields'>
|
<div className='account__header__fields'>
|
||||||
{identity_proofs.map((proof, i) => (
|
{identity_proofs.map((proof, i) => (
|
||||||
<dl key={i}>
|
<dl key={i}>
|
||||||
@ -324,11 +338,12 @@ class Header extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.get('id') !== me && <AccountNoteContainer account={account} />}
|
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
|
||||||
|
|
||||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!suspended && (
|
||||||
<div className='account__header__extra__links'>
|
<div className='account__header__extra__links'>
|
||||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||||
<ShortNumber
|
<ShortNumber
|
||||||
@ -351,6 +366,7 @@ class Header extends ImmutablePureComponent {
|
|||||||
/>
|
/>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
|||||||
import LoadMore from 'mastodon/components/load_more';
|
import LoadMore from 'mastodon/components/load_more';
|
||||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||||
attachments: getAccountGallery(state, props.params.accountId),
|
attachments: getAccountGallery(state, props.params.accountId),
|
||||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||||
|
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
|
||||||
|
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||||
});
|
});
|
||||||
|
|
||||||
class LoadMoreMedia extends ImmutablePureComponent {
|
class LoadMoreMedia extends ImmutablePureComponent {
|
||||||
@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
blockedBy: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -119,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
|
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
|
||||||
const { width } = this.state;
|
const { width } = this.state;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
@ -152,6 +157,11 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||||
<HeaderContainer accountId={this.props.params.accountId} />
|
<HeaderContainer accountId={this.props.params.accountId} />
|
||||||
|
|
||||||
|
{(suspended || blockedBy) ? (
|
||||||
|
<div className='empty-column-indicator'>
|
||||||
|
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||||
{attachments.map((attachment, index) => attachment === null ? (
|
{attachments.map((attachment, index) => attachment === null ? (
|
||||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||||
@ -161,6 +171,7 @@ class AccountGallery extends ImmutablePureComponent {
|
|||||||
|
|
||||||
{loadOlder}
|
{loadOlder}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading && attachments.size === 0 && (
|
{isLoading && attachments.size === 0 && (
|
||||||
<div className='scrollable__append'>
|
<div className='scrollable__append'>
|
||||||
|
@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
this.props.onReblogToggle(this.props.account);
|
this.props.onReblogToggle(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleNotifyToggle = () => {
|
||||||
|
this.props.onNotifyToggle(this.props.account);
|
||||||
|
}
|
||||||
|
|
||||||
handleMute = () => {
|
handleMute = () => {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
}
|
||||||
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
onDirect={this.handleDirect}
|
onDirect={this.handleDirect}
|
||||||
onReblogToggle={this.handleReblogToggle}
|
onReblogToggle={this.handleReblogToggle}
|
||||||
|
onNotifyToggle={this.handleNotifyToggle}
|
||||||
onReport={this.handleReport}
|
onReport={this.handleReport}
|
||||||
onMute={this.handleMute}
|
onMute={this.handleMute}
|
||||||
onBlockDomain={this.handleBlockDomain}
|
onBlockDomain={this.handleBlockDomain}
|
||||||
|
@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
|
|
||||||
onReblogToggle (account) {
|
onReblogToggle (account) {
|
||||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||||
dispatch(followAccount(account.get('id'), false));
|
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id'), true));
|
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onNotifyToggle (account) {
|
||||||
|
if (account.getIn(['relationship', 'notifying'])) {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: false }));
|
||||||
|
} else {
|
||||||
|
dispatch(followAccount(account.get('id'), { notify: true }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onReport (account) {
|
onReport (account) {
|
||||||
dispatch(initReport(account));
|
dispatch(initReport(account));
|
||||||
},
|
},
|
||||||
|
@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
|
|||||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
withReplies: PropTypes.bool,
|
withReplies: PropTypes.bool,
|
||||||
blockedBy: PropTypes.bool,
|
blockedBy: PropTypes.bool,
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
|
suspended: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
remote: PropTypes.bool,
|
||||||
remoteUrl: PropTypes.string,
|
remoteUrl: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (!isAccount) {
|
if (!isAccount) {
|
||||||
return (
|
return (
|
||||||
@ -134,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
|
|
||||||
let emptyMessage;
|
let emptyMessage;
|
||||||
|
|
||||||
if (blockedBy) {
|
if (suspended || blockedBy) {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||||
} else if (remote && statusIds.isEmpty()) {
|
} else if (remote && statusIds.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||||
@ -153,7 +155,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
|||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={remoteMessage}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={blockedBy ? emptyList : statusIds}
|
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
featuredStatusIds={featuredStatusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
@ -37,7 +37,11 @@ class Audio extends React.PureComponent {
|
|||||||
backgroundColor: PropTypes.string,
|
backgroundColor: PropTypes.string,
|
||||||
foregroundColor: PropTypes.string,
|
foregroundColor: PropTypes.string,
|
||||||
accentColor: PropTypes.string,
|
accentColor: PropTypes.string,
|
||||||
|
currentTime: PropTypes.number,
|
||||||
autoPlay: PropTypes.bool,
|
autoPlay: PropTypes.bool,
|
||||||
|
volume: PropTypes.number,
|
||||||
|
muted: PropTypes.bool,
|
||||||
|
deployPictureInPicture: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -64,6 +68,19 @@ class Audio extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pack() {
|
||||||
|
return {
|
||||||
|
src: this.props.src,
|
||||||
|
volume: this.audio.volume,
|
||||||
|
muted: this.audio.muted,
|
||||||
|
currentTime: this.audio.currentTime,
|
||||||
|
poster: this.props.poster,
|
||||||
|
backgroundColor: this.props.backgroundColor,
|
||||||
|
foregroundColor: this.props.foregroundColor,
|
||||||
|
accentColor: this.props.accentColor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_setDimensions () {
|
_setDimensions () {
|
||||||
const width = this.player.offsetWidth;
|
const width = this.player.offsetWidth;
|
||||||
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
||||||
@ -112,6 +129,10 @@ class Audio extends React.PureComponent {
|
|||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
window.removeEventListener('scroll', this.handleScroll);
|
window.removeEventListener('scroll', this.handleScroll);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
|
||||||
|
if (!this.state.paused && this.audio && this.props.deployPictureInPicture) {
|
||||||
|
this.props.deployPictureInPicture('audio', this._pack());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePlay = () => {
|
togglePlay = () => {
|
||||||
@ -248,7 +269,13 @@ class Audio extends React.PureComponent {
|
|||||||
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
||||||
|
|
||||||
if (!this.state.paused && !inView) {
|
if (!this.state.paused && !inView) {
|
||||||
this.setState({ paused: true }, () => this.audio.pause());
|
this.audio.pause();
|
||||||
|
|
||||||
|
if (this.props.deployPictureInPicture) {
|
||||||
|
this.props.deployPictureInPicture('audio', this._pack());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ paused: true });
|
||||||
}
|
}
|
||||||
}, 150, { trailing: true });
|
}, 150, { trailing: true });
|
||||||
|
|
||||||
@ -261,10 +288,22 @@ class Audio extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLoadedData = () => {
|
handleLoadedData = () => {
|
||||||
const { autoPlay } = this.props;
|
const { autoPlay, currentTime, volume, muted } = this.props;
|
||||||
|
|
||||||
|
if (currentTime) {
|
||||||
|
this.audio.currentTime = currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volume !== undefined) {
|
||||||
|
this.audio.volume = volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (muted !== undefined) {
|
||||||
|
this.audio.muted = muted;
|
||||||
|
}
|
||||||
|
|
||||||
if (autoPlay) {
|
if (autoPlay) {
|
||||||
this.audio.play();
|
this.togglePlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -350,7 +389,7 @@ class Audio extends React.PureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const { src, intl, alt, editable, autoPlay } = this.props;
|
const { src, intl, alt, editable, autoPlay } = this.props;
|
||||||
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
|
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
|
||||||
const progress = (currentTime / duration) * 100;
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||||
|
@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Emoji requiring extra borders depending on theme
|
// Emoji requiring extra borders depending on theme
|
||||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞']);
|
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
|
||||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
||||||
|
|
||||||
const emojiFilename = (filename) => {
|
const emojiFilename = (filename) => {
|
||||||
|
@ -9,6 +9,7 @@ const tooltips = defineMessages({
|
|||||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
|
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @injectIntl
|
export default @injectIntl
|
||||||
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
|
|||||||
>
|
>
|
||||||
<Icon id='tasks' fixedWidth />
|
<Icon id='tasks' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedFilter === 'status' ? 'active' : ''}
|
||||||
|
onClick={this.onClick('status')}
|
||||||
|
title={intl.formatMessage(tooltips.statuses)}
|
||||||
|
>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'follow' ? 'active' : ''}
|
className={selectedFilter === 'follow' ? 'active' : ''}
|
||||||
onClick={this.onClick('follow')}
|
onClick={this.onClick('follow')}
|
||||||
|
@ -10,6 +10,7 @@ import AccountContainer from 'mastodon/containers/account_container';
|
|||||||
import FollowRequestContainer from '../containers/follow_request_container';
|
import FollowRequestContainer from '../containers/follow_request_container';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import Permalink from 'mastodon/components/permalink';
|
import Permalink from 'mastodon/components/permalink';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
|
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
|
||||||
@ -17,6 +18,7 @@ const messages = defineMessages({
|
|||||||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||||
|
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||||
@ -49,6 +51,7 @@ class Notification extends ImmutablePureComponent {
|
|||||||
updateScrollBottom: PropTypes.func,
|
updateScrollBottom: PropTypes.func,
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
|
unread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMoveUp = () => {
|
handleMoveUp = () => {
|
||||||
@ -113,11 +116,11 @@ class Notification extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFollow (notification, account, link) {
|
renderFollow (notification, account, link) {
|
||||||
const { intl } = this.props;
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='user-plus' fixedWidth />
|
<Icon id='user-plus' fixedWidth />
|
||||||
@ -135,11 +138,11 @@ class Notification extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFollowRequest (notification, account, link) {
|
renderFollowRequest (notification, account, link) {
|
||||||
const { intl } = this.props;
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='user' fixedWidth />
|
<Icon id='user' fixedWidth />
|
||||||
@ -169,16 +172,17 @@ class Notification extends ImmutablePureComponent {
|
|||||||
updateScrollBottom={this.props.updateScrollBottom}
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
unread={this.props.unread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFavourite (notification, link) {
|
renderFavourite (notification, link) {
|
||||||
const { intl } = this.props;
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='star' className='star-icon' fixedWidth />
|
<Icon id='star' className='star-icon' fixedWidth />
|
||||||
@ -206,11 +210,11 @@ class Notification extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderReblog (notification, link) {
|
renderReblog (notification, link) {
|
||||||
const { intl } = this.props;
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='retweet' fixedWidth />
|
<Icon id='retweet' fixedWidth />
|
||||||
@ -237,14 +241,46 @@ class Notification extends ImmutablePureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatus (notification, link) {
|
||||||
|
const { intl, unread } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
|
<div className={classNames('notification notification-status focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||||
|
<div className='notification__message'>
|
||||||
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
|
<Icon id='home' fixedWidth />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span title={notification.get('created_at')}>
|
||||||
|
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatusContainer
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
muted
|
||||||
|
withDismiss
|
||||||
|
hidden={this.props.hidden}
|
||||||
|
getScrollPosition={this.props.getScrollPosition}
|
||||||
|
updateScrollBottom={this.props.updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderPoll (notification, account) {
|
renderPoll (notification, account) {
|
||||||
const { intl } = this.props;
|
const { intl, unread } = this.props;
|
||||||
const ownPoll = me === account.get('id');
|
const ownPoll = me === account.get('id');
|
||||||
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
|
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
<HotKeys handlers={this.getHandlers()}>
|
||||||
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||||
<div className='notification__message'>
|
<div className='notification__message'>
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
<div className='notification__favourite-icon-wrapper'>
|
||||||
<Icon id='tasks' fixedWidth />
|
<Icon id='tasks' fixedWidth />
|
||||||
@ -292,6 +328,8 @@ class Notification extends ImmutablePureComponent {
|
|||||||
return this.renderFavourite(notification, link);
|
return this.renderFavourite(notification, link);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return this.renderReblog(notification, link);
|
return this.renderReblog(notification, link);
|
||||||
|
case 'status':
|
||||||
|
return this.renderStatus(notification, link);
|
||||||
case 'poll':
|
case 'poll':
|
||||||
return this.renderPoll(notification, account);
|
return this.renderPoll(notification, account);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,14 @@ import PropTypes from 'prop-types';
|
|||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
import ColumnHeader from '../../components/column_header';
|
||||||
import { expandNotifications, scrollTopNotifications, loadPending, mountNotifications, unmountNotifications } from '../../actions/notifications';
|
import {
|
||||||
|
expandNotifications,
|
||||||
|
scrollTopNotifications,
|
||||||
|
loadPending,
|
||||||
|
mountNotifications,
|
||||||
|
unmountNotifications,
|
||||||
|
markNotificationsAsRead,
|
||||||
|
} from '../../actions/notifications';
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import NotificationContainer from './containers/notification_container';
|
import NotificationContainer from './containers/notification_container';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
@ -15,9 +22,12 @@ import { List as ImmutableList } from 'immutable';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import LoadGap from '../../components/load_gap';
|
import LoadGap from '../../components/load_gap';
|
||||||
|
import Icon from 'mastodon/components/icon';
|
||||||
|
import compareId from 'mastodon/compare_id';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
|
markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNotifications = createSelector([
|
const getNotifications = createSelector([
|
||||||
@ -32,7 +42,7 @@ const getNotifications = createSelector([
|
|||||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||||
}
|
}
|
||||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
return notifications.filter(item => item === null || allowedType === item.get('type'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
@ -42,6 +52,8 @@ const mapStateToProps = state => ({
|
|||||||
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
|
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
|
||||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||||
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
||||||
|
lastReadId: state.getIn(['notifications', 'readMarkerId']),
|
||||||
|
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
@ -60,6 +72,8 @@ class Notifications extends React.PureComponent {
|
|||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
numPending: PropTypes.number,
|
numPending: PropTypes.number,
|
||||||
|
lastReadId: PropTypes.string,
|
||||||
|
canMarkAsRead: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@ -146,8 +160,12 @@ class Notifications extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMarkAsRead = () => {
|
||||||
|
this.props.dispatch(markNotificationsAsRead());
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
|
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
||||||
|
|
||||||
@ -174,6 +192,7 @@ class Notifications extends React.PureComponent {
|
|||||||
accountId={item.get('account')}
|
accountId={item.get('account')}
|
||||||
onMoveUp={this.handleMoveUp}
|
onMoveUp={this.handleMoveUp}
|
||||||
onMoveDown={this.handleMoveDown}
|
onMoveDown={this.handleMoveDown}
|
||||||
|
unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
@ -202,6 +221,21 @@ class Notifications extends React.PureComponent {
|
|||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let extraButton = null;
|
||||||
|
|
||||||
|
if (canMarkAsRead) {
|
||||||
|
extraButton = (
|
||||||
|
<button
|
||||||
|
aria-label={intl.formatMessage(messages.markAsRead)}
|
||||||
|
title={intl.formatMessage(messages.markAsRead)}
|
||||||
|
onClick={this.handleMarkAsRead}
|
||||||
|
className='column-header__button'
|
||||||
|
>
|
||||||
|
<Icon id='check' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
|
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
|
||||||
<ColumnHeader
|
<ColumnHeader
|
||||||
@ -213,6 +247,7 @@ class Notifications extends React.PureComponent {
|
|||||||
onClick={this.handleHeaderClick}
|
onClick={this.handleHeaderClick}
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
|
extraButton={extraButton}
|
||||||
>
|
>
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
@ -0,0 +1,137 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { me, boostModal } from 'mastodon/initial_state';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { replyCompose } from 'mastodon/actions/compose';
|
||||||
|
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
||||||
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
reply: { id: 'status.reply', defaultMessage: 'Reply' },
|
||||||
|
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
|
||||||
|
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
|
||||||
|
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
|
||||||
|
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
|
||||||
|
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' },
|
||||||
|
favourite: { id: 'status.favourite', defaultMessage: 'Favourite' },
|
||||||
|
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||||
|
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeMapStateToProps = () => {
|
||||||
|
const getStatus = makeGetStatus();
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { statusId }) => ({
|
||||||
|
status: getStatus(state, { id: statusId }),
|
||||||
|
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapStateToProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default @connect(makeMapStateToProps)
|
||||||
|
@injectIntl
|
||||||
|
class Footer extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
askReplyConfirmation: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
_performReply = () => {
|
||||||
|
const { dispatch, status } = this.props;
|
||||||
|
dispatch(replyCompose(status, this.context.router.history));
|
||||||
|
};
|
||||||
|
|
||||||
|
handleReplyClick = () => {
|
||||||
|
const { dispatch, askReplyConfirmation, intl } = this.props;
|
||||||
|
|
||||||
|
if (askReplyConfirmation) {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
message: intl.formatMessage(messages.replyMessage),
|
||||||
|
confirm: intl.formatMessage(messages.replyConfirm),
|
||||||
|
onConfirm: this._performReply,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this._performReply();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFavouriteClick = () => {
|
||||||
|
const { dispatch, status } = this.props;
|
||||||
|
|
||||||
|
if (status.get('favourited')) {
|
||||||
|
dispatch(unfavourite(status));
|
||||||
|
} else {
|
||||||
|
dispatch(favourite(status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_performReblog = () => {
|
||||||
|
const { dispatch, status } = this.props;
|
||||||
|
dispatch(reblog(status));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReblogClick = e => {
|
||||||
|
const { dispatch, status } = this.props;
|
||||||
|
|
||||||
|
if (status.get('reblogged')) {
|
||||||
|
dispatch(unreblog(status));
|
||||||
|
} else if ((e && e.shiftKey) || !boostModal) {
|
||||||
|
this._performReblog();
|
||||||
|
} else {
|
||||||
|
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||||
|
|
||||||
|
let replyIcon, replyTitle;
|
||||||
|
|
||||||
|
if (status.get('in_reply_to_id', null) === null) {
|
||||||
|
replyIcon = 'reply';
|
||||||
|
replyTitle = intl.formatMessage(messages.reply);
|
||||||
|
} else {
|
||||||
|
replyIcon = 'reply-all';
|
||||||
|
replyTitle = intl.formatMessage(messages.replyAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reblogTitle = '';
|
||||||
|
|
||||||
|
if (status.get('reblogged')) {
|
||||||
|
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||||
|
} else if (publicStatus) {
|
||||||
|
reblogTitle = intl.formatMessage(messages.reblog);
|
||||||
|
} else if (reblogPrivate) {
|
||||||
|
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||||
|
} else {
|
||||||
|
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='picture-in-picture__footer'>
|
||||||
|
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||||
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||||
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import IconButton from 'mastodon/components/icon_button';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import Avatar from 'mastodon/components/avatar';
|
||||||
|
import DisplayName from 'mastodon/components/display_name';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, { accountId }) => ({
|
||||||
|
account: state.getIn(['accounts', accountId]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
statusId: PropTypes.string.isRequired,
|
||||||
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, statusId, onClose } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='picture-in-picture__header'>
|
||||||
|
<Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'>
|
||||||
|
<Avatar account={account} size={36} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<IconButton icon='times' onClick={onClose} title='Close' />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal file
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Video from 'mastodon/features/video';
|
||||||
|
import Audio from 'mastodon/features/audio';
|
||||||
|
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||||
|
import Header from './components/header';
|
||||||
|
import Footer from './components/footer';
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
...state.get('picture_in_picture'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
|
class PictureInPicture extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
statusId: PropTypes.string,
|
||||||
|
accountId: PropTypes.string,
|
||||||
|
type: PropTypes.string,
|
||||||
|
src: PropTypes.string,
|
||||||
|
muted: PropTypes.bool,
|
||||||
|
volume: PropTypes.number,
|
||||||
|
currentTime: PropTypes.number,
|
||||||
|
poster: PropTypes.string,
|
||||||
|
backgroundColor: PropTypes.string,
|
||||||
|
foregroundColor: PropTypes.string,
|
||||||
|
accentColor: PropTypes.string,
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
dispatch(removePictureInPicture());
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { type, src, currentTime, accountId, statusId } = this.props;
|
||||||
|
|
||||||
|
if (!currentTime) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let player;
|
||||||
|
|
||||||
|
if (type === 'video') {
|
||||||
|
player = (
|
||||||
|
<Video
|
||||||
|
src={src}
|
||||||
|
currentTime={this.props.currentTime}
|
||||||
|
volume={this.props.volume}
|
||||||
|
muted={this.props.muted}
|
||||||
|
autoPlay
|
||||||
|
inline
|
||||||
|
alwaysVisible
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (type === 'audio') {
|
||||||
|
player = (
|
||||||
|
<Audio
|
||||||
|
src={src}
|
||||||
|
currentTime={this.props.currentTime}
|
||||||
|
volume={this.props.volume}
|
||||||
|
muted={this.props.muted}
|
||||||
|
poster={this.props.poster}
|
||||||
|
backgroundColor={this.props.backgroundColor}
|
||||||
|
foregroundColor={this.props.foregroundColor}
|
||||||
|
accentColor={this.props.accentColor}
|
||||||
|
autoPlay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='picture-in-picture'>
|
||||||
|
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
|
||||||
|
|
||||||
|
{player}
|
||||||
|
|
||||||
|
<Footer statusId={statusId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -15,6 +15,7 @@ import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Icon from 'mastodon/components/icon';
|
import Icon from 'mastodon/components/icon';
|
||||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||||
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||||
@ -40,6 +41,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
compact: PropTypes.bool,
|
compact: PropTypes.bool,
|
||||||
showMedia: PropTypes.bool,
|
showMedia: PropTypes.bool,
|
||||||
|
usingPiP: PropTypes.bool,
|
||||||
onToggleMediaVisibility: PropTypes.func,
|
onToggleMediaVisibility: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -100,7 +102,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
render () {
|
render () {
|
||||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
const outerStyle = { boxSizing: 'border-box' };
|
||||||
const { intl, compact } = this.props;
|
const { intl, compact, usingPiP } = this.props;
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return null;
|
return null;
|
||||||
@ -116,7 +118,9 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||||||
outerStyle.height = `${this.state.height}px`;
|
outerStyle.height = `${this.state.height}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('media_attachments').size > 0) {
|
if (usingPiP) {
|
||||||
|
media = <PictureInPicturePlaceholder />;
|
||||||
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
|
||||||
|
@ -143,6 +143,7 @@ const makeMapStateToProps = () => {
|
|||||||
descendantsIds,
|
descendantsIds,
|
||||||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||||
domain: state.getIn(['meta', 'domain']),
|
domain: state.getIn(['meta', 'domain']),
|
||||||
|
usingPiP: state.get('picture_in_picture').statusId === props.params.statusId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -167,6 +168,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
askReplyConfirmation: PropTypes.bool,
|
askReplyConfirmation: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
|
usingPiP: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -492,7 +494,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
let ancestors, descendants;
|
let ancestors, descendants;
|
||||||
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
|
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props;
|
||||||
const { fullscreen } = this.state;
|
const { fullscreen } = this.state;
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
@ -550,6 +552,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
domain={domain}
|
domain={domain}
|
||||||
showMedia={this.state.showMedia}
|
showMedia={this.state.showMedia}
|
||||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||||
|
usingPiP={usingPiP}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionBar
|
<ActionBar
|
||||||
|
@ -160,7 +160,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||||||
src={image.get('url')}
|
src={image.get('url')}
|
||||||
width={image.get('width')}
|
width={image.get('width')}
|
||||||
height={image.get('height')}
|
height={image.get('height')}
|
||||||
startTime={time || 0}
|
currentTime={time || 0}
|
||||||
onCloseVideo={onClose}
|
onCloseVideo={onClose}
|
||||||
detailed
|
detailed
|
||||||
alt={image.get('description')}
|
alt={image.get('description')}
|
||||||
|
@ -66,9 +66,9 @@ export default class VideoModal extends ImmutablePureComponent {
|
|||||||
preview={media.get('preview_url')}
|
preview={media.get('preview_url')}
|
||||||
blurhash={media.get('blurhash')}
|
blurhash={media.get('blurhash')}
|
||||||
src={media.get('url')}
|
src={media.get('url')}
|
||||||
startTime={options.startTime}
|
currentTime={options.startTime}
|
||||||
autoPlay={options.autoPlay}
|
autoPlay={options.autoPlay}
|
||||||
defaultVolume={options.defaultVolume}
|
volume={options.defaultVolume}
|
||||||
onCloseVideo={onClose}
|
onCloseVideo={onClose}
|
||||||
detailed
|
detailed
|
||||||
alt={media.get('description')}
|
alt={media.get('description')}
|
||||||
|
@ -16,11 +16,12 @@ import { expandNotifications } from '../../actions/notifications';
|
|||||||
import { fetchFilters } from '../../actions/filters';
|
import { fetchFilters } from '../../actions/filters';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { focusApp, unfocusApp } from 'mastodon/actions/app';
|
import { focusApp, unfocusApp } from 'mastodon/actions/app';
|
||||||
import { synchronouslySubmitMarkers } from 'mastodon/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
import DocumentTitle from './components/document_title';
|
import DocumentTitle from './components/document_title';
|
||||||
|
import PictureInPicture from 'mastodon/features/picture_in_picture';
|
||||||
import {
|
import {
|
||||||
Compose,
|
Compose,
|
||||||
Status,
|
Status,
|
||||||
@ -265,6 +266,7 @@ class UI extends React.PureComponent {
|
|||||||
|
|
||||||
handleWindowFocus = () => {
|
handleWindowFocus = () => {
|
||||||
this.props.dispatch(focusApp());
|
this.props.dispatch(focusApp());
|
||||||
|
this.props.dispatch(submitMarkers());
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowBlur = () => {
|
handleWindowBlur = () => {
|
||||||
@ -368,6 +370,7 @@ class UI extends React.PureComponent {
|
|||||||
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.props.dispatch(fetchMarkers());
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
|
||||||
@ -545,6 +548,7 @@ class UI extends React.PureComponent {
|
|||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
<PictureInPicture />
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
<LoadingBarContainer className='loading-bar' />
|
<LoadingBarContainer className='loading-bar' />
|
||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
|
@ -104,20 +104,23 @@ class Video extends React.PureComponent {
|
|||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
height: PropTypes.number,
|
height: PropTypes.number,
|
||||||
sensitive: PropTypes.bool,
|
sensitive: PropTypes.bool,
|
||||||
startTime: PropTypes.number,
|
currentTime: PropTypes.number,
|
||||||
onOpenVideo: PropTypes.func,
|
onOpenVideo: PropTypes.func,
|
||||||
onCloseVideo: PropTypes.func,
|
onCloseVideo: PropTypes.func,
|
||||||
detailed: PropTypes.bool,
|
detailed: PropTypes.bool,
|
||||||
inline: PropTypes.bool,
|
inline: PropTypes.bool,
|
||||||
editable: PropTypes.bool,
|
editable: PropTypes.bool,
|
||||||
|
alwaysVisible: PropTypes.bool,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
onToggleVisibility: PropTypes.func,
|
onToggleVisibility: PropTypes.func,
|
||||||
|
deployPictureInPicture: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
blurhash: PropTypes.string,
|
blurhash: PropTypes.string,
|
||||||
link: PropTypes.node,
|
link: PropTypes.node,
|
||||||
autoPlay: PropTypes.bool,
|
autoPlay: PropTypes.bool,
|
||||||
defaultVolume: PropTypes.number,
|
volume: PropTypes.number,
|
||||||
|
muted: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -297,6 +300,15 @@ class Video extends React.PureComponent {
|
|||||||
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||||
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||||
|
|
||||||
|
if (!this.state.paused && this.video && this.props.deployPictureInPicture) {
|
||||||
|
this.props.deployPictureInPicture('video', {
|
||||||
|
src: this.props.src,
|
||||||
|
currentTime: this.video.currentTime,
|
||||||
|
muted: this.video.muted,
|
||||||
|
volume: this.video.volume,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
@ -328,7 +340,18 @@ class Video extends React.PureComponent {
|
|||||||
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
||||||
|
|
||||||
if (!this.state.paused && !inView) {
|
if (!this.state.paused && !inView) {
|
||||||
this.setState({ paused: true }, () => this.video.pause());
|
this.video.pause();
|
||||||
|
|
||||||
|
if (this.props.deployPictureInPicture) {
|
||||||
|
this.props.deployPictureInPicture('video', {
|
||||||
|
src: this.props.src,
|
||||||
|
currentTime: this.video.currentTime,
|
||||||
|
muted: this.video.muted,
|
||||||
|
volume: this.video.volume,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ paused: true });
|
||||||
}
|
}
|
||||||
}, 150, { trailing: true })
|
}, 150, { trailing: true })
|
||||||
|
|
||||||
@ -361,15 +384,21 @@ class Video extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleLoadedData = () => {
|
handleLoadedData = () => {
|
||||||
if (this.props.startTime) {
|
const { currentTime, volume, muted, autoPlay } = this.props;
|
||||||
this.video.currentTime = this.props.startTime;
|
|
||||||
|
if (currentTime) {
|
||||||
|
this.video.currentTime = currentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.defaultVolume !== undefined) {
|
if (volume !== undefined) {
|
||||||
this.video.volume = this.props.defaultVolume;
|
this.video.volume = volume;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.autoPlay) {
|
if (muted !== undefined) {
|
||||||
|
this.video.muted = muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoPlay) {
|
||||||
this.video.play();
|
this.video.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -414,9 +443,9 @@ class Video extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
|
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
|
||||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||||
const progress = (currentTime / duration) * 100;
|
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||||
const playerStyle = {};
|
const playerStyle = {};
|
||||||
|
|
||||||
let { width, height } = this.props;
|
let { width, height } = this.props;
|
||||||
@ -430,7 +459,7 @@ class Video extends React.PureComponent {
|
|||||||
|
|
||||||
let preload;
|
let preload;
|
||||||
|
|
||||||
if (startTime || fullscreen || dragging) {
|
if (this.props.currentTime || fullscreen || dragging) {
|
||||||
preload = 'auto';
|
preload = 'auto';
|
||||||
} else if (detailed) {
|
} else if (detailed) {
|
||||||
preload = 'metadata';
|
preload = 'metadata';
|
||||||
@ -530,7 +559,7 @@ class Video extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='video-player__buttons right'>
|
<div className='video-player__buttons right'>
|
||||||
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
{(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||||
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
||||||
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||||
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||||
|
@ -36,6 +36,7 @@ import trends from './trends';
|
|||||||
import missed_updates from './missed_updates';
|
import missed_updates from './missed_updates';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import markers from './markers';
|
import markers from './markers';
|
||||||
|
import picture_in_picture from './picture_in_picture';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
announcements,
|
announcements,
|
||||||
@ -75,6 +76,7 @@ const reducers = {
|
|||||||
trends,
|
trends,
|
||||||
missed_updates,
|
missed_updates,
|
||||||
markers,
|
markers,
|
||||||
|
picture_in_picture,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
export default combineReducers(reducers);
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
NOTIFICATIONS_LOAD_PENDING,
|
NOTIFICATIONS_LOAD_PENDING,
|
||||||
NOTIFICATIONS_MOUNT,
|
NOTIFICATIONS_MOUNT,
|
||||||
NOTIFICATIONS_UNMOUNT,
|
NOTIFICATIONS_UNMOUNT,
|
||||||
|
NOTIFICATIONS_MARK_AS_READ,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
@ -16,6 +17,13 @@ import {
|
|||||||
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
|
||||||
FOLLOW_REQUEST_REJECT_SUCCESS,
|
FOLLOW_REQUEST_REJECT_SUCCESS,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
|
import {
|
||||||
|
MARKERS_FETCH_SUCCESS,
|
||||||
|
} from '../actions/markers';
|
||||||
|
import {
|
||||||
|
APP_FOCUS,
|
||||||
|
APP_UNFOCUS,
|
||||||
|
} from '../actions/app';
|
||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
@ -26,8 +34,11 @@ const initialState = ImmutableMap({
|
|||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
top: false,
|
top: false,
|
||||||
mounted: false,
|
mounted: 0,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
|
lastReadId: '0',
|
||||||
|
readMarkerId: '0',
|
||||||
|
isTabVisible: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -46,8 +57,10 @@ const normalizeNotification = (state, notification, usePendingItems) => {
|
|||||||
return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1);
|
return state.update('pendingItems', list => list.unshift(notificationToMap(notification))).update('unread', unread => unread + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!top) {
|
if (shouldCountUnreadNotifications(state)) {
|
||||||
state = state.update('unread', unread => unread + 1);
|
state = state.update('unread', unread => unread + 1);
|
||||||
|
} else {
|
||||||
|
state = state.set('lastReadId', notification.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.update('items', list => {
|
return state.update('items', list => {
|
||||||
@ -60,6 +73,7 @@ const normalizeNotification = (state, notification, usePendingItems) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
|
const expandNormalizedNotifications = (state, notifications, next, isLoadingRecent, usePendingItems) => {
|
||||||
|
const lastReadId = state.get('lastReadId');
|
||||||
let items = ImmutableList();
|
let items = ImmutableList();
|
||||||
|
|
||||||
notifications.forEach((n, i) => {
|
notifications.forEach((n, i) => {
|
||||||
@ -87,6 +101,15 @@ const expandNormalizedNotifications = (state, notifications, next, isLoadingRece
|
|||||||
mutable.set('hasMore', false);
|
mutable.set('hasMore', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldCountUnreadNotifications(state)) {
|
||||||
|
mutable.update('unread', unread => unread + items.count(item => compareId(item.get('id'), lastReadId) > 0));
|
||||||
|
} else {
|
||||||
|
const mostRecent = items.find(item => item !== null);
|
||||||
|
if (mostRecent && compareId(lastReadId, mostRecent.get('id')) < 0) {
|
||||||
|
mutable.set('lastReadId', mostRecent.get('id'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutable.set('isLoading', false);
|
mutable.set('isLoading', false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -96,21 +119,92 @@ const filterNotifications = (state, accountIds, type) => {
|
|||||||
return state.update('items', helper).update('pendingItems', helper);
|
return state.update('items', helper).update('pendingItems', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTop = (state, top) => {
|
const clearUnread = (state) => {
|
||||||
if (top) {
|
|
||||||
state = state.set('unread', state.get('pendingItems').size);
|
state = state.set('unread', state.get('pendingItems').size);
|
||||||
|
const lastNotification = state.get('items').find(item => item !== null);
|
||||||
|
return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0');
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateTop = (state, top) => {
|
||||||
|
state = state.set('top', top);
|
||||||
|
|
||||||
|
if (!shouldCountUnreadNotifications(state)) {
|
||||||
|
state = clearUnread(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.set('top', top);
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteByStatus = (state, statusId) => {
|
const deleteByStatus = (state, statusId) => {
|
||||||
|
const lastReadId = state.get('lastReadId');
|
||||||
|
|
||||||
|
if (shouldCountUnreadNotifications(state)) {
|
||||||
|
const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
|
||||||
|
state = state.update('unread', unread => unread - deletedUnread.size);
|
||||||
|
}
|
||||||
|
|
||||||
const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
|
const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId);
|
||||||
|
const deletedUnread = state.get('pendingItems').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0);
|
||||||
|
state = state.update('unread', unread => unread - deletedUnread.size);
|
||||||
return state.update('items', helper).update('pendingItems', helper);
|
return state.update('items', helper).update('pendingItems', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateMounted = (state) => {
|
||||||
|
state = state.update('mounted', count => count + 1);
|
||||||
|
if (!shouldCountUnreadNotifications(state)) {
|
||||||
|
state = state.set('readMarkerId', state.get('lastReadId'));
|
||||||
|
state = clearUnread(state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVisibility = (state, visibility) => {
|
||||||
|
state = state.set('isTabVisible', visibility);
|
||||||
|
if (!shouldCountUnreadNotifications(state)) {
|
||||||
|
state = state.set('readMarkerId', state.get('lastReadId'));
|
||||||
|
state = clearUnread(state);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldCountUnreadNotifications = (state) => {
|
||||||
|
const isTabVisible = state.get('isTabVisible');
|
||||||
|
const isOnTop = state.get('top');
|
||||||
|
const isMounted = state.get('mounted') > 0;
|
||||||
|
const lastReadId = state.get('lastReadId');
|
||||||
|
const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0);
|
||||||
|
|
||||||
|
return !(isTabVisible && isOnTop && isMounted && lastItemReached);
|
||||||
|
};
|
||||||
|
|
||||||
|
const recountUnread = (state, last_read_id) => {
|
||||||
|
return state.withMutations(mutable => {
|
||||||
|
if (compareId(last_read_id, mutable.get('lastReadId')) > 0) {
|
||||||
|
mutable.set('lastReadId', last_read_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (compareId(last_read_id, mutable.get('readMarkerId')) > 0) {
|
||||||
|
mutable.set('readMarkerId', last_read_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.get('unread') > 0 || shouldCountUnreadNotifications(state)) {
|
||||||
|
mutable.set('unread', mutable.get('pendingItems').count(item => item !== null) + mutable.get('items').count(item => item && compareId(item.get('id'), last_read_id) > 0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default function notifications(state = initialState, action) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case MARKERS_FETCH_SUCCESS:
|
||||||
|
return action.markers.notifications ? recountUnread(state, action.markers.notifications.last_read_id) : state;
|
||||||
|
case NOTIFICATIONS_MOUNT:
|
||||||
|
return updateMounted(state);
|
||||||
|
case NOTIFICATIONS_UNMOUNT:
|
||||||
|
return state.update('mounted', count => count - 1);
|
||||||
|
case APP_FOCUS:
|
||||||
|
return updateVisibility(state, true);
|
||||||
|
case APP_UNFOCUS:
|
||||||
|
return updateVisibility(state, false);
|
||||||
case NOTIFICATIONS_LOAD_PENDING:
|
case NOTIFICATIONS_LOAD_PENDING:
|
||||||
return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
|
return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0);
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
@ -144,10 +238,9 @@ export default function notifications(state = initialState, action) {
|
|||||||
return action.timeline === 'home' ?
|
return action.timeline === 'home' ?
|
||||||
state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
|
state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) :
|
||||||
state;
|
state;
|
||||||
case NOTIFICATIONS_MOUNT:
|
case NOTIFICATIONS_MARK_AS_READ:
|
||||||
return state.set('mounted', true);
|
const lastNotification = state.get('items').find(item => item !== null);
|
||||||
case NOTIFICATIONS_UNMOUNT:
|
return lastNotification ? recountUnread(state, lastNotification.get('id')) : state;
|
||||||
return state.set('mounted', false);
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
22
app/javascript/mastodon/reducers/picture_in_picture.js
Normal file
22
app/javascript/mastodon/reducers/picture_in_picture.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { PICTURE_IN_PICTURE_DEPLOY, PICTURE_IN_PICTURE_REMOVE } from 'mastodon/actions/picture_in_picture';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
statusId: null,
|
||||||
|
accountId: null,
|
||||||
|
type: null,
|
||||||
|
src: null,
|
||||||
|
muted: false,
|
||||||
|
volume: 0,
|
||||||
|
currentTime: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function pictureInPicture(state = initialState, action) {
|
||||||
|
switch(action.type) {
|
||||||
|
case PICTURE_IN_PICTURE_DEPLOY:
|
||||||
|
return { statusId: action.statusId, accountId: action.accountId, type: action.playerType, ...action.props };
|
||||||
|
case PICTURE_IN_PICTURE_REMOVE:
|
||||||
|
return { ...initialState };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
@ -75,3 +75,8 @@
|
|||||||
.public-layout .public-account-header__tabs__tabs .counter.active::after {
|
.public-layout .public-account-header__tabs__tabs .counter.active::after {
|
||||||
border-bottom: 4px solid $ui-highlight-color;
|
border-bottom: 4px solid $ui-highlight-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-form .autosuggest-textarea__textarea::placeholder,
|
||||||
|
.compose-form .spoiler-input__input::placeholder {
|
||||||
|
color: $inverted-text-color;
|
||||||
|
}
|
||||||
|
@ -163,7 +163,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
.icon-button {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: $action-button-color;
|
color: $action-button-color;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -245,6 +246,14 @@
|
|||||||
background: rgba($base-overlay-background, 0.9);
|
background: rgba($base-overlay-background, 0.9);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__counter {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-icon-button {
|
.text-icon-button {
|
||||||
@ -1139,24 +1148,6 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|
||||||
&__counter {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-right: 11px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.status__action-bar-button {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
display: inline-block;
|
|
||||||
width: 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: $action-button-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__action-bar-button {
|
.status__action-bar-button {
|
||||||
@ -6502,6 +6493,10 @@ noscript {
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .icon-button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
}
|
}
|
||||||
@ -7011,3 +7006,119 @@ noscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification,
|
||||||
|
.status__wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.unread {
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-left: 2px solid $highlight-text-color;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picture-in-picture {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
background: lighten($ui-base-color, 4%);
|
||||||
|
padding: 10px;
|
||||||
|
padding-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
background: lighten($ui-base-color, 4%);
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&__account {
|
||||||
|
display: flex;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account__avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-name {
|
||||||
|
color: $primary-text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
strong,
|
||||||
|
span {
|
||||||
|
display: block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 415px) {
|
||||||
|
width: 210px;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
|
||||||
|
&__footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player,
|
||||||
|
.audio-player {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picture-in-picture-placeholder {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 2px dashed lighten($ui-base-color, 8%);
|
||||||
|
background: $base-shadow-color;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $darker-text-color;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: block;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
border-color: lighten($ui-base-color, 12%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -118,13 +118,13 @@ class ActivityPub::Activity
|
|||||||
end
|
end
|
||||||
|
|
||||||
def notify_about_reblog(status)
|
def notify_about_reblog(status)
|
||||||
NotifyService.new.call(status.reblog.account, status)
|
NotifyService.new.call(status.reblog.account, :reblog, status)
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_about_mentions(status)
|
def notify_about_mentions(status)
|
||||||
status.active_mentions.includes(:account).each do |mention|
|
status.active_mentions.includes(:account).each do |mention|
|
||||||
next unless mention.account.local? && audience_includes?(mention.account)
|
next unless mention.account.local? && audience_includes?(mention.account)
|
||||||
NotifyService.new.call(mention.account, mention)
|
NotifyService.new.call(mention.account, :mention, mention)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||||||
|
|
||||||
def delete_person
|
def delete_person
|
||||||
lock_or_return("delete_in_progress:#{@account.id}") do
|
lock_or_return("delete_in_progress:#{@account.id}") do
|
||||||
SuspendAccountService.new.call(@account, reserve_username: false)
|
DeleteAccountService.new.call(@account, reserve_username: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -22,10 +22,10 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
|
|||||||
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
|
follow_request = FollowRequest.create!(account: @account, target_account: target_account, uri: @json['id'])
|
||||||
|
|
||||||
if target_account.locked? || @account.silenced?
|
if target_account.locked? || @account.silenced?
|
||||||
NotifyService.new.call(target_account, follow_request)
|
NotifyService.new.call(target_account, :follow_request, follow_request)
|
||||||
else
|
else
|
||||||
AuthorizeFollowService.new.call(@account, target_account)
|
AuthorizeFollowService.new.call(@account, target_account)
|
||||||
NotifyService.new.call(target_account, ::Follow.find_by(account: @account, target_account: target_account))
|
NotifyService.new.call(target_account, :follow, ::Follow.find_by(account: @account, target_account: target_account))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -7,6 +7,6 @@ class ActivityPub::Activity::Like < ActivityPub::Activity
|
|||||||
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
|
return if original_status.nil? || !original_status.account.local? || delete_arrived_first?(@json['id']) || @account.favourited?(original_status)
|
||||||
|
|
||||||
favourite = original_status.favourites.create!(account: @account)
|
favourite = original_status.favourites.create!(account: @account)
|
||||||
NotifyService.new.call(original_status.account, favourite)
|
NotifyService.new.call(original_status.account, :favourite, favourite)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -253,7 +253,15 @@ class Request
|
|||||||
alias new open
|
alias new open
|
||||||
|
|
||||||
def check_private_address(address)
|
def check_private_address(address)
|
||||||
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
|
addr = IPAddr.new(address.to_s)
|
||||||
|
return if private_address_exceptions.any? { |range| range.include?(addr) }
|
||||||
|
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(addr)
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_address_exceptions
|
||||||
|
@private_address_exceptions = begin
|
||||||
|
(ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(',').map { |addr| IPAddr.new(addr) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@me = recipient
|
@me = recipient
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
return if @me.user.disabled? || @status.nil?
|
return unless @me.user.functional? && @status.present?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@me = recipient
|
@me = recipient
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
|
|
||||||
return if @me.user.disabled?
|
return unless @me.user.functional?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
||||||
@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
return if @me.user.disabled? || @status.nil?
|
return unless @me.user.functional? && @status.present?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
return if @me.user.disabled? || @status.nil?
|
return unless @me.user.functional? && @status.present?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
@me = recipient
|
@me = recipient
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
|
|
||||||
return if @me.user.disabled?
|
return unless @me.user.functional?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
||||||
@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def digest(recipient, **opts)
|
def digest(recipient, **opts)
|
||||||
return if recipient.user.disabled?
|
return unless recipient.user.functional?
|
||||||
|
|
||||||
@me = recipient
|
@me = recipient
|
||||||
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
|
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
|
||||||
@ -88,7 +88,9 @@ class NotificationMailer < ApplicationMailer
|
|||||||
|
|
||||||
def thread_by_conversation(conversation)
|
def thread_by_conversation(conversation)
|
||||||
return if conversation.nil?
|
return if conversation.nil?
|
||||||
|
|
||||||
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"
|
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"
|
||||||
|
|
||||||
headers['In-Reply-To'] = msg_id
|
headers['In-Reply-To'] = msg_id
|
||||||
headers['References'] = msg_id
|
headers['References'] = msg_id
|
||||||
end
|
end
|
||||||
|
@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@token = token
|
@token = token
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.unconfirmed_email.presence || @resource.email,
|
mail to: @resource.unconfirmed_email.presence || @resource.email,
|
||||||
@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@token = token
|
@token = token
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
||||||
@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
||||||
@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
|
||||||
@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
|
||||||
@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
|
||||||
@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
|
||||||
@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
|
||||||
@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
|
||||||
@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
@webauthn_credential = webauthn_credential
|
@webauthn_credential = webauthn_credential
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
|
||||||
@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
@webauthn_credential = webauthn_credential
|
@webauthn_credential = webauthn_credential
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
|
||||||
@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
||||||
@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
@backup = backup
|
@backup = backup
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
|
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
|
||||||
@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer
|
|||||||
@detection = Browser.new(user_agent)
|
@detection = Browser.new(user_agent)
|
||||||
@timestamp = timestamp.to_time.utc
|
@timestamp = timestamp.to_time.utc
|
||||||
|
|
||||||
return if @resource.disabled?
|
return unless @resource.active_for_authentication?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email,
|
mail to: @resource.email,
|
||||||
|
@ -226,24 +226,21 @@ class Account < ApplicationRecord
|
|||||||
|
|
||||||
def suspend!(date = Time.now.utc)
|
def suspend!(date = Time.now.utc)
|
||||||
transaction do
|
transaction do
|
||||||
user&.disable! if local?
|
create_deletion_request!
|
||||||
update!(suspended_at: date)
|
update!(suspended_at: date)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsuspend!
|
def unsuspend!
|
||||||
transaction do
|
transaction do
|
||||||
user&.enable! if local?
|
deletion_request&.destroy!
|
||||||
update!(suspended_at: nil)
|
update!(suspended_at: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def memorialize!
|
def memorialize!
|
||||||
transaction do
|
|
||||||
user&.disable! if local?
|
|
||||||
update!(memorial: true)
|
update!(memorial: true)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def sign?
|
def sign?
|
||||||
true
|
true
|
||||||
|
@ -38,15 +38,16 @@ class AccountConversation < ApplicationRecord
|
|||||||
class << self
|
class << self
|
||||||
def to_a_paginated_by_id(limit, options = {})
|
def to_a_paginated_by_id(limit, options = {})
|
||||||
if options[:min_id]
|
if options[:min_id]
|
||||||
paginate_by_min_id(limit, options[:min_id]).reverse
|
paginate_by_min_id(limit, options[:min_id], options[:max_id]).reverse
|
||||||
else
|
else
|
||||||
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
paginate_by_max_id(limit, options[:max_id], options[:since_id]).to_a
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def paginate_by_min_id(limit, min_id = nil)
|
def paginate_by_min_id(limit, min_id = nil, max_id = nil)
|
||||||
query = order(arel_table[:last_status_id].asc).limit(limit)
|
query = order(arel_table[:last_status_id].asc).limit(limit)
|
||||||
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
|
query = query.where(arel_table[:last_status_id].gt(min_id)) if min_id.present?
|
||||||
|
query = query.where(arel_table[:last_status_id].lt(max_id)) if max_id.present?
|
||||||
query
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
|
20
app/models/account_deletion_request.rb
Normal file
20
app/models/account_deletion_request.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: account_deletion_requests
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8)
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class AccountDeletionRequest < ApplicationRecord
|
||||||
|
DELAY_TO_DELETION = 30.days.freeze
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
def due_at
|
||||||
|
created_at + DELAY_TO_DELETION
|
||||||
|
end
|
||||||
|
end
|
@ -134,7 +134,7 @@ class Admin::AccountAction
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_email!
|
def process_email!
|
||||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
|
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
|
||||||
end
|
end
|
||||||
|
|
||||||
def warnable?
|
def warnable?
|
||||||
@ -142,7 +142,7 @@ class Admin::AccountAction
|
|||||||
end
|
end
|
||||||
|
|
||||||
def status_ids
|
def status_ids
|
||||||
@report.status_ids if @report && include_statuses
|
report.status_ids if report && include_statuses
|
||||||
end
|
end
|
||||||
|
|
||||||
def reports
|
def reports
|
||||||
|
@ -60,5 +60,8 @@ module AccountAssociations
|
|||||||
# Hashtags
|
# Hashtags
|
||||||
has_and_belongs_to_many :tags
|
has_and_belongs_to_many :tags
|
||||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||||
|
|
||||||
|
# Account deletion requests
|
||||||
|
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -8,6 +8,7 @@ module AccountInteractions
|
|||||||
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
Follow.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow, mapping|
|
||||||
mapping[follow.target_account_id] = {
|
mapping[follow.target_account_id] = {
|
||||||
reblogs: follow.show_reblogs?,
|
reblogs: follow.show_reblogs?,
|
||||||
|
notify: follow.notify?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -36,6 +37,7 @@ module AccountInteractions
|
|||||||
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
FollowRequest.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |follow_request, mapping|
|
||||||
mapping[follow_request.target_account_id] = {
|
mapping[follow_request.target_account_id] = {
|
||||||
reblogs: follow_request.show_reblogs?,
|
reblogs: follow_request.show_reblogs?,
|
||||||
|
notify: follow_request.notify?,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -95,25 +97,29 @@ module AccountInteractions
|
|||||||
has_many :announcement_mutes, dependent: :destroy
|
has_many :announcement_mutes, dependent: :destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
def follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
def follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||||
reblogs = true if reblogs.nil?
|
rel = active_relationships.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||||
|
|
||||||
rel = active_relationships.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
|
||||||
.find_or_create_by!(target_account: other_account)
|
.find_or_create_by!(target_account: other_account)
|
||||||
|
|
||||||
rel.update!(show_reblogs: reblogs)
|
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||||
|
rel.notify = notify unless notify.nil?
|
||||||
|
|
||||||
|
rel.save! if rel.changed?
|
||||||
|
|
||||||
remove_potential_friendship(other_account)
|
remove_potential_friendship(other_account)
|
||||||
|
|
||||||
rel
|
rel
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_follow!(other_account, reblogs: nil, uri: nil, rate_limit: false)
|
def request_follow!(other_account, reblogs: nil, notify: nil, uri: nil, rate_limit: false)
|
||||||
reblogs = true if reblogs.nil?
|
rel = follow_requests.create_with(show_reblogs: reblogs.nil? ? true : reblogs, notify: notify.nil? ? false : notify, uri: uri, rate_limit: rate_limit)
|
||||||
|
|
||||||
rel = follow_requests.create_with(show_reblogs: reblogs, uri: uri, rate_limit: rate_limit)
|
|
||||||
.find_or_create_by!(target_account: other_account)
|
.find_or_create_by!(target_account: other_account)
|
||||||
|
|
||||||
rel.update!(show_reblogs: reblogs)
|
rel.show_reblogs = reblogs unless reblogs.nil?
|
||||||
|
rel.notify = notify unless notify.nil?
|
||||||
|
|
||||||
|
rel.save! if rel.changed?
|
||||||
|
|
||||||
remove_potential_friendship(other_account)
|
remove_potential_friendship(other_account)
|
||||||
|
|
||||||
rel
|
rel
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user