Compare commits

..

1 Commits

Author SHA1 Message Date
Ondřej Hruška
0e0ff911b4 option to add title to <Button>, use for toot buttons 2017-10-24 18:29:15 +02:00
314 changed files with 2754 additions and 6224 deletions

View File

@@ -35,17 +35,6 @@ PAPERCLIP_SECRET=$PAPERCLIP_SECRET
SECRET_KEY_BASE=$SECRET_KEY_BASE SECRET_KEY_BASE=$SECRET_KEY_BASE
OTP_SECRET=$OTP_SECRET OTP_SECRET=$OTP_SECRET
# VAPID keys (used for push notifications)
# You can generate the keys using the following command (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
#
# Generate with `rake mastodon:webpush:generate_vapid_key` task (`nanobox run bundle exec rake mastodon:webpush:generate_vapid_key`)
#
# For more information visit https://rossta.net/blog/using-the-web-push-api-with-vapid.html
VAPID_PRIVATE_KEY=$VAPID_PRIVATE_KEY
VAPID_PUBLIC_KEY=$VAPID_PUBLIC_KEY
# Registrations # Registrations
# Single user mode will disable registrations and redirect frontpage to the first profile # Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true # SINGLE_USER_MODE=true
@@ -73,7 +62,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
#SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt #SMTP_CA_FILE=/etc/ssl/certs/ca-certificates.crt
#SMTP_OPENSSL_VERIFY_MODE=peer #SMTP_OPENSSL_VERIFY_MODE=peer
#SMTP_ENABLE_STARTTLS_AUTO=true #SMTP_ENABLE_STARTTLS_AUTO=true
#SMTP_TLS=true
# Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files. # Optional user upload path and URL (images, avatars). Default is :rails_root/public/system. If you set this variable, you are responsible for making your HTTP server (eg. nginx) serve these files.
# PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system # PAPERCLIP_ROOT_PATH=/var/lib/mastodon/public-system
@@ -102,23 +91,6 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# S3_ENDPOINT= # S3_ENDPOINT=
# S3_SIGNATURE_VERSION= # S3_SIGNATURE_VERSION=
# Swift (optional)
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to 'default'
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=
# Optional alias for S3 if you want to use Cloudfront or Cloudflare in front # Optional alias for S3 if you want to use Cloudfront or Cloudflare in front
# S3_CLOUDFRONT_HOST= # S3_CLOUDFRONT_HOST=

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "app/javascript/themes/mastodon-go"]
path = app/javascript/themes/mastodon-go
url = https://github.com/marrus-sh/mastodon-go

View File

@@ -1,46 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eugen@zeonfederated.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

32
Gemfile
View File

@@ -14,10 +14,8 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7' gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2'
gem 'fog-aws', '~> 1.4', require: false gem 'aws-sdk', '~> 2.9'
gem 'fog-core', '~> 1.45' gem 'fog-openstack', '~> 0.1'
gem 'fog-local', '~> 0.4', require: false
gem 'fog-openstack', '~> 0.1', require: false
gem 'paperclip', '~> 5.1' gem 'paperclip', '~> 5.1'
gem 'paperclip-av-transcoder', '~> 0.6' gem 'paperclip-av-transcoder', '~> 0.6'
@@ -40,14 +38,14 @@ gem 'http', '~> 2.2'
gem 'http_accept_language', '~> 2.1' gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99' gem 'httplog', '~> 0.99'
gem 'idn-ruby', require: 'idn' gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1' gem 'kaminari', '~> 1.0'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1' gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.8' gem 'nokogiri', '~> 1.7'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.3' gem 'oj', '~> 3.0'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8' gem 'ox', '~> 2.5'
gem 'pundit', '~> 1.1' gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13' gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0' gem 'rack-attack', '~> 5.0'
@@ -77,15 +75,15 @@ gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1' gem 'rdf-normalize', '~> 0.3.1'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.18' gem 'fabrication', '~> 2.16'
gem 'fuubar', '~> 2.2' gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.7' gem 'rspec-rails', '~> 3.6'
end end
group :test do group :test do
gem 'capybara', '~> 2.15' gem 'capybara', '~> 2.14'
gem 'climate_control', '~> 0.2' gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7' gem 'faker', '~> 1.7'
gem 'microformats', '~> 4.0' gem 'microformats', '~> 4.0'
@@ -93,13 +91,13 @@ group :test do
gem 'rspec-sidekiq', '~> 3.0' gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false gem 'simplecov', '~> 0.14', require: false
gem 'webmock', '~> 3.0' gem 'webmock', '~> 3.0'
gem 'parallel_tests', '~> 2.17' gem 'parallel_tests', '~> 2.14'
end end
group :development do group :development do
gem 'active_record_query_trace', '~> 1.5' gem 'active_record_query_trace', '~> 1.5'
gem 'annotate', '~> 2.7' gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.4' gem 'better_errors', '~> 2.1'
gem 'binding_of_caller', '~> 0.7' gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5' gem 'bullet', '~> 5.5'
gem 'letter_opener', '~> 1.4' gem 'letter_opener', '~> 1.4'
@@ -107,15 +105,15 @@ group :development do
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'brakeman', '~> 4.0', require: false gem 'brakeman', '~> 4.0', require: false
gem 'bundler-audit', '~> 0.6', require: false gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.55', require: false gem 'scss_lint', '~> 0.53', require: false
gem 'capistrano', '~> 3.10' gem 'capistrano', '~> 3.8'
gem 'capistrano-rails', '~> 1.3' gem 'capistrano-rails', '~> 1.2'
gem 'capistrano-rbenv', '~> 2.1' gem 'capistrano-rbenv', '~> 2.1'
gem 'capistrano-yarn', '~> 2.0' gem 'capistrano-yarn', '~> 2.0'
end end
group :production do group :production do
gem 'lograge', '~> 0.7' gem 'lograge', '~> 0.5'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end

View File

@@ -57,17 +57,25 @@ GEM
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
av (0.9.0) av (0.9.0)
cocaine (~> 0.5.3) cocaine (~> 0.5.3)
aws-sdk (2.10.46)
aws-sdk-resources (= 2.10.46)
aws-sdk-core (2.10.46)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
aws-sdk-resources (2.10.46)
aws-sdk-core (= 2.10.46)
aws-sigv4 (1.0.2)
bcrypt (3.1.11) bcrypt (3.1.11)
better_errors (2.4.0) better_errors (2.3.0)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
binding_of_caller (0.7.3) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.5) bootsnap (1.1.3)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.0.1) brakeman (4.0.1)
browser (2.5.2) browser (2.5.1)
builder (3.2.3) builder (3.2.3)
bullet (5.6.1) bullet (5.6.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@@ -75,23 +83,23 @@ GEM
bundler-audit (0.6.0) bundler-audit (0.6.0)
bundler (~> 1.2) bundler (~> 1.2)
thor (~> 0.18) thor (~> 0.18)
capistrano (3.10.0) capistrano (3.9.1)
airbrussh (>= 1.0.0) airbrussh (>= 1.0.0)
i18n i18n
rake (>= 10.0.0) rake (>= 10.0.0)
sshkit (>= 1.9.0) sshkit (>= 1.9.0)
capistrano-bundler (1.3.0) capistrano-bundler (1.2.0)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.2) sshkit (~> 1.2)
capistrano-rails (1.3.0) capistrano-rails (1.3.0)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.2) capistrano-rbenv (2.1.1)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.3) sshkit (~> 1.3)
capistrano-yarn (2.0.2) capistrano-yarn (2.0.2)
capistrano (~> 3.0) capistrano (~> 3.0)
capybara (2.15.4) capybara (2.15.1)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
@@ -102,7 +110,7 @@ GEM
activesupport activesupport
charlock_holmes (0.7.5) charlock_holmes (0.7.5)
chunky_png (1.3.8) chunky_png (1.3.8)
cld3 (3.2.1) cld3 (3.2.0)
ffi (>= 1.1.0, < 1.10.0) ffi (>= 1.1.0, < 1.10.0)
climate_control (0.2.0) climate_control (0.2.0)
cocaine (0.5.8) cocaine (0.5.8)
@@ -142,21 +150,16 @@ GEM
thread thread
thread_safe thread_safe
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.7.0) erubi (1.6.1)
et-orbi (1.0.8) et-orbi (1.0.5)
tzinfo tzinfo
excon (0.59.0) excon (0.59.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.18.0) fabrication (2.16.3)
faker (1.8.4) faker (1.8.4)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
ffi (1.9.18) ffi (1.9.18)
fog-aws (1.4.1)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (1.45.0) fog-core (1.45.0)
builder builder
excon (~> 0.58) excon (~> 0.58)
@@ -164,20 +167,15 @@ GEM
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
fog-local (0.4.0) fog-openstack (0.1.21)
fog-core (~> 1.27)
fog-openstack (0.1.22)
fog-core (>= 1.40) fog-core (>= 1.40)
fog-json (>= 1.0) fog-json (>= 1.0)
ipaddress (>= 0.8) ipaddress (>= 0.8)
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5) formatador (0.2.5)
fuubar (2.2.0) fuubar (2.2.0)
rspec-core (~> 3.0) rspec-core (~> 3.0)
ruby-progressbar (~> 1.4) ruby-progressbar (~> 1.4)
globalid (0.4.1) globalid (0.4.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.0.1) goldfinger (2.0.1)
addressable (~> 2.5) addressable (~> 2.5)
@@ -213,8 +211,7 @@ GEM
httplog (0.99.7) httplog (0.99.7)
colorize colorize
rack rack
i18n (0.9.0) i18n (0.8.6)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.18) i18n-tasks (0.9.18)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
@@ -228,28 +225,29 @@ GEM
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8) iso-639 (0.2.8)
jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.7) json-ld (2.1.5)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 2.2, >= 2.2.8) rdf (~> 2.2)
json-ld-preloaded (2.2.2) json-ld-preloaded (2.2.2)
json-ld (~> 2.1, >= 2.1.5) json-ld (~> 2.1, >= 2.1.5)
multi_json (~> 1.11) multi_json (~> 1.11)
rdf (~> 2.2) rdf (~> 2.2)
jsonapi-renderer (0.1.3) jsonapi-renderer (0.1.3)
jwt (1.5.6) jwt (1.5.6)
kaminari (1.1.1) kaminari (1.0.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.1.1) kaminari-actionview (= 1.0.1)
kaminari-activerecord (= 1.1.1) kaminari-activerecord (= 1.0.1)
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-actionview (1.1.1) kaminari-actionview (1.0.1)
actionview actionview
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-activerecord (1.1.1) kaminari-activerecord (1.0.1)
activerecord activerecord
kaminari-core (= 1.1.1) kaminari-core (= 1.0.1)
kaminari-core (1.1.1) kaminari-core (1.0.1)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
@@ -259,19 +257,18 @@ GEM
letter_opener (~> 1.0) letter_opener (~> 1.0)
railties (>= 3.2) railties (>= 3.2)
link_header (0.0.8) link_header (0.0.8)
lograge (0.7.1) lograge (0.6.0)
actionpack (>= 4, < 5.2) actionpack (>= 4, < 5.2)
activesupport (>= 4, < 5.2) activesupport (>= 4, < 5.2)
railties (>= 4, < 5.2) railties (>= 4, < 5.2)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.1.1) loofah (2.0.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.6) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mario-redis-lock (1.2.0) mario-redis-lock (1.2.0)
redis (~> 3, >= 3.0.5) redis (~> 3, >= 3.0.5)
method_source (0.9.0) method_source (0.8.2)
microformats (4.0.7) microformats (4.0.7)
json json
nokogiri nokogiri
@@ -280,7 +277,7 @@ GEM
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mimemagic (0.3.2) mimemagic (0.3.2)
mini_mime (0.1.4) mini_mime (0.1.4)
mini_portile2 (2.3.0) mini_portile2 (2.2.0)
minitest (5.10.3) minitest (5.10.3)
msgpack (1.1.0) msgpack (1.1.0)
multi_json (1.12.2) multi_json (1.12.2)
@@ -288,8 +285,8 @@ GEM
net-ssh (>= 2.6.5) net-ssh (>= 2.6.5)
net-ssh (4.2.0) net-ssh (4.2.0)
nio4r (2.1.0) nio4r (2.1.0)
nokogiri (1.8.1) nokogiri (1.8.0)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.2.0)
nokogumbo (1.4.13) nokogumbo (1.4.13)
nokogiri nokogiri
nsa (0.2.4) nsa (0.2.4)
@@ -297,15 +294,15 @@ GEM
concurrent-ruby (~> 1.0.0) concurrent-ruby (~> 1.0.0)
sidekiq (>= 3.5.0) sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0) statsd-ruby (~> 1.2.0)
oj (3.3.9) oj (3.3.5)
openssl (2.0.6) openssl (2.0.5)
orm_adapter (0.5.0) orm_adapter (0.5.0)
ostatus2 (2.0.1) ostatus2 (2.0.1)
addressable (~> 2.4) addressable (~> 2.4)
http (~> 2.0) http (~> 2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
openssl (~> 2.0) openssl (~> 2.0)
ox (2.8.1) ox (2.6.0)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@@ -316,18 +313,19 @@ GEM
av (~> 0.9.0) av (~> 0.9.0)
paperclip (>= 2.5.2) paperclip (>= 2.5.2)
parallel (1.12.0) parallel (1.12.0)
parallel_tests (2.17.0) parallel_tests (2.15.0)
parallel parallel
parser (2.4.0.0) parser (2.4.0.0)
ast (~> 2.2) ast (~> 2.2)
pg (0.21.0) pg (0.21.0)
pghero (1.7.0) pghero (1.7.0)
activerecord activerecord
pkg-config (1.2.8) pkg-config (1.2.7)
powerpack (0.1.1) powerpack (0.1.1)
pry (0.11.2) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.9.0) method_source (~> 0.8.1)
slop (~> 3.4)
pry-rails (0.3.6) pry-rails (0.3.6)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (3.0.0) public_suffix (3.0.0)
@@ -381,31 +379,31 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
rake (12.2.1) rake (12.1.0)
rdf (2.2.11) rdf (2.2.9)
hamster (~> 3.0) hamster (~> 3.0)
link_header (~> 0.0, >= 0.0.8) link_header (~> 0.0, >= 0.0.8)
rdf-normalize (0.3.2) rdf-normalize (0.3.2)
rdf (~> 2.0) rdf (~> 2.0)
redis (3.3.5) redis (3.3.3)
redis-actionpack (5.0.2) redis-actionpack (5.0.1)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 2) redis-store (>= 1.1.0, < 1.4.0)
redis-activesupport (5.0.4) redis-activesupport (5.0.3)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2) redis-store (~> 1.3.0)
redis-namespace (1.5.3) redis-namespace (1.5.3)
redis (~> 3.0, >= 3.0.4) redis (~> 3.0, >= 3.0.4)
redis-rack (2.0.3) redis-rack (2.0.2)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 1.4)
redis-rails (5.0.2) redis-rails (5.0.2)
redis-actionpack (>= 5.0, < 6) redis-actionpack (>= 5.0, < 6)
redis-activesupport (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
redis-store (1.4.1) redis-store (1.3.0)
redis (>= 2.2, < 5) redis (>= 2.2)
request_store (1.3.2) request_store (1.3.2)
responders (2.4.0) responders (2.4.0)
actionpack (>= 4.2.0, < 5.3) actionpack (>= 4.2.0, < 5.3)
@@ -413,27 +411,27 @@ GEM
rotp (2.1.2) rotp (2.1.2)
rqrcode (0.10.1) rqrcode (0.10.1)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rspec-core (3.7.0) rspec-core (3.6.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-expectations (3.7.0) rspec-expectations (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-mocks (3.7.0) rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-rails (3.7.1) rspec-rails (3.6.1)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.7.0) rspec-core (~> 3.6.0)
rspec-expectations (~> 3.7.0) rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.7.0) rspec-mocks (~> 3.6.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.6.0)
rspec-sidekiq (3.0.3) rspec-sidekiq (3.0.3)
rspec-core (~> 3.0, >= 3.0.0) rspec-core (~> 3.0, >= 3.0.0)
sidekiq (>= 2.4.0) sidekiq (>= 2.4.0)
rspec-support (3.7.0) rspec-support (3.6.0)
rubocop (0.51.0) rubocop (0.50.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0) parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
@@ -441,7 +439,7 @@ GEM
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-oembed (0.12.0) ruby-oembed (0.12.0)
ruby-progressbar (1.9.0) ruby-progressbar (1.8.3)
rufus-scheduler (3.4.2) rufus-scheduler (3.4.2)
et-orbi (~> 1.0) et-orbi (~> 1.0)
safe_yaml (1.0.4) safe_yaml (1.0.4)
@@ -450,19 +448,19 @@ GEM
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1) nokogumbo (~> 1.4.1)
sass (3.4.25) sass (3.4.25)
scss_lint (0.55.0) scss_lint (0.54.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.4.20)
sidekiq (5.0.5) sidekiq (5.0.4)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0) connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0) rack-protection (>= 1.5.0)
redis (>= 3.3.4, < 5) redis (~> 3.3, >= 3.3.3)
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.1.10) sidekiq-scheduler (2.1.9)
redis (>= 3, < 5) redis (~> 3)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
@@ -479,6 +477,7 @@ GEM
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.2) simplecov-html (0.10.2)
slop (3.6.0)
sprockets (3.7.1) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
@@ -501,9 +500,9 @@ GEM
tilt (2.0.8) tilt (2.0.8)
twitter-text (1.14.7) twitter-text (1.14.7)
unf (~> 0.1.0) unf (~> 0.1.0)
tzinfo (1.2.4) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2017.3) tzinfo-data (1.2017.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uglifier (3.2.0) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
@@ -518,7 +517,7 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.0.2) webpacker (3.0.1)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
@@ -539,18 +538,19 @@ DEPENDENCIES
active_record_query_trace (~> 1.5) active_record_query_trace (~> 1.5)
addressable (~> 2.5) addressable (~> 2.5)
annotate (~> 2.7) annotate (~> 2.7)
better_errors (~> 2.4) aws-sdk (~> 2.9)
better_errors (~> 2.1)
binding_of_caller (~> 0.7) binding_of_caller (~> 0.7)
bootsnap bootsnap
brakeman (~> 4.0) brakeman (~> 4.0)
browser browser
bullet (~> 5.5) bullet (~> 5.5)
bundler-audit (~> 0.6) bundler-audit (~> 0.6)
capistrano (~> 3.10) capistrano (~> 3.8)
capistrano-rails (~> 1.3) capistrano-rails (~> 1.2)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.15) capybara (~> 2.14)
charlock_holmes (~> 0.7.5) charlock_holmes (~> 0.7.5)
cld3 (~> 3.2.0) cld3 (~> 3.2.0)
climate_control (~> 0.2) climate_control (~> 0.2)
@@ -558,12 +558,9 @@ DEPENDENCIES
devise-two-factor (~> 3.0) devise-two-factor (~> 3.0)
doorkeeper (~> 4.2) doorkeeper (~> 4.2)
dotenv-rails (~> 2.2) dotenv-rails (~> 2.2)
fabrication (~> 2.18) fabrication (~> 2.16)
faker (~> 1.7) faker (~> 1.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
fog-aws (~> 1.4)
fog-core (~> 1.45)
fog-local (~> 0.4)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fuubar (~> 2.2) fuubar (~> 2.2)
goldfinger (~> 2.0) goldfinger (~> 2.0)
@@ -577,22 +574,22 @@ DEPENDENCIES
idn-ruby idn-ruby
iso-639 iso-639
json-ld-preloaded (~> 2.2.1) json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.1) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)
letter_opener_web (~> 1.3) letter_opener_web (~> 1.3)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.7) lograge (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
microformats (~> 4.0) microformats (~> 4.0)
mime-types (~> 3.1) mime-types (~> 3.1)
nokogiri (~> 1.8) nokogiri (~> 1.7)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.3) oj (~> 3.0)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.8) ox (~> 2.5)
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.17) parallel_tests (~> 2.14)
pg (~> 0.20) pg (~> 0.20)
pghero (~> 1.7) pghero (~> 1.7)
pkg-config (~> 1.2) pkg-config (~> 1.2)
@@ -612,12 +609,12 @@ DEPENDENCIES
redis-namespace (~> 1.5) redis-namespace (~> 1.5)
redis-rails (~> 5.0) redis-rails (~> 5.0)
rqrcode (~> 0.10) rqrcode (~> 0.10)
rspec-rails (~> 3.7) rspec-rails (~> 3.6)
rspec-sidekiq (~> 3.0) rspec-sidekiq (~> 3.0)
rubocop rubocop
ruby-oembed (~> 0.12) ruby-oembed (~> 0.12)
sanitize (~> 4.4) sanitize (~> 4.4)
scss_lint (~> 0.55) scss_lint (~> 0.53)
sidekiq (~> 5.0) sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1) sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1) sidekiq-scheduler (~> 2.1)

2
Vagrantfile vendored
View File

@@ -83,7 +83,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.provider :virtualbox do |vb| config.vm.provider :virtualbox do |vb|
vb.name = "mastodon" vb.name = "mastodon"
vb.customize ["modifyvm", :id, "--memory", "4096"] vb.customize ["modifyvm", :id, "--memory", "2048"]
# Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions. # Disable VirtualBox DNS proxy to skip long-delay IPv6 resolutions.
# https://github.com/mitchellh/vagrant/issues/1172 # https://github.com/mitchellh/vagrant/issues/1172

View File

@@ -1,28 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin class Admin::AccountModerationNotesController < Admin::BaseController
class AccountModerationNotesController < BaseController
before_action :set_account_moderation_note, only: [:destroy]
def create def create
authorize AccountModerationNote, :create?
@account_moderation_note = current_account.account_moderation_notes.new(resource_params) @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
if @account_moderation_note.save if @account_moderation_note.save
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg') @target_account = @account_moderation_note.target_account
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
else else
@account = @account_moderation_note.target_account @account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest @moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show' render template: 'admin/accounts/show'
end end
end end
def destroy def destroy
authorize @account_moderation_note, :destroy? @account_moderation_note = AccountModerationNote.find(params[:id])
@target_account = @account_moderation_note.target_account
@account_moderation_note.destroy @account_moderation_note.destroy
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg') redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end end
private private
@@ -33,9 +28,4 @@ module Admin
:target_account_id :target_account_id
) )
end end
def set_account_moderation_note
@account_moderation_note = AccountModerationNote.find(params[:id])
end
end
end end

View File

@@ -2,54 +2,29 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload] before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
def index def index
authorize :account, :index?
@accounts = filtered_accounts.page(params[:page]) @accounts = filtered_accounts.page(params[:page])
end end
def show def show
authorize @account, :show?
@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
end end
def subscribe def subscribe
authorize @account, :subscribe?
Pubsubhubbub::SubscribeWorker.perform_async(@account.id) Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def unsubscribe def unsubscribe
authorize @account, :unsubscribe?
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id) Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end
def memorialize
authorize @account, :memorialize?
@account.memorialize!
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
redirect_to admin_account_path(@account.id)
end
def redownload def redownload
authorize @account, :redownload?
@account.reset_avatar! @account.reset_avatar!
@account.reset_header! @account.reset_header!
@account.save! @account.save!
@@ -67,10 +42,6 @@ module Admin
redirect_to admin_account_path(@account.id) if @account.local? redirect_to admin_account_path(@account.id) if @account.local?
end end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def filtered_accounts def filtered_accounts
AccountFilter.new(filter_params).results AccountFilter.new(filter_params).results
end end

View File

@@ -2,9 +2,7 @@
module Admin module Admin
class BaseController < ApplicationController class BaseController < ApplicationController
include Authorization before_action :require_admin!
before_action :require_staff!
layout 'admin' layout 'admin'
end end

View File

@@ -2,18 +2,15 @@
module Admin module Admin
class ConfirmationsController < BaseController class ConfirmationsController < BaseController
before_action :set_user
def create def create
authorize @user, :confirm? account_user.confirm
@user.confirm!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_user def account_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end end
end end
end end

View File

@@ -5,18 +5,14 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_custom_emoji, except: [:index, :new, :create]
def index def index
authorize :custom_emoji, :index? @custom_emojis = filtered_custom_emojis.page(params[:page])
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
end end
def new def new
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new @custom_emoji = CustomEmoji.new
end end
def create def create
authorize :custom_emoji, :create?
@custom_emoji = CustomEmoji.new(resource_params) @custom_emoji = CustomEmoji.new(resource_params)
if @custom_emoji.save if @custom_emoji.save
@@ -26,44 +22,29 @@ module Admin
end end
end end
def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
else
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
end
end
def destroy def destroy
authorize @custom_emoji, :destroy?
@custom_emoji.destroy @custom_emoji.destroy
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
end end
def copy def copy
authorize @custom_emoji, :copy? emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode) if emoji.save
if emoji.update(image: @custom_emoji.image)
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg') flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
else else
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg') flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
end end
redirect_to admin_custom_emojis_path(page: params[:page]) redirect_to admin_custom_emojis_path(params[:page])
end end
def enable def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false) @custom_emoji.update!(disabled: false)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
end end
def disable def disable
authorize @custom_emoji, :disable?
@custom_emoji.update!(disabled: true) @custom_emoji.update!(disabled: true)
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
end end
@@ -75,7 +56,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker) params.require(:custom_emoji).permit(:shortcode, :image)
end end
def filtered_custom_emojis def filtered_custom_emojis

View File

@@ -5,18 +5,14 @@ module Admin
before_action :set_domain_block, only: [:show, :destroy] before_action :set_domain_block, only: [:show, :destroy]
def index def index
authorize :domain_block, :index?
@domain_blocks = DomainBlock.page(params[:page]) @domain_blocks = DomainBlock.page(params[:page])
end end
def new def new
authorize :domain_block, :create?
@domain_block = DomainBlock.new @domain_block = DomainBlock.new
end end
def create def create
authorize :domain_block, :create?
@domain_block = DomainBlock.new(resource_params) @domain_block = DomainBlock.new(resource_params)
if @domain_block.save if @domain_block.save
@@ -27,12 +23,9 @@ module Admin
end end
end end
def show def show; end
authorize @domain_block, :show?
end
def destroy def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end

View File

@@ -5,18 +5,14 @@ module Admin
before_action :set_email_domain_block, only: [:show, :destroy] before_action :set_email_domain_block, only: [:show, :destroy]
def index def index
authorize :email_domain_block, :index?
@email_domain_blocks = EmailDomainBlock.page(params[:page]) @email_domain_blocks = EmailDomainBlock.page(params[:page])
end end
def new def new
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new @email_domain_block = EmailDomainBlock.new
end end
def create def create
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.new(resource_params) @email_domain_block = EmailDomainBlock.new(resource_params)
if @email_domain_block.save if @email_domain_block.save
@@ -27,7 +23,6 @@ module Admin
end end
def destroy def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy @email_domain_block.destroy
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg') redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
end end

View File

@@ -3,12 +3,10 @@
module Admin module Admin
class InstancesController < BaseController class InstancesController < BaseController
def index def index
authorize :instance, :index?
@instances = ordered_instances @instances = ordered_instances
end end
def resubscribe def resubscribe
authorize :instance, :resubscribe?
params.require(:by_domain) params.require(:by_domain)
Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id)) Pubsubhubbub::SubscribeWorker.push_bulk(subscribeable_accounts.pluck(:id))
redirect_to admin_instances_path redirect_to admin_instances_path

View File

@@ -2,20 +2,19 @@
module Admin module Admin
class ReportedStatusesController < BaseController class ReportedStatusesController < BaseController
include Authorization
before_action :set_report before_action :set_report
before_action :set_status, only: [:update, :destroy] before_action :set_status, only: [:update, :destroy]
def create def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@@ -5,17 +5,14 @@ module Admin
before_action :set_report, except: [:index] before_action :set_report, except: [:index]
def index def index
authorize :report, :index?
@reports = filtered_reports.page(params[:page]) @reports = filtered_reports.page(params[:page])
end end
def show def show
authorize @report, :show?
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def update def update
authorize @report, :update?
process_report process_report
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end

View File

@@ -2,18 +2,17 @@
module Admin module Admin
class ResetsController < BaseController class ResetsController < BaseController
before_action :set_user before_action :set_account
def create def create
authorize @user, :reset_password? @account.user.send_reset_password_instructions
@user.send_reset_password_instructions
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
private private
def set_user def set_account
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) @account = Account.find(params[:account_id])
end end
end end
end end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
module Admin
class RolesController < BaseController
before_action :set_user
def promote
authorize @user, :promote?
@user.promote!
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
redirect_to admin_account_path(@user.account_id)
end
private
def set_user
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
end
end
end

View File

@@ -28,13 +28,10 @@ module Admin
).freeze ).freeze
def edit def edit
authorize :settings, :show?
@admin_settings = Form::AdminSettings.new @admin_settings = Form::AdminSettings.new
end end
def update def update
authorize :settings, :update?
settings_params.each do |key, value| settings_params.each do |key, value|
if UPLOAD_SETTINGS.include?(key) if UPLOAD_SETTINGS.include?(key)
upload = SiteUpload.where(var: key).first_or_initialize(var: key) upload = SiteUpload.where(var: key).first_or_initialize(var: key)

View File

@@ -5,13 +5,11 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :silence?
@account.update(silenced: true) @account.update(silenced: true)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsilence?
@account.update(silenced: false) @account.update(silenced: false)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -2,6 +2,8 @@
module Admin module Admin
class StatusesController < BaseController class StatusesController < BaseController
include Authorization
helper_method :current_params helper_method :current_params
before_action :set_account before_action :set_account
@@ -10,30 +12,24 @@ module Admin
PER_PAGE = 20 PER_PAGE = 20
def index def index
authorize :status, :index?
@statuses = @account.statuses @statuses = @account.statuses
if params[:media] if params[:media]
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
@statuses.merge!(Status.where(id: account_media_status_ids)) @statuses.merge!(Status.where(id: account_media_status_ids))
end end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE) @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new @form = Form::StatusBatch.new
end end
def create def create
authorize :status, :update?
@form = Form::StatusBatch.new(form_status_batch_params) @form = Form::StatusBatch.new(form_status_batch_params)
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
def update def update
authorize @status, :update?
@status.update(status_params) @status.update(status_params)
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
@@ -64,7 +60,6 @@ module Admin
def current_params def current_params
page = (params[:page] || 1).to_i page = (params[:page] || 1).to_i
{ {
media: params[:media], media: params[:media],
page: page > 1 && page, page: page > 1 && page,

View File

@@ -3,7 +3,6 @@
module Admin module Admin
class SubscriptionsController < BaseController class SubscriptionsController < BaseController
def index def index
authorize :subscription, :index?
@subscriptions = ordered_subscriptions.page(requested_page) @subscriptions = ordered_subscriptions.page(requested_page)
end end

View File

@@ -5,14 +5,12 @@ module Admin
before_action :set_account before_action :set_account
def create def create
authorize @account, :suspend?
Admin::SuspensionWorker.perform_async(@account.id) Admin::SuspensionWorker.perform_async(@account.id)
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
authorize @account, :unsuspend? @account.update(suspended: false)
@account.unsuspend!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -5,7 +5,6 @@ module Admin
before_action :set_user before_action :set_user
def destroy def destroy
authorize @user, :disable_2fa?
@user.disable_two_factor! @user.disable_two_factor!
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@@ -13,11 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
end end
def follow def follow
reblogs_arg = { reblogs: params[:reblogs] } FollowService.new.call(current_user.account, @account.acct)
FollowService.new.call(current_user.account, @account.acct, reblogs_arg) options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, 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

View File

@@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment] comment: report_params[:comment]
) )
User.staff.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later }
render json: @report, serializer: REST::ReportSerializer render json: @report, serializer: REST::ReportSerializer
end end

View File

@@ -1,8 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 10 RESULTS_LIMIT = 10
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
@@ -11,24 +9,12 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json respond_to :json
def index def index
@search = Search.new(search) @search = Search.new(search_results)
render json: @search, serializer: REST::SearchSerializer render json: @search, serializer: REST::SearchSerializer
end end
private private
def search
search_results.tap do |search|
search[:statuses].keep_if do |status|
begin
authorize status, :show?
rescue Mastodon::NotPermittedError
false
end
end
end
end
def search_results def search_results
SearchService.new.call( SearchService.new.call(
params[:q], params[:q],

View File

@@ -1,60 +0,0 @@
# frozen_string_literal: true
class Api::V1::Timelines::DirectController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action :require_user!, only: [:show]
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
respond_to :json
def show
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
end
private
def load_statuses
cached_direct_statuses
end
def cached_direct_statuses
cache_collection direct_statuses, Status
end
def direct_statuses
direct_timeline_statuses.paginate_by_max_id(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def direct_timeline_statuses
Status.as_direct_timeline(current_account)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:local, :limit).merge(core_params)
end
def next_path
api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id)
end
def pagination_max_id
@statuses.last.id
end
def pagination_since_id
@statuses.first.id
end
end

View File

@@ -13,13 +13,11 @@ class ApplicationController < ActionController::Base
helper_method :current_account helper_method :current_account
helper_method :current_session helper_method :current_session
helper_method :current_theme helper_method :current_theme
helper_method :theme_data
helper_method :single_user_mode? helper_method :single_user_mode?
rescue_from ActionController::RoutingError, with: :not_found rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::NotPermittedError, with: :forbidden
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
before_action :check_suspension, if: :user_signed_in? before_action :check_suspension, if: :user_signed_in?
@@ -42,10 +40,6 @@ class ApplicationController < ActionController::Base
redirect_to root_path unless current_user&.admin? redirect_to root_path unless current_user&.admin?
end end
def require_staff!
redirect_to root_path unless current_user&.staff?
end
def check_suspension def check_suspension
forbidden if current_user.account.suspended? forbidden if current_user.account.suspended?
end end
@@ -89,10 +83,6 @@ class ApplicationController < ActionController::Base
current_user.setting_theme current_user.setting_theme
end end
def theme_data
Themes.instance.get(current_theme)
end
def cache_collection(raw, klass) def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes) return raw unless klass.respond_to?(:with_includes)
@@ -109,7 +99,7 @@ class ApplicationController < ActionController::Base
unless uncached_ids.empty? unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
uncached.each_value do |item| uncached.values.each do |item|
Rails.cache.write(item.cache_key, item) Rails.cache.write(item.cache_key, item)
end end
end end

View File

@@ -62,7 +62,7 @@ class Auth::SessionsController < Devise::SessionsController
if user_params[:otp_attempt].present? && session[:otp_user_id] if user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user) authenticate_with_two_factor_via_otp(user)
elsif user&.valid_password?(user_params[:password]) elsif user && user.valid_password?(user_params[:password])
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
end end

View File

@@ -2,7 +2,6 @@
module Authorization module Authorization
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Pundit include Pundit
def pundit_user def pundit_user

View File

@@ -6,6 +6,7 @@ class HomeController < ApplicationController
def index def index
@body_classes = 'app-body' @body_classes = 'app-body'
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
end end
private private

View File

@@ -1,64 +0,0 @@
# frozen_string_literal: true
class Settings::KeywordMutesController < ApplicationController
layout 'admin'
before_action :authenticate_user!
before_action :load_keyword_mute, only: [:edit, :update, :destroy]
def index
@keyword_mutes = paginated_keyword_mutes_for_account
end
def new
@keyword_mute = keyword_mutes_for_account.build
end
def create
@keyword_mute = keyword_mutes_for_account.create(keyword_mute_params)
if @keyword_mute.persisted?
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
else
render :new
end
end
def update
if @keyword_mute.update(keyword_mute_params)
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
else
render :edit
end
end
def destroy
@keyword_mute.destroy!
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
end
def destroy_all
keyword_mutes_for_account.delete_all
redirect_to settings_keyword_mutes_path, notice: I18n.t('generic.changes_saved_msg')
end
private
def keyword_mutes_for_account
Glitch::KeywordMute.where(account: current_account)
end
def load_keyword_mute
@keyword_mute = keyword_mutes_for_account.find(params[:id])
end
def keyword_mute_params
params.require(:keyword_mute).permit(:keyword, :whole_word)
end
def paginated_keyword_mutes_for_account
keyword_mutes_for_account.order(:keyword).page params[:page]
end
end

View File

@@ -26,7 +26,7 @@ class Settings::NotificationsController < ApplicationController
def user_settings_params def user_settings_params
params.require(:user).permit( params.require(:user).permit(
notification_emails: %i(follow follow_request reblog favourite mention digest), notification_emails: %i(follow follow_request reblog favourite mention digest),
interactions: %i(must_be_follower must_be_following must_be_following_dm) interactions: %i(must_be_follower must_be_following)
) )
end end
end end

View File

@@ -18,7 +18,7 @@ module Admin::FilterHelper
def selected?(more_params) def selected?(more_params)
new_url = filtered_url_for(more_params) new_url = filtered_url_for(more_params)
filter_link_class(new_url) == 'selected' filter_link_class(new_url) == 'selected' ? true : false
end end
private private

View File

@@ -35,11 +35,6 @@ module ApplicationHelper
Rails.env.production? ? site_title : "#{site_title} (Dev)" Rails.env.production? ? site_title : "#{site_title} (Dev)"
end end
def can?(action, record)
return false if record.nil?
policy(record).public_send("#{action}?")
end
def fa_icon(icon, attributes = {}) def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || [] class_names = attributes[:class]&.split(' ') || []
class_names << 'fa' class_names << 'fa'
@@ -48,10 +43,6 @@ module ApplicationHelper
content_tag(:i, nil, attributes.merge(class: class_names.join(' '))) content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end end
def custom_emoji_tag(custom_emoji)
image_tag(custom_emoji.image.url, class: 'emojione', alt: ":#{custom_emoji.shortcode}:")
end
def opengraph(property, content) def opengraph(property, content)
tag(:meta, content: content, property: property) tag(:meta, content: content, property: property)
end end

View File

@@ -9,10 +9,6 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value value.is_a?(Array) ? value.first : value
end end
def as_array(value)
value.is_a?(Array) ? value : [value]
end
def value_or_id(value) def value_or_id(value)
value.is_a?(String) || value.nil? ? value : value['id'] value.is_a?(String) || value.nil? ? value : value['id']
end end

View File

@@ -1,2 +0,0 @@
module Settings::KeywordMutesHelper
end

View File

@@ -51,7 +51,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import emojify from '../../../mastodon/features/emoji/emoji'; import emojify from '../../../mastodon/features/emoji/emoji';
import IconButton from '../../../mastodon/components/icon_button'; import IconButton from '../../../mastodon/components/icon_button';
import Avatar from '../../../mastodon/components/avatar'; import Avatar from '../../../mastodon/components/avatar';
import { me } from '../../../mastodon/initial_state';
// Our imports // // Our imports //
import { processBio } from '../../util/bio_metadata'; import { processBio } from '../../util/bio_metadata';
@@ -89,6 +88,7 @@ export default class AccountHeader extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account : ImmutablePropTypes.map, account : ImmutablePropTypes.map,
me : PropTypes.string.isRequired,
onFollow : PropTypes.func.isRequired, onFollow : PropTypes.func.isRequired,
intl : PropTypes.object.isRequired, intl : PropTypes.object.isRequired,
}; };
@@ -102,7 +102,7 @@ The `render()` function is used to render our component.
*/ */
render () { render () {
const { account, intl } = this.props; const { account, me, intl } = this.props;
/* /*
@@ -152,7 +152,7 @@ appropriate icon.
<IconButton <IconButton
size={26} size={26}
icon={following ? 'user-times' : 'user-plus'} icon={following ? 'user-times' : 'user-plus'}
active={following ? true : false} active={following}
title={intl.formatMessage(following ? messages.unfollow : messages.follow)} title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
onClick={this.props.onFollow} onClick={this.props.onFollow}
/> />

View File

@@ -124,16 +124,6 @@ export default class LocalSettingsPage extends React.PureComponent {
> >
<FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' /> <FormattedMessage id='settings.auto_collapse_lengthy' defaultMessage='Lengthy toots' />
</LocalSettingsPageItem> </LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
item={['collapsed', 'auto', 'reblogs']}
id='mastodon-settings--collapsed-auto-reblogs'
onChange={onChange}
dependsOn={[['collapsed', 'enabled']]}
dependsOnNot={[['collapsed', 'auto', 'all']]}
>
<FormattedMessage id='settings.auto_collapse_reblogs' defaultMessage='Boosts' />
</LocalSettingsPageItem>
<LocalSettingsPageItem <LocalSettingsPageItem
settings={settings} settings={settings}
item={['collapsed', 'auto', 'replies']} item={['collapsed', 'auto', 'replies']}

View File

@@ -9,7 +9,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import RelativeTimestamp from '../../../mastodon/components/relative_timestamp'; import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
import IconButton from '../../../mastodon/components/icon_button'; import IconButton from '../../../mastodon/components/icon_button';
import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
import { me } from '../../../mastodon/initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -51,6 +50,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
me: PropTypes.string,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@@ -59,6 +59,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage. // evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [ updateOnProps = [
'status', 'status',
'me',
'withDismiss', 'withDismiss',
] ]
@@ -118,7 +119,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
render () { render () {
const { status, intl, withDismiss } = this.props; const { status, me, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;

View File

@@ -140,10 +140,12 @@ Here are the props we pass to `<Status>`.
return { return {
status : status, status : status,
account : account || ownProps.account, account : account || ownProps.account,
me : state.getIn(['meta', 'me']),
settings : state.get('local_settings'), settings : state.get('local_settings'),
prepend : prepend || ownProps.prepend, prepend : prepend || ownProps.prepend,
reblogModal : state.getIn(['meta', 'boost_modal']), reblogModal : state.getIn(['meta', 'boost_modal']),
deleteModal : state.getIn(['meta', 'delete_modal']), deleteModal : state.getIn(['meta', 'delete_modal']),
autoPlayGif : state.getIn(['meta', 'auto_play_gif']),
}; };
}; };

View File

@@ -39,7 +39,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
// Mastodon imports // // Mastodon imports //
import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task'; import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
import { autoPlayGif } from '../../../mastodon/initial_state';
// Our imports // // Our imports //
import StatusPrepend from './prepend'; import StatusPrepend from './prepend';
@@ -90,6 +89,9 @@ few parts:
These are our local settings, fetched from our store. We need this These are our local settings, fetched from our store. We need this
to determine how best to collapse our statuses, among other things. to determine how best to collapse our statuses, among other things.
- __`me` (`PropTypes.number`) :__
This is the id of the currently-signed-in user.
- __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`, - __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`,
`onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`, `onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
`onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__ `onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
@@ -101,6 +103,9 @@ few parts:
reblogging and deleting statuses. They are used by the `onReblog` reblogging and deleting statuses. They are used by the `onReblog`
and `onDelete` functions, but we don't deal with them here. and `onDelete` functions, but we don't deal with them here.
- __`autoPlayGif` (`PropTypes.bool`) :__
This tells the frontend whether or not to autoplay gifs!
- __`muted` (`PropTypes.bool`) :__ - __`muted` (`PropTypes.bool`) :__
This has nothing to do with a user or conversation mute! "Muted" is This has nothing to do with a user or conversation mute! "Muted" is
what Mastodon internally calls the subdued look of statuses in the what Mastodon internally calls the subdued look of statuses in the
@@ -155,6 +160,7 @@ export default class Status extends ImmutablePureComponent {
account : ImmutablePropTypes.map, account : ImmutablePropTypes.map,
settings : ImmutablePropTypes.map, settings : ImmutablePropTypes.map,
notification : ImmutablePropTypes.map, notification : ImmutablePropTypes.map,
me : PropTypes.string,
onFavourite : PropTypes.func, onFavourite : PropTypes.func,
onReblog : PropTypes.func, onReblog : PropTypes.func,
onModalReblog : PropTypes.func, onModalReblog : PropTypes.func,
@@ -171,6 +177,7 @@ export default class Status extends ImmutablePureComponent {
onOpenVideo : PropTypes.func, onOpenVideo : PropTypes.func,
reblogModal : PropTypes.bool, reblogModal : PropTypes.bool,
deleteModal : PropTypes.bool, deleteModal : PropTypes.bool,
autoPlayGif : PropTypes.bool,
muted : PropTypes.bool, muted : PropTypes.bool,
collapse : PropTypes.bool, collapse : PropTypes.bool,
prepend : PropTypes.string, prepend : PropTypes.string,
@@ -204,7 +211,9 @@ to remember to specify it here.
'account', 'account',
'settings', 'settings',
'prepend', 'prepend',
'me',
'boostModal', 'boostModal',
'autoPlayGif',
'muted', 'muted',
'collapse', 'collapse',
'notification', 'notification',
@@ -278,7 +287,6 @@ properly and our intersection observer is good to go.
muted, muted,
id, id,
intersectionObserverWrapper, intersectionObserverWrapper,
prepend,
} = this.props; } = this.props;
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']); const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
@@ -291,9 +299,6 @@ properly and our intersection observer is good to go.
node.clientHeight > ( node.clientHeight > (
status.get('media_attachments').size && !muted ? 650 : 400 status.get('media_attachments').size && !muted ? 650 : 400
) )
) || (
autoCollapseSettings.get('reblogs') &&
prepend === 'reblogged_by'
) || ( ) || (
autoCollapseSettings.get('replies') && autoCollapseSettings.get('replies') &&
status.get('in_reply_to_id', null) !== null status.get('in_reply_to_id', null) !== null
@@ -551,6 +556,7 @@ this operation are further explained in the code below.
intersectionObserverWrapper, intersectionObserverWrapper,
onOpenVideo, onOpenVideo,
onOpenMedia, onOpenMedia,
autoPlayGif,
notification, notification,
...other ...other
} = this.props; } = this.props;

View File

@@ -14,7 +14,6 @@
"settings.auto_collapse_lengthy": "Lengthy toots", "settings.auto_collapse_lengthy": "Lengthy toots",
"settings.auto_collapse_media": "Toots with media", "settings.auto_collapse_media": "Toots with media",
"settings.auto_collapse_notifications": "Notifications", "settings.auto_collapse_notifications": "Notifications",
"settings.auto_collapse_reblogs": "Boosts",
"settings.auto_collapse_replies": "Replies", "settings.auto_collapse_replies": "Replies",
"settings.close": "Close", "settings.close": "Close",
"settings.collapsed_statuses": "Collapsed toots", "settings.collapsed_statuses": "Collapsed toots",

View File

@@ -59,7 +59,6 @@ const initialState = ImmutableMap({
all : false, all : false,
notifications : true, notifications : true,
lengthy : true, lengthy : true,
reblogs : false,
replies : false, replies : false,
media : false, media : false,
}), }),

View File

@@ -105,13 +105,12 @@ export function fetchAccountFail(id, error) {
}; };
}; };
export function followAccount(id, reblogs = true) { export function followAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
const alreadyFollowing = getState().getIn(['relationships', id, 'following']);
dispatch(followAccountRequest(id)); dispatch(followAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { api(getState).post(`/api/v1/accounts/${id}/follow`).then(response => {
dispatch(followAccountSuccess(response.data, alreadyFollowing)); dispatch(followAccountSuccess(response.data));
}).catch(error => { }).catch(error => {
dispatch(followAccountFail(error)); dispatch(followAccountFail(error));
}); });
@@ -137,11 +136,10 @@ export function followAccountRequest(id) {
}; };
}; };
export function followAccountSuccess(relationship, alreadyFollowing) { export function followAccountSuccess(relationship) {
return { return {
type: ACCOUNT_FOLLOW_SUCCESS, type: ACCOUNT_FOLLOW_SUCCESS,
relationship, relationship,
alreadyFollowing,
}; };
}; };

View File

@@ -8,7 +8,6 @@ import {
refreshHomeTimeline, refreshHomeTimeline,
refreshCommunityTimeline, refreshCommunityTimeline,
refreshPublicTimeline, refreshPublicTimeline,
refreshDirectTimeline,
} from './timelines'; } from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
@@ -134,8 +133,6 @@ export function submitCompose() {
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertOrRefresh('community', refreshCommunityTimeline); insertOrRefresh('community', refreshCommunityTimeline);
insertOrRefresh('public', refreshPublicTimeline); insertOrRefresh('public', refreshPublicTimeline);
} else if (response.data.visibility === 'direct') {
insertOrRefresh('direct', refreshDirectTimeline);
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));

View File

@@ -4,13 +4,12 @@ export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
import { me } from '../initial_state';
export function fetchPinnedStatuses() { export function fetchPinnedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest()); dispatch(fetchPinnedStatusesRequest());
api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { const accountId = getState().getIn(['meta', 'me']);
api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null)); dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => { }).catch(error => {
dispatch(fetchPinnedStatusesFail(error)); dispatch(fetchPinnedStatusesFail(error));

View File

@@ -1,4 +1,4 @@
import { connectStream } from '../stream'; import createStream from '../stream';
import { import {
updateTimeline, updateTimeline,
deleteFromTimelines, deleteFromTimelines,
@@ -12,19 +12,42 @@ import { getLocale } from '../locales';
const { messages } = getLocale(); const { messages } = getLocale();
export function connectTimelineStream (timelineId, path, pollingRefresh = null) { export function connectTimelineStream (timelineId, path, pollingRefresh = null) {
return (dispatch, getState) => {
return connectStream (path, pollingRefresh, (dispatch, getState) => { const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
const accessToken = getState().getIn(['meta', 'access_token']);
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);
return { let polling = null;
onConnect() {
const setupPolling = () => {
polling = setInterval(() => {
pollingRefresh(dispatch);
}, 20000);
};
const clearPolling = () => {
if (polling) {
clearInterval(polling);
polling = null;
}
};
const subscription = createStream(streamingAPIBaseURL, accessToken, path, {
connected () {
if (pollingRefresh) {
clearPolling();
}
dispatch(connectTimeline(timelineId)); dispatch(connectTimeline(timelineId));
}, },
onDisconnect() { disconnected () {
if (pollingRefresh) {
setupPolling();
}
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline(timelineId));
}, },
onReceive (data) { received (data) {
switch(data.event) { switch(data.event) {
case 'update': case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload))); dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
@@ -37,8 +60,26 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
break; break;
} }
}, },
};
reconnected () {
if (pollingRefresh) {
clearPolling();
pollingRefresh(dispatch);
}
dispatch(connectTimeline(timelineId));
},
}); });
const disconnect = () => {
if (subscription) {
subscription.close();
}
clearPolling();
};
return disconnect;
};
} }
function refreshHomeTimelineAndNotification (dispatch) { function refreshHomeTimelineAndNotification (dispatch) {
@@ -51,4 +92,3 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public'); export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');

View File

@@ -115,7 +115,6 @@ export function refreshTimeline(timelineId, path, params = {}) {
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
@@ -156,7 +155,6 @@ export function expandTimeline(timelineId, path, params = {}) {
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home'); export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true }); export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);

View File

@@ -7,7 +7,6 @@ import Permalink from './permalink';
import IconButton from './icon_button'; import IconButton from './icon_button';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' },
@@ -24,6 +23,7 @@ export default class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
@@ -52,7 +52,7 @@ export default class Account extends ImmutablePureComponent {
} }
render () { render () {
const { account, intl, hidden } = this.props; const { account, me, intl, hidden } = this.props;
if (!account) { if (!account) {
return <div />; return <div />;
@@ -93,7 +93,7 @@ export default class Account extends ImmutablePureComponent {
</div> </div>
); );
} else { } else {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following ? true : false} />; buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
} }
} }

View File

@@ -175,9 +175,7 @@ export default class ColumnHeader extends React.PureComponent {
<div className={wrapperClassName}> <div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}> <h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} /> <i className={`fa fa-fw fa-${icon} column-header__icon`} />
<span className='column-header__title'>
{title} {title}
</span>
<div className='column-header__buttons'> <div className='column-header__buttons'>
{backButton} {backButton}
{ notifCleaning ? ( { notifCleaning ? (

View File

@@ -65,7 +65,6 @@ export default class IconButton extends React.PureComponent {
expanded, expanded,
icon, icon,
inverted, inverted,
flip,
overlay, overlay,
pressed, pressed,
tabIndex, tabIndex,
@@ -79,8 +78,8 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay, overlayed: overlay,
}); });
const flipDeg = flip ? -180 : -360; const flipDeg = this.props.flip ? -180 : -360;
const rotateDeg = active ? flipDeg : 0; const rotateDeg = this.props.active ? flipDeg : 0;
const motionDefaultStyle = { const motionDefaultStyle = {
rotate: rotateDeg, rotate: rotateDeg,
@@ -94,25 +93,6 @@ export default class IconButton extends React.PureComponent {
rotate: animate ? spring(rotateDeg, springOpts) : 0, rotate: animate ? spring(rotateDeg, springOpts) : 0,
}; };
if (!animate) {
// Perf optimization: avoid unnecessary <Motion> components unless
// we actually need to animate.
return (
<button
aria-label={title}
aria-pressed={pressed}
aria-expanded={expanded}
title={title}
className={classes}
onClick={this.handleClick}
style={style}
tabIndex={tabIndex}
>
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
</button>
);
}
return ( return (
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}> <Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
{({ rotate }) => {({ rotate }) =>

View File

@@ -9,7 +9,6 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile'; import { isIOS } from '../is_mobile';
import classNames from 'classnames'; import classNames from 'classnames';
import { autoPlayGif } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' }, toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@@ -27,9 +26,11 @@ class Item extends React.PureComponent {
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
autoPlayGif: false,
standalone: false, standalone: false,
index: 0, index: 0,
size: 1, size: 1,
@@ -49,7 +50,7 @@ class Item extends React.PureComponent {
} }
hoverToPlay () { hoverToPlay () {
const { attachment } = this.props; const { attachment, autoPlayGif } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv'; return !autoPlayGif && attachment.get('type') === 'gifv';
} }
@@ -141,7 +142,7 @@ class Item extends React.PureComponent {
</a> </a>
); );
} else if (attachment.get('type') === 'gifv') { } else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && autoPlayGif; const autoPlay = !isIOS() && this.props.autoPlayGif;
thumbnail = ( thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@@ -183,9 +184,11 @@ export default class MediaGallery extends React.PureComponent {
height: PropTypes.number.isRequired, height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
autoPlayGif: false,
standalone: false, standalone: false,
}; };
@@ -261,9 +264,9 @@ export default class MediaGallery extends React.PureComponent {
const size = media.take(4).size; const size = media.take(4).size;
if (this.isStandaloneEligible()) { if (this.isStandaloneEligible()) {
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />; children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
} else { } else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
} }
} }

View File

@@ -1,5 +1,5 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more'; import LoadMore from './load_more';

View File

@@ -39,6 +39,9 @@ export default class Status extends ImmutablePureComponent {
onBlock: PropTypes.func, onBlock: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onHeightChange: PropTypes.func, onHeightChange: PropTypes.func,
me: PropTypes.string,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
muted: PropTypes.bool, muted: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
@@ -54,6 +57,9 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [ updateOnProps = [
'status', 'status',
'account', 'account',
'me',
'boostModal',
'autoPlayGif',
'muted', 'muted',
'hidden', 'hidden',
] ]
@@ -194,7 +200,7 @@ export default class Status extends ImmutablePureComponent {
} else { } else {
media = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} > <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />} {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />}
</Bundle> </Bundle>
); );
} }

View File

@@ -8,7 +8,6 @@ import IconButton from './icon_button';
import DropdownMenuContainer from '../containers/dropdown_menu_container'; import DropdownMenuContainer from '../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -51,6 +50,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
onMuteConversation: PropTypes.func, onMuteConversation: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
me: PropTypes.string,
withDismiss: PropTypes.bool, withDismiss: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@@ -59,6 +59,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
// evaluate to false. See react-immutable-pure-component for usage. // evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [ updateOnProps = [
'status', 'status',
'me',
'withDismiss', 'withDismiss',
] ]
@@ -118,7 +119,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
render () { render () {
const { status, intl, withDismiss } = this.props; const { status, me, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;

View File

@@ -13,7 +13,6 @@ import {
} from '../actions/accounts'; } from '../actions/accounts';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { initMuteModal } from '../actions/mutes'; import { initMuteModal } from '../actions/mutes';
import { unfollowModal } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -24,6 +23,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
account: getAccount(state, props.id), account: getAccount(state, props.id),
me: state.getIn(['meta', 'me']),
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
}); });
return mapStateToProps; return mapStateToProps;
@@ -33,7 +34,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) { if (this.unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm), confirm: intl.formatMessage(messages.unfollowConfirm),

View File

@@ -6,14 +6,15 @@ import { hydrateStore } from '../actions/store';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import Compose from '../features/standalone/compose'; import Compose from '../features/standalone/compose';
import initialState from '../initial_state';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
const store = configureStore(); const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialState) { if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }

View File

@@ -4,18 +4,23 @@ import PropTypes from 'prop-types';
import configureStore from '../store/configureStore'; import configureStore from '../store/configureStore';
import { showOnboardingOnce } from '../actions/onboarding'; import { showOnboardingOnce } from '../actions/onboarding';
import { BrowserRouter, Route } from 'react-router-dom'; import { BrowserRouter, Route } from 'react-router-dom';
import { ScrollContext } from 'react-router-scroll-4'; import { ScrollContext } from 'react-router-scroll';
import UI from '../features/ui'; import UI from '../features/ui';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming'; import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import initialState from '../initial_state';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
export const store = configureStore(); export const store = configureStore();
const initialState = JSON.parse(document.getElementById('initial-state').textContent);
try {
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
} catch (e) {
initialState.local_settings = {};
}
const hydrateAction = hydrateStore(initialState); const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction); store.dispatch(hydrateAction);

View File

@@ -17,18 +17,20 @@ import {
pin, pin,
unpin, unpin,
} from '../actions/interactions'; } from '../actions/interactions';
import { blockAccount } from '../actions/accounts'; import {
blockAccount,
muteAccount,
} from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initMuteModal } from '../actions/mutes';
import { initReport } from '../actions/reports'; import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
}); });
const makeMapStateToProps = () => { const makeMapStateToProps = () => {
@@ -36,6 +38,10 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
status: getStatus(state, props.id), status: getStatus(state, props.id),
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
}); });
return mapStateToProps; return mapStateToProps;
@@ -55,7 +61,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else { } else {
if (e.shiftKey || !boostModal) { if (e.shiftKey || !this.boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
@@ -84,7 +90,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onDelete (status) { onDelete (status) {
if (!deleteModal) { if (!this.deleteModal) {
dispatch(deleteStatus(status.get('id'))); dispatch(deleteStatus(status.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
@@ -120,7 +126,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onMute (account) { onMute (account) {
dispatch(initMuteModal(account)); dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.muteConfirm),
onConfirm: () => dispatch(muteAccount(account.get('id'))),
}));
}, },
onMuteConversation (status) { onMuteConversation (status) {

View File

@@ -7,14 +7,15 @@ import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import PublicTimeline from '../features/standalone/public_timeline'; import PublicTimeline from '../features/standalone/public_timeline';
import HashtagTimeline from '../features/standalone/hashtag_timeline'; import HashtagTimeline from '../features/standalone/hashtag_timeline';
import initialState from '../initial_state';
const { localeData, messages } = getLocale(); const { localeData, messages } = getLocale();
addLocaleData(localeData); addLocaleData(localeData);
const store = configureStore(); const store = configureStore();
const initialStateContainer = document.getElementById('initial-state');
if (initialState) { if (initialStateContainer !== null) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }

View File

@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl';
import { me } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
@@ -20,8 +19,6 @@ const messages = defineMessages({
media: { id: 'account.media', defaultMessage: 'Media' }, media: { id: 'account.media', defaultMessage: 'Media' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
}); });
@injectIntl @injectIntl
@@ -29,10 +26,10 @@ export default class ActionBar extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func, onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired,
@@ -47,7 +44,7 @@ export default class ActionBar extends React.PureComponent {
} }
render () { render () {
const { account, intl } = this.props; const { account, me, intl } = this.props;
let menu = []; let menu = [];
let extraInfo = ''; let extraInfo = '';
@@ -63,15 +60,6 @@ export default class ActionBar extends React.PureComponent {
if (account.get('id') === me) { if (account.get('id') === me) {
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
} else { } else {
const following = account.getIn(['relationship', 'following']);
if (following) {
if (following.get('reblogs')) {
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
} else {
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
}
}
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
} else { } else {

View File

@@ -8,8 +8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import Motion from '../../ui/util/optional_motion'; import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring'; import spring from 'react-motion/lib/spring';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { autoPlayGif, me } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@@ -17,10 +17,19 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
}); });
const makeMapStateToProps = () => {
const mapStateToProps = state => ({
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
});
return mapStateToProps;
};
class Avatar extends ImmutablePureComponent { class Avatar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
}; };
state = { state = {
@@ -38,7 +47,7 @@ class Avatar extends ImmutablePureComponent {
} }
render () { render () {
const { account } = this.props; const { account, autoPlayGif } = this.props;
const { isHovered } = this.state; const { isHovered } = this.state;
return ( return (
@@ -65,17 +74,20 @@ class Avatar extends ImmutablePureComponent {
} }
@connect(makeMapStateToProps)
@injectIntl @injectIntl
export default class Header extends ImmutablePureComponent { export default class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
autoPlayGif: PropTypes.bool.isRequired,
}; };
render () { render () {
const { account, intl } = this.props; const { account, me, intl } = this.props;
if (!account) { if (!account) {
return null; return null;
@@ -115,7 +127,7 @@ export default class Header extends ImmutablePureComponent {
return ( return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div> <div>
<Avatar account={account} /> <Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} /> <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>

View File

@@ -12,13 +12,14 @@ import { getAccountGallery } from '../../selectors';
import MediaItem from './components/media_item'; import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import LoadMore from '../../components/load_more'; import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, props.params.accountId), medias: 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`, 'next']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@@ -30,6 +31,7 @@ export default class AccountGallery extends ImmutablePureComponent {
medias: ImmutablePropTypes.list.isRequired, medias: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
autoPlayGif: PropTypes.bool,
}; };
componentDidMount () { componentDidMount () {
@@ -65,7 +67,7 @@ export default class AccountGallery extends ImmutablePureComponent {
} }
render () { render () {
const { medias, isLoading, hasMore } = this.props; const { medias, autoPlayGif, isLoading, hasMore } = this.props;
let loadMore = null; let loadMore = null;
@@ -98,6 +100,7 @@ export default class AccountGallery extends ImmutablePureComponent {
<MediaItem <MediaItem
key={media.get('id')} key={media.get('id')}
media={media} media={media}
autoPlayGif={autoPlayGif}
/> />
)} )}
{loadMore} {loadMore}

View File

@@ -10,10 +10,10 @@ export default class Header extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
me: PropTypes.string.isRequired,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func.isRequired,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired,
onReblogToggle: PropTypes.func.isRequired,
onReport: PropTypes.func.isRequired, onReport: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
onBlockDomain: PropTypes.func.isRequired, onBlockDomain: PropTypes.func.isRequired,
@@ -40,10 +40,6 @@ export default class Header extends ImmutablePureComponent {
this.props.onReport(this.props.account); this.props.onReport(this.props.account);
} }
handleReblogToggle = () => {
this.props.onReblogToggle(this.props.account);
}
handleMute = () => { handleMute = () => {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
} }
@@ -65,7 +61,7 @@ export default class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account } = this.props; const { account, me } = this.props;
if (account === null) { if (account === null) {
return <MissingIndicator />; return <MissingIndicator />;
@@ -75,14 +71,15 @@ export default class Header extends ImmutablePureComponent {
<div className='account-timeline__header'> <div className='account-timeline__header'>
<InnerHeader <InnerHeader
account={account} account={account}
me={me}
onFollow={this.handleFollow} onFollow={this.handleFollow}
/> />
<ActionBar <ActionBar
account={account} account={account}
me={me}
onBlock={this.handleBlock} onBlock={this.handleBlock}
onMention={this.handleMention} onMention={this.handleMention}
onReblogToggle={this.handleReblogToggle}
onReport={this.handleReport} onReport={this.handleReport}
onMute={this.handleMute} onMute={this.handleMute}
onBlockDomain={this.handleBlockDomain} onBlockDomain={this.handleBlockDomain}

View File

@@ -15,7 +15,6 @@ import { initReport } from '../../../actions/reports';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks'; import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { unfollowModal } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
@@ -28,6 +27,8 @@ const makeMapStateToProps = () => {
const mapStateToProps = (state, { accountId }) => ({ const mapStateToProps = (state, { accountId }) => ({
account: getAccount(state, accountId), account: getAccount(state, accountId),
me: state.getIn(['meta', 'me']),
unfollowModal: state.getIn(['meta', 'unfollow_modal']),
}); });
return mapStateToProps; return mapStateToProps;
@@ -37,7 +38,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
onFollow (account) { onFollow (account) {
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
if (unfollowModal) { if (this.unfollowModal) {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
confirm: intl.formatMessage(messages.unfollowConfirm), confirm: intl.formatMessage(messages.unfollowConfirm),
@@ -67,14 +68,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onReblogToggle (account) {
if (account.getIn(['relationship', 'following', 'reblogs'])) {
dispatch(followAccount(account.get('id'), false));
} else {
dispatch(followAccount(account.get('id'), true));
}
},
onReport (account) { onReport (account) {
dispatch(initReport(account)); dispatch(initReport(account));
}, },

View File

@@ -16,6 +16,7 @@ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()), statusIds: state.getIn(['timelines', `account:${props.params.accountId}`, 'items'], ImmutableList()),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']), isLoading: state.getIn(['timelines', `account:${props.params.accountId}`, 'isLoading']),
hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']), hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}`, 'next']),
me: state.getIn(['meta', 'me']),
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@@ -27,6 +28,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds: ImmutablePropTypes.list, statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool, isLoading: PropTypes.bool,
hasMore: PropTypes.bool, hasMore: PropTypes.bool,
me: PropTypes.string.isRequired,
}; };
componentWillMount () { componentWillMount () {
@@ -48,7 +50,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, isLoading, hasMore } = this.props; const { statusIds, isLoading, hasMore, me } = this.props;
if (!statusIds && isLoading) { if (!statusIds && isLoading) {
return ( return (
@@ -68,6 +70,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
statusIds={statusIds} statusIds={statusIds}
isLoading={isLoading} isLoading={isLoading}
hasMore={hasMore} hasMore={hasMore}
me={me}
onScrollToBottom={this.handleScrollToBottom} onScrollToBottom={this.handleScrollToBottom}
/> />
</Column> </Column>

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';

View File

@@ -45,6 +45,7 @@ export default class ComposeForm extends ImmutablePureComponent {
preselectDate: PropTypes.instanceOf(Date), preselectDate: PropTypes.instanceOf(Date),
is_submitting: PropTypes.bool, is_submitting: PropTypes.bool,
is_uploading: PropTypes.bool, is_uploading: PropTypes.bool,
me: PropTypes.string,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
onClearSuggestions: PropTypes.func.isRequired, onClearSuggestions: PropTypes.func.isRequired,

View File

@@ -157,6 +157,7 @@ class EmojiPickerMenu extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired, skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired,
autoPlay: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@@ -234,7 +235,7 @@ class EmojiPickerMenu extends React.PureComponent {
} }
render () { render () {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) { if (loading) {
return <div style={{ width: 299 }} />; return <div style={{ width: 299 }} />;
@@ -249,7 +250,7 @@ class EmojiPickerMenu extends React.PureComponent {
perLine={8} perLine={8}
emojiSize={22} emojiSize={22}
sheetSize={32} sheetSize={32}
custom={buildCustomEmojis(custom_emojis)} custom={buildCustomEmojis(custom_emojis, autoPlay)}
color='' color=''
emoji='' emoji=''
set='twitter' set='twitter'
@@ -283,6 +284,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
static propTypes = { static propTypes = {
custom_emojis: ImmutablePropTypes.list, custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
autoPlay: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired,
@@ -344,7 +346,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
} }
render () { render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
const title = intl.formatMessage(messages.emoji); const title = intl.formatMessage(messages.emoji);
const { active, loading } = this.state; const { active, loading } = this.state;
@@ -364,6 +366,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
loading={loading} loading={loading}
onClose={this.onHideDropdown} onClose={this.onHideDropdown}
onPick={onPickEmoji} onPick={onPickEmoji}
autoPlay={autoPlay}
onSkinTone={onSkinTone} onSkinTone={onSkinTone}
skinTone={skinTone} skinTone={skinTone}
frequentlyUsedEmojis={frequentlyUsedEmojis} frequentlyUsedEmojis={frequentlyUsedEmojis}

View File

@@ -23,6 +23,7 @@ const mapStateToProps = state => ({
preselectDate: state.getIn(['compose', 'preselectDate']), preselectDate: state.getIn(['compose', 'preselectDate']),
is_submitting: state.getIn(['compose', 'is_submitting']), is_submitting: state.getIn(['compose', 'is_submitting']),
is_uploading: state.getIn(['compose', 'is_uploading']), is_uploading: state.getIn(['compose', 'is_uploading']),
me: state.getIn(['compose', 'me']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
settings: state.get('local_settings'), settings: state.get('local_settings'),
filesAttached: state.getIn(['compose', 'media_attachments']).size > 0, filesAttached: state.getIn(['compose', 'media_attachments']).size > 0,

View File

@@ -46,7 +46,7 @@ const getFrequentlyUsedEmojis = createSelector([
const getCustomEmojis = createSelector([ const getCustomEmojis = createSelector([
state => state.get('custom_emojis'), state => state.get('custom_emojis'),
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => { ], emojis => emojis.sort((a, b) => {
const aShort = a.get('shortcode').toLowerCase(); const aShort = a.get('shortcode').toLowerCase();
const bShort = b.get('shortcode').toLowerCase(); const bShort = b.get('shortcode').toLowerCase();
@@ -61,6 +61,7 @@ const getCustomEmojis = createSelector([
const mapStateToProps = state => ({ const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state), custom_emojis: getCustomEmojis(state),
autoPlay: state.getIn(['meta', 'auto_play_gif']),
skinTone: state.getIn(['settings', 'skinTone']), skinTone: state.getIn(['settings', 'skinTone']),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state), frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
}); });

View File

@@ -1,10 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import NavigationBar from '../components/navigation_bar'; import NavigationBar from '../components/navigation_bar';
import { me } from '../../../initial_state';
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
}; };
}; };

View File

@@ -3,10 +3,9 @@ import { connect } from 'react-redux';
import Warning from '../components/warning'; import Warning from '../components/warning';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', state.getIn(['meta', 'me']), 'locked']),
}); });
const WarningWrapper = ({ needsLockWarning }) => { const WarningWrapper = ({ needsLockWarning }) => {

View File

@@ -5,5 +5,5 @@ const urlPlaceholder = 'xxxxxxxxxxxxxxxxxxxxxxx';
export function countableText(inputText) { export function countableText(inputText) {
return inputText return inputText
.replace(urlRegex, urlPlaceholder) .replace(urlRegex, urlPlaceholder)
.replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3'); .replace(/(?:^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '@$2');
}; };

View File

@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import ColumnSettings from '../../community_timeline/components/column_settings';
import { changeSetting } from '../../../actions/settings';
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'direct']),
});
const mapDispatchToProps = dispatch => ({
onChange (key, checked) {
dispatch(changeSetting(['direct', ...key], checked));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings);

View File

@@ -1,107 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import {
refreshDirectTimeline,
expandDirectTimeline,
} from '../../actions/timelines';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectDirectStream } from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
});
const mapStateToProps = state => ({
hasUnread: state.getIn(['timelines', 'direct', 'unread']) > 0,
});
@connect(mapStateToProps)
@injectIntl
export default class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('DIRECT', {}));
}
}
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
handleHeaderClick = () => {
this.column.scrollTop();
}
componentDidMount () {
const { dispatch } = this.props;
dispatch(refreshDirectTimeline());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
this.props.dispatch(expandDirectTimeline());
}
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='envelope'
active={hasUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
>
<ColumnSettingsContainer />
</ColumnHeader>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`}
timelineId='direct'
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
/>
</Column>
);
}
}

View File

@@ -57,21 +57,5 @@ describe('emoji', () => {
it('does an emoji whose filename is irregular', () => { it('does an emoji whose filename is irregular', () => {
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />'); expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
}); });
it('avoid emojifying on invisible text', () => {
expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
.toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
.toEqual('<span class="invisible">:luigi:</span>');
});
it('avoid emojifying on invisible text with nested tags', () => {
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
});
}); });
}); });

View File

@@ -1,4 +1,3 @@
import { autoPlayGif } from '../../initial_state';
import unicodeMapping from './emoji_unicode_mapping_light'; import unicodeMapping from './emoji_unicode_mapping_light';
import Trie from 'substring-trie'; import Trie from 'substring-trie';
@@ -6,13 +5,13 @@ const trie = new Trie(Object.keys(unicodeMapping));
const assetHost = process.env.CDN_HOST || ''; const assetHost = process.env.CDN_HOST || '';
let allowAnimations = false;
const emojify = (str, customEmojis = {}) => { const emojify = (str, customEmojis = {}) => {
const tagCharsWithoutEmojis = '<&'; let rtn = '';
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
for (;;) { for (;;) {
let match, i = 0, tag; let match, i = 0, tag;
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
i += str.codePointAt(i) < 65536 ? 1 : 2; i += str.codePointAt(i) < 65536 ? 1 : 2;
} }
let rend, replacement = ''; let rend, replacement = '';
@@ -28,7 +27,7 @@ const emojify = (str, customEmojis = {}) => {
// now got a replacee as ':shortname:' // now got a replacee as ':shortname:'
// if you want additional emoji handler, add statements below which set replacement and return true. // if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) { if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`; replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
return true; return true;
} }
@@ -36,26 +35,7 @@ const emojify = (str, customEmojis = {}) => {
})()) rend = ++i; })()) rend = ++i;
} else if (tag >= 0) { // <, & } else if (tag >= 0) { // <, &
rend = str.indexOf('>;'[tag], i + 1) + 1; rend = str.indexOf('>;'[tag], i + 1) + 1;
if (!rend) { if (!rend) break;
break;
}
if (tag === 0) {
if (invisible) {
if (str[i + 1] === '/') { // closing tag
if (!--invisible) {
tagChars = tagCharsWithEmojis;
}
} else if (str[rend - 2] !== '/') { // opening tag
invisible++;
}
} else {
if (str.startsWith('<span class="invisible">', i)) {
// avoid emojifying on invisible text
invisible = 1;
tagChars = tagCharsWithoutEmojis;
}
}
}
i = rend; i = rend;
} else { // matched to unicode emoji } else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match]; const { filename, shortCode } = unicodeMapping[match];
@@ -71,12 +51,14 @@ const emojify = (str, customEmojis = {}) => {
export default emojify; export default emojify;
export const buildCustomEmojis = (customEmojis) => { export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => {
const emojis = []; const emojis = [];
allowAnimations = overrideAllowAnimations;
customEmojis.forEach(emoji => { customEmojis.forEach(emoji => {
const shortcode = emoji.get('shortcode'); const shortcode = emoji.get('shortcode');
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url'); const url = allowAnimations ? emoji.get('url') : emoji.get('static_url');
const name = shortcode.replace(':', ''); const name = shortcode.replace(':', '');
emojis.push({ emojis.push({

View File

@@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => {
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach(key => {
const { native } = emojiIndex.emojis[key]; const { native } = emojiIndex.emojis[key];
let { short_names, search, unified } = emojiMartData.emojis[key]; const { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) { if (short_names[0] !== key) {
throw new Error('The compresser expects the first short_code to be the ' + throw new Error('The compresser expects the first short_code to be the ' +
'key. It may need to be rewritten if the emoji change such that this ' + 'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.'); 'is no longer the case.');
} }
short_names = short_names.slice(1); // first short name can be inferred from the key short_names.splice(0, 1); // first short name can be inferred from the key
const searchData = [native, short_names, search]; const searchData = [native, short_names, search];
if (unicodeToUnifiedName(native) !== unified) { if (unicodeToUnifiedName(native) !== unified) {

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { fetchFavourites } from '../../actions/interactions'; import { fetchFavourites } from '../../actions/interactions';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountAuthorizeContainer from './containers/account_authorize_container'; import AccountAuthorizeContainer from './containers/account_authorize_container';

View File

@@ -8,7 +8,7 @@ import {
fetchFollowers, fetchFollowers,
expandFollowers, expandFollowers,
} from '../../actions/accounts'; } from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';

View File

@@ -8,7 +8,7 @@ import {
fetchFollowing, fetchFollowing,
expandFollowing, expandFollowing,
} from '../../actions/accounts'; } from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import HeaderContainer from '../account_timeline/containers/header_container'; import HeaderContainer from '../account_timeline/containers/header_container';

View File

@@ -8,7 +8,6 @@ import { openModal } from '../../actions/modal';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -18,7 +17,6 @@ const messages = defineMessages({
navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' }, navigation_subheading: { id: 'column_subheading.navigation', defaultMessage: 'Navigation' },
settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' }, settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
@@ -32,7 +30,7 @@ const messages = defineMessages({
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]), me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
columns: state.getIn(['settings', 'columns']), columns: state.getIn(['settings', 'columns']),
}); });
@@ -42,7 +40,7 @@ export default class GettingStarted extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map.isRequired, me: ImmutablePropTypes.map.isRequired,
columns: ImmutablePropTypes.list, columns: ImmutablePropTypes.list,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
@@ -58,7 +56,7 @@ export default class GettingStarted extends ImmutablePureComponent {
} }
render () { render () {
const { intl, myAccount, columns, multiColumn } = this.props; const { intl, me, columns, multiColumn } = this.props;
let navItems = []; let navItems = [];
@@ -80,22 +78,18 @@ export default class GettingStarted extends ImmutablePureComponent {
} }
} }
if (!multiColumn || !columns.find(item => item.get('id') === 'DIRECT')) {
navItems.push(<ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />);
}
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]); ]);
if (myAccount.get('locked')) { if (me.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
} }
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]); ]);
return ( return (

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim'; import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';

View File

@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator'; import LoadingIndicator from '../../components/loading_indicator';
import { fetchReblogs } from '../../actions/interactions'; import { fetchReblogs } from '../../actions/interactions';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; import AccountContainer from '../../containers/account_container';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';

View File

@@ -4,7 +4,6 @@ import IconButton from '../../../components/icon_button';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { me } from '../../../initial_state';
const messages = defineMessages({ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' }, delete: { id: 'status.delete', defaultMessage: 'Delete' },
@@ -37,6 +36,7 @@ export default class ActionBar extends React.PureComponent {
onReport: PropTypes.func, onReport: PropTypes.func,
onPin: PropTypes.func, onPin: PropTypes.func,
onEmbed: PropTypes.func, onEmbed: PropTypes.func,
me: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent {
} }
render () { render () {
const { status, intl } = this.props; const { status, me, intl } = this.props;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));

View File

@@ -25,6 +25,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
onOpenVideo: PropTypes.func.isRequired, onOpenVideo: PropTypes.func.isRequired,
autoPlayGif: PropTypes.bool,
}; };
handleAccountClick = (e) => { handleAccountClick = (e) => {
@@ -75,6 +76,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
fullwidth={settings.getIn(['media', 'fullwidth'])} fullwidth={settings.getIn(['media', 'fullwidth'])}
height={250} height={250}
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
autoPlayGif={this.props.autoPlayGif}
/> />
); );
mediaIcon = 'picture-o'; mediaIcon = 'picture-o';

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchStatus } from '../../actions/statuses'; import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator'; import MissingIndicator from '../../components/missing_indicator';
@@ -23,15 +22,13 @@ import {
import { deleteStatus } from '../../actions/statuses'; import { deleteStatus } from '../../actions/statuses';
import { initReport } from '../../actions/reports'; import { initReport } from '../../actions/reports';
import { makeGetStatus } from '../../selectors'; import { makeGetStatus } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4'; import { ScrollContainer } from 'react-router-scroll';
import ColumnBackButton from '../../components/column_back_button'; import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../../glitch/components/status/container'; import StatusContainer from '../../../glitch/components/status/container';
import { openModal } from '../../actions/modal'; import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
const messages = defineMessages({ const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
@@ -46,6 +43,10 @@ const makeMapStateToProps = () => {
settings: state.get('local_settings'), settings: state.get('local_settings'),
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]), ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]), descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
me: state.getIn(['meta', 'me']),
boostModal: state.getIn(['meta', 'boost_modal']),
deleteModal: state.getIn(['meta', 'delete_modal']),
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
}); });
return mapStateToProps; return mapStateToProps;
@@ -66,21 +67,17 @@ export default class Status extends ImmutablePureComponent {
settings: ImmutablePropTypes.map.isRequired, settings: ImmutablePropTypes.map.isRequired,
ancestorsIds: ImmutablePropTypes.list, ancestorsIds: ImmutablePropTypes.list,
descendantsIds: ImmutablePropTypes.list, descendantsIds: ImmutablePropTypes.list,
me: PropTypes.string,
boostModal: PropTypes.bool,
deleteModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
state = {
fullscreen: false,
};
componentWillMount () { componentWillMount () {
this.props.dispatch(fetchStatus(this.props.params.statusId)); this.props.dispatch(fetchStatus(this.props.params.statusId));
} }
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
}
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
this._scrolledIntoView = false; this._scrolledIntoView = false;
@@ -116,7 +113,7 @@ export default class Status extends ImmutablePureComponent {
if (status.get('reblogged')) { if (status.get('reblogged')) {
this.props.dispatch(unreblog(status)); this.props.dispatch(unreblog(status));
} else { } else {
if (e.shiftKey || !boostModal) { if (e.shiftKey || !this.props.boostModal) {
this.handleModalReblog(status); this.handleModalReblog(status);
} else { } else {
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog })); this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
@@ -127,7 +124,7 @@ export default class Status extends ImmutablePureComponent {
handleDeleteClick = (status) => { handleDeleteClick = (status) => {
const { dispatch, intl } = this.props; const { dispatch, intl } = this.props;
if (!deleteModal) { if (!this.props.deleteModal) {
dispatch(deleteStatus(status.get('id'))); dispatch(deleteStatus(status.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
@@ -262,18 +259,9 @@ export default class Status extends ImmutablePureComponent {
} }
} }
componentWillUnmount () {
detachFullscreenListener(this.onFullScreenChange);
}
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { status, settings, ancestorsIds, descendantsIds } = this.props; const { status, settings, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
const { fullscreen } = this.state;
if (status === null) { if (status === null) {
return ( return (
@@ -307,7 +295,7 @@ export default class Status extends ImmutablePureComponent {
<ColumnBackButton /> <ColumnBackButton />
<ScrollContainer scrollKey='thread'> <ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}> <div className='scrollable detailed-status__wrapper' ref={this.setRef}>
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
@@ -315,12 +303,15 @@ export default class Status extends ImmutablePureComponent {
<DetailedStatus <DetailedStatus
status={status} status={status}
settings={settings} settings={settings}
autoPlayGif={autoPlayGif}
me={me}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
/> />
<ActionBar <ActionBar
status={status} status={status}
me={me}
onReply={this.handleReplyClick} onReply={this.handleReplyClick}
onFavourite={this.handleFavouriteClick} onFavourite={this.handleFavouriteClick}
onReblog={this.handleReblogClick} onReblog={this.handleReblogClick}

View File

@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading'; import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading'; import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
@@ -23,7 +23,6 @@ const componentMap = {
'PUBLIC': PublicTimeline, 'PUBLIC': PublicTimeline,
'COMMUNITY': CommunityTimeline, 'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline,
'FAVOURITES': FavouritedStatuses, 'FAVOURITES': FavouritedStatuses,
}; };

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl, FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import Button from '../../../components/button'; import Button from '../../../components/button';
import { closeModal } from '../../../actions/modal'; import { closeModal } from '../../../actions/modal';
import { muteAccount } from '../../../actions/accounts'; import { muteAccount } from '../../../actions/accounts';
@@ -81,13 +80,12 @@ export default class MuteModal extends React.PureComponent {
values={{ name: <strong>@{account.get('acct')}</strong> }} values={{ name: <strong>@{account.get('acct')}</strong> }}
/> />
</p> </p>
<div> <p>
<label htmlFor='mute-modal__hide-notifications-checkbox'> <label htmlFor='mute-modal__hide-notifications-checkbox'>
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' /> <FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
{' '} <input id='mute-modal__hide-notifications-checkbox' type='checkbox' checked={notifications} onChange={this.toggleNotifications} />
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
</label> </label>
</div> </p>
</div> </div>
<div className='mute-modal__action-bar'> <div className='mute-modal__action-bar'>

View File

@@ -14,7 +14,6 @@ import {
List as ImmutableList, List as ImmutableList,
Map as ImmutableMap, Map as ImmutableMap,
} from 'immutable'; } from 'immutable';
import { me } from '../../../initial_state';
const noop = () => { }; const noop = () => { };
@@ -44,11 +43,11 @@ PageOne.propTypes = {
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
}; };
const PageTwo = ({ myAccount }) => ( const PageTwo = ({ me }) => (
<div className='onboarding-modal__page onboarding-modal__page-two'> <div className='onboarding-modal__page onboarding-modal__page-two'>
<div className='figure non-interactive'> <div className='figure non-interactive'>
<div className='pseudo-drawer'> <div className='pseudo-drawer'>
<NavigationBar onClose={noop} account={myAccount} /> <NavigationBar onClose={noop} account={me} />
</div> </div>
<ComposeForm <ComposeForm
text='Awoo! #introductions' text='Awoo! #introductions'
@@ -74,10 +73,10 @@ const PageTwo = ({ myAccount }) => (
); );
PageTwo.propTypes = { PageTwo.propTypes = {
myAccount: ImmutablePropTypes.map.isRequired, me: ImmutablePropTypes.map.isRequired,
}; };
const PageThree = ({ myAccount }) => ( const PageThree = ({ me }) => (
<div className='onboarding-modal__page onboarding-modal__page-three'> <div className='onboarding-modal__page onboarding-modal__page-three'>
<div className='figure non-interactive'> <div className='figure non-interactive'>
<Search <Search
@@ -89,7 +88,7 @@ const PageThree = ({ myAccount }) => (
/> />
<div className='pseudo-drawer'> <div className='pseudo-drawer'>
<NavigationBar onClose={noop} account={myAccount} /> <NavigationBar onClose={noop} account={me} />
</div> </div>
</div> </div>
@@ -99,7 +98,7 @@ const PageThree = ({ myAccount }) => (
); );
PageThree.propTypes = { PageThree.propTypes = {
myAccount: ImmutablePropTypes.map.isRequired, me: ImmutablePropTypes.map.isRequired,
}; };
const PageFour = ({ domain, intl }) => ( const PageFour = ({ domain, intl }) => (
@@ -167,7 +166,7 @@ PageSix.propTypes = {
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
myAccount: state.getIn(['accounts', me]), me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]), admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
}); });
@@ -179,7 +178,7 @@ export default class OnboardingModal extends React.PureComponent {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
myAccount: ImmutablePropTypes.map.isRequired, me: ImmutablePropTypes.map.isRequired,
domain: PropTypes.string.isRequired, domain: PropTypes.string.isRequired,
admin: ImmutablePropTypes.map, admin: ImmutablePropTypes.map,
}; };
@@ -189,11 +188,11 @@ export default class OnboardingModal extends React.PureComponent {
}; };
componentWillMount() { componentWillMount() {
const { myAccount, admin, domain, intl } = this.props; const { me, admin, domain, intl } = this.props;
this.pages = [ this.pages = [
<PageOne acct={myAccount.get('acct')} domain={domain} />, <PageOne acct={me.get('acct')} domain={domain} />,
<PageTwo myAccount={myAccount} />, <PageTwo me={me} />,
<PageThree myAccount={myAccount} />, <PageThree me={me} />,
<PageFour domain={domain} intl={intl} />, <PageFour domain={domain} intl={intl} />,
<PageSix admin={admin} domain={domain} />, <PageSix admin={admin} domain={domain} />,
]; ];

View File

@@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { me } from '../../../initial_state';
const makeGetStatusIds = () => createSelector([ const makeGetStatusIds = () => createSelector([
(state, { type }) => state.getIn(['settings', type], ImmutableMap()), (state, { type }) => state.getIn(['settings', type], ImmutableMap()),
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
(state) => state.get('statuses'), (state) => state.get('statuses'),
], (columnSettings, statusIds, statuses) => { (state) => state.getIn(['meta', 'me']),
], (columnSettings, statusIds, statuses, me) => {
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim(); const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
let regex = null; let regex = null;

View File

@@ -29,7 +29,6 @@ import {
Following, Following,
Reblogs, Reblogs,
Favourites, Favourites,
DirectTimeline,
HashtagTimeline, HashtagTimeline,
Notifications, Notifications,
FollowRequests, FollowRequests,
@@ -40,20 +39,18 @@ import {
PinnedStatuses, PinnedStatuses,
} from './util/async-components'; } from './util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { me } from '../../initial_state';
import { defineMessages, injectIntl } from 'react-intl';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles. // Without this it ends up in ~8 very commonly used bundles.
import '../../../glitch/components/status'; import '../../../glitch/components/status';
const messages = defineMessages({
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
});
const mapStateToProps = state => ({ const mapStateToProps = state => ({
systemFontUi: state.getIn(['meta', 'system_font_ui']),
layout: state.getIn(['local_settings', 'layout']),
isWide: state.getIn(['local_settings', 'stretch']),
navbarUnder: state.getIn(['local_settings', 'navbar_under']),
me: state.getIn(['meta', 'me']),
isComposing: state.getIn(['compose', 'is_composing']), isComposing: state.getIn(['compose', 'is_composing']),
hasComposingText: state.getIn(['compose', 'text']) !== '',
}); });
const keyMap = { const keyMap = {
@@ -74,7 +71,6 @@ const keyMap = {
goToNotifications: 'g n', goToNotifications: 'g n',
goToLocal: 'g l', goToLocal: 'g l',
goToFederated: 'g t', goToFederated: 'g t',
goToDirect: 'g d',
goToStart: 'g s', goToStart: 'g s',
goToFavourites: 'g f', goToFavourites: 'g f',
goToPinned: 'g p', goToPinned: 'g p',
@@ -84,7 +80,6 @@ const keyMap = {
}; };
@connect(mapStateToProps) @connect(mapStateToProps)
@injectIntl
@withRouter @withRouter
export default class UI extends React.Component { export default class UI extends React.Component {
@@ -100,9 +95,8 @@ export default class UI extends React.Component {
systemFontUi: PropTypes.bool, systemFontUi: PropTypes.bool,
navbarUnder: PropTypes.bool, navbarUnder: PropTypes.bool,
isComposing: PropTypes.bool, isComposing: PropTypes.bool,
hasComposingText: PropTypes.bool, me: PropTypes.string,
location: PropTypes.object, location: PropTypes.object,
intl: PropTypes.object.isRequired,
}; };
state = { state = {
@@ -110,17 +104,6 @@ export default class UI extends React.Component {
draggingOver: false, draggingOver: false,
}; };
handleBeforeUnload = (e) => {
const { intl, isComposing, hasComposingText } = this.props;
if (isComposing && hasComposingText) {
// Setting returnValue to any string causes confirmation dialog.
// Many browsers no longer display this text to users,
// but we set user-friendly message for other browsers, e.g. Edge.
e.returnValue = intl.formatMessage(messages.beforeUnload);
}
}
handleResize = debounce(() => { handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate // The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight()); this.props.dispatch(clearHeight());
@@ -195,7 +178,6 @@ export default class UI extends React.Component {
} }
componentWillMount () { componentWillMount () {
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
window.addEventListener('resize', this.handleResize, { passive: true }); window.addEventListener('resize', this.handleResize, { passive: true });
document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragenter', this.handleDragEnter, false);
document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('dragover', this.handleDragOver, false);
@@ -238,7 +220,6 @@ export default class UI extends React.Component {
} }
componentWillUnmount () { componentWillUnmount () {
window.removeEventListener('beforeunload', this.handleBeforeUnload);
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragenter', this.handleDragEnter);
document.removeEventListener('dragover', this.handleDragOver); document.removeEventListener('dragover', this.handleDragOver);
@@ -321,10 +302,6 @@ export default class UI extends React.Component {
this.context.router.history.push('/timelines/public'); this.context.router.history.push('/timelines/public');
} }
handleHotkeyGoToDirect = () => {
this.context.router.history.push('/timelines/direct');
}
handleHotkeyGoToStart = () => { handleHotkeyGoToStart = () => {
this.context.router.history.push('/getting-started'); this.context.router.history.push('/getting-started');
} }
@@ -338,7 +315,7 @@ export default class UI extends React.Component {
} }
handleHotkeyGoToProfile = () => { handleHotkeyGoToProfile = () => {
this.context.router.history.push(`/accounts/${me}`); this.context.router.history.push(`/accounts/${this.props.me}`);
} }
handleHotkeyGoToBlocked = () => { handleHotkeyGoToBlocked = () => {
@@ -380,7 +357,6 @@ export default class UI extends React.Component {
goToNotifications: this.handleHotkeyGoToNotifications, goToNotifications: this.handleHotkeyGoToNotifications,
goToLocal: this.handleHotkeyGoToLocal, goToLocal: this.handleHotkeyGoToLocal,
goToFederated: this.handleHotkeyGoToFederated, goToFederated: this.handleHotkeyGoToFederated,
goToDirect: this.handleHotkeyGoToDirect,
goToStart: this.handleHotkeyGoToStart, goToStart: this.handleHotkeyGoToStart,
goToFavourites: this.handleHotkeyGoToFavourites, goToFavourites: this.handleHotkeyGoToFavourites,
goToPinned: this.handleHotkeyGoToPinned, goToPinned: this.handleHotkeyGoToPinned,
@@ -401,7 +377,6 @@ export default class UI extends React.Component {
<WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} /> <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/direct' component={DirectTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} />

View File

@@ -26,10 +26,6 @@ export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
} }
export function DirectTimeline() {
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
}
export function Status () { export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status'); return import(/* webpackChunkName: "features/status" */'../../status');
} }

View File

@@ -1,5 +1,56 @@
import { reduceMotion } from '../../../initial_state'; // Like react-motion's Motion, but checks to see if the user prefers
import ReducedMotion from './reduced_motion'; // reduced motion and uses a cross-fade in those cases.
import Motion from 'react-motion/lib/Motion';
export default reduceMotion ? ReducedMotion : Motion; import React from 'react';
import Motion from 'react-motion/lib/Motion';
import PropTypes from 'prop-types';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
let reduceMotion;
const extractValue = (value) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
class OptionalMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
render() {
const { style, defaultStyle, children } = this.props;
if (typeof reduceMotion !== 'boolean') {
// This never changes without a page reload, so we can just grab it
// once from the body classes as opposed to using Redux's connect(),
// which would unnecessarily update every state change
reduceMotion = document.body.classList.contains('reduce-motion');
}
if (reduceMotion) {
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
}
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default OptionalMotion;

View File

@@ -7,19 +7,11 @@ import BundleColumnError from '../components/bundle_column_error';
import BundleContainer from '../containers/bundle_container'; import BundleContainer from '../containers/bundle_container';
// Small wrapper to pass multiColumn to the route components // Small wrapper to pass multiColumn to the route components
export class WrappedSwitch extends React.PureComponent { export const WrappedSwitch = ({ multiColumn, children }) => (
render () {
const { multiColumn, children } = this.props;
return (
<Switch> <Switch>
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
</Switch> </Switch>
); );
}
}
WrappedSwitch.propTypes = { WrappedSwitch.propTypes = {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,

View File

@@ -1,44 +0,0 @@
// Like react-motion's Motion, but reduces all animations to cross-fades
// for the benefit of users with motion sickness.
import React from 'react';
import Motion from 'react-motion/lib/Motion';
import PropTypes from 'prop-types';
const stylesToKeep = ['opacity', 'backgroundOpacity'];
const extractValue = (value) => {
// This is either an object with a "val" property or it's a number
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
};
class ReducedMotion extends React.Component {
static propTypes = {
defaultStyle: PropTypes.object,
style: PropTypes.object,
children: PropTypes.func,
}
render() {
const { style, defaultStyle, children } = this.props;
Object.keys(style).forEach(key => {
if (stylesToKeep.includes(key)) {
return;
}
// If it's setting an x or height or scale or some other value, we need
// to preserve the end-state value without actually animating it
style[key] = defaultStyle[key] = extractValue(style[key]);
});
return (
<Motion style={style} defaultStyle={defaultStyle}>
{children}
</Motion>
);
}
}
export default ReducedMotion;

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