Merge branch 'master' into kosmos

This commit is contained in:
Basti 2017-12-20 10:15:42 +00:00
commit 6d1b204b6a
543 changed files with 15853 additions and 4630 deletions

View File

@ -1,21 +1,36 @@
engines: version: "2"
checks:
argument-count:
enabled: false
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
enabled: false
nested-control-flow:
enabled: false
return-statements:
enabled: false
similar-code:
enabled: false
identical-code:
enabled: false
plugins:
brakeman: brakeman:
enabled: true enabled: true
bundler-audit: bundler-audit:
enabled: true enabled: true
duplication:
enabled: false
eslint: eslint:
enabled: true enabled: true
rubocop: rubocop:
enabled: true enabled: true
scss-lint: scss-lint:
enabled: true enabled: true
ratings: exclude_patterns:
paths:
- "**.rb"
- "**.js"
- "**.scss"
exclude_paths:
- spec/ - spec/
- vendor/asset - vendor/asset

View File

@ -35,6 +35,17 @@ 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
@ -62,7 +73,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
@ -91,6 +102,23 @@ 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=

View File

View File

@ -27,11 +27,14 @@ addons:
apt: apt:
sources: sources:
- trusty-media - trusty-media
- sourceline: deb https://dl.yarnpkg.com/debian/ stable main
key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
packages: packages:
- ffmpeg - ffmpeg
- libicu-dev
- libprotobuf-dev - libprotobuf-dev
- protobuf-compiler - protobuf-compiler
- libicu-dev - yarn
rvm: rvm:
- 2.3.4 - 2.3.4
@ -42,7 +45,6 @@ services:
install: install:
- nvm install - nvm install
- npm install -g yarn
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16 - bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- yarn install - yarn install

46
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,46 @@
# 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/

View File

@ -7,8 +7,8 @@ ENV UID=991 GID=991 \
RAILS_SERVE_STATIC_FILES=true \ RAILS_SERVE_STATIC_FILES=true \
RAILS_ENV=production NODE_ENV=production RAILS_ENV=production NODE_ENV=production
ARG YARN_VERSION=1.1.0 ARG YARN_VERSION=1.3.2
ARG YARN_DOWNLOAD_SHA256=171c1f9ee93c488c0d774ac6e9c72649047c3f896277d88d0f805266519430f3 ARG YARN_DOWNLOAD_SHA256=6cfe82e530ef0837212f13e45c1565ba53f5199eec2527b85ecbcd88bf26821d
ARG LIBICONV_VERSION=1.15 ARG LIBICONV_VERSION=1.15
ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178 ARG LIBICONV_DOWNLOAD_SHA256=ccf536620a45458d26ba83887a983b96827001e92a13847b45e4925cc8913178
@ -48,7 +48,7 @@ RUN apk -U upgrade \
&& rm yarn.tar.gz \ && rm yarn.tar.gz \
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \ && mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \ && ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \ && wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \ && echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
&& tar -xzf libiconv.tar.gz -C /tmp/src \ && tar -xzf libiconv.tar.gz -C /tmp/src \
&& rm libiconv.tar.gz \ && rm libiconv.tar.gz \

33
Gemfile
View File

@ -14,8 +14,10 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7' gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2' gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk', '~> 2.9' gem 'aws-sdk', '~> 2.10', require: false
gem 'fog-openstack', '~> 0.1' gem 'fog-core', '~> 1.45'
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'
@ -38,16 +40,15 @@ 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.0' gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1' gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.7' gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2' gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.0' gem 'oj', '~> 3.3'
gem 'ostatus2', '~> 2.0' gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.5' gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1' gem 'pundit', '~> 1.1'
gem 'rabl', '~> 0.13'
gem 'rack-attack', '~> 5.0' gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors' gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4' gem 'rack-timeout', '~> 0.4'
@ -75,15 +76,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.16' gem 'fabrication', '~> 2.18'
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.6' gem 'rspec-rails', '~> 3.7'
end end
group :test do group :test do
gem 'capybara', '~> 2.14' gem 'capybara', '~> 2.15'
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'
@ -91,13 +92,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.14' gem 'parallel_tests', '~> 2.17'
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.1' gem 'better_errors', '~> 2.4'
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'
@ -105,15 +106,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.53', require: false gem 'scss_lint', '~> 0.55', require: false
gem 'capistrano', '~> 3.8' gem 'capistrano', '~> 3.10'
gem 'capistrano-rails', '~> 1.2' gem 'capistrano-rails', '~> 1.3'
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.5' gem 'lograge', '~> 0.7'
gem 'redis-rails', '~> 5.0' gem 'redis-rails', '~> 5.0'
end end

View File

@ -24,11 +24,11 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.6) active_model_serializers (0.10.7)
actionpack (>= 4.1, < 6) actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6) activemodel (>= 4.1, < 6)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_record_query_trace (1.5.4) active_record_query_trace (1.5.4)
activejob (5.1.4) activejob (5.1.4)
activesupport (= 5.1.4) activesupport (= 5.1.4)
@ -57,25 +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 (2.10.100)
aws-sdk-resources (= 2.10.46) aws-sdk-resources (= 2.10.100)
aws-sdk-core (2.10.46) aws-sdk-core (2.10.100)
aws-sigv4 (~> 1.0) aws-sigv4 (~> 1.0)
jmespath (~> 1.0) jmespath (~> 1.0)
aws-sdk-resources (2.10.46) aws-sdk-resources (2.10.100)
aws-sdk-core (= 2.10.46) aws-sdk-core (= 2.10.100)
aws-sigv4 (1.0.2) aws-sigv4 (1.0.2)
bcrypt (3.1.11) bcrypt (3.1.11)
better_errors (2.3.0) better_errors (2.4.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.2) binding_of_caller (0.7.3)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootsnap (1.1.3) bootsnap (1.1.5)
msgpack (~> 1.0) msgpack (~> 1.0)
brakeman (4.0.1) brakeman (4.0.1)
browser (2.5.1) browser (2.5.2)
builder (3.2.3) builder (3.2.3)
bullet (5.6.1) bullet (5.6.1)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@ -83,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.9.1) capistrano (3.10.0)
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.2.0) capistrano-bundler (1.3.0)
capistrano (~> 3.1) capistrano (~> 3.1)
sshkit (~> 1.2) sshkit (~> 1.2)
capistrano-rails (1.3.0) capistrano-rails (1.3.1)
capistrano (~> 3.1) capistrano (~> 3.1)
capistrano-bundler (~> 1.1) capistrano-bundler (~> 1.1)
capistrano-rbenv (2.1.1) capistrano-rbenv (2.1.3)
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.1) capybara (2.16.1)
addressable addressable
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3) nokogiri (>= 1.3.3)
@ -110,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.0) cld3 (3.2.1)
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)
@ -121,7 +121,7 @@ GEM
connection_pool (2.2.1) connection_pool (2.2.1)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
crass (1.0.2) crass (1.0.3)
debug_inspector (0.0.3) debug_inspector (0.0.3)
devise (4.3.0) devise (4.3.0)
bcrypt (~> 3.0) bcrypt (~> 3.0)
@ -129,11 +129,11 @@ GEM
railties (>= 4.1.0, < 5.2) railties (>= 4.1.0, < 5.2)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (3.0.0) devise-two-factor (3.0.2)
activesupport activesupport (< 5.2)
attr_encrypted (>= 1.3, < 4, != 2) attr_encrypted (>= 1.3, < 4, != 2)
devise (~> 4.0) devise (~> 4.0)
railties railties (< 5.2)
rotp (~> 2.0) rotp (~> 2.0)
diff-lcs (1.3) diff-lcs (1.3)
docile (1.1.5) docile (1.1.5)
@ -150,12 +150,12 @@ GEM
thread thread
thread_safe thread_safe
encryptor (3.0.0) encryptor (3.0.0)
erubi (1.6.1) erubi (1.7.0)
et-orbi (1.0.5) et-orbi (1.0.8)
tzinfo tzinfo
excon (0.59.0) excon (0.59.0)
execjs (2.7.0) execjs (2.7.0)
fabrication (2.16.3) fabrication (2.18.0)
faker (1.8.4) faker (1.8.4)
i18n (~> 0.5) i18n (~> 0.5)
fast_blank (1.0.0) fast_blank (1.0.0)
@ -167,7 +167,9 @@ 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-openstack (0.1.21) fog-local (0.4.0)
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)
@ -175,14 +177,14 @@ GEM
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.0) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
goldfinger (2.0.1) goldfinger (2.0.1)
addressable (~> 2.5) addressable (~> 2.5)
http (~> 2.2) http (~> 2.2)
nokogiri (~> 1.8) nokogiri (~> 1.8)
oj (~> 3.0) oj (~> 3.0)
hamlit (2.8.4) hamlit (2.8.5)
temple (>= 0.8.0) temple (>= 0.8.0)
thor thor
tilt tilt
@ -194,7 +196,7 @@ GEM
hamster (3.0.0) hamster (3.0.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
hashdiff (0.3.7) hashdiff (0.3.7)
highline (1.7.8) highline (1.7.10)
hiredis (0.6.1) hiredis (0.6.1)
hkdf (0.3.0) hkdf (0.3.0)
htmlentities (4.3.4) htmlentities (4.3.4)
@ -211,8 +213,9 @@ GEM
httplog (0.99.7) httplog (0.99.7)
colorize colorize
rack rack
i18n (0.8.6) i18n (0.9.1)
i18n-tasks (0.9.18) concurrent-ruby (~> 1.0)
i18n-tasks (0.9.19)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)
ast (>= 2.1.0) ast (>= 2.1.0)
easy_translate (>= 0.5.0) easy_translate (>= 0.5.0)
@ -227,27 +230,27 @@ GEM
iso-639 (0.2.8) iso-639 (0.2.8)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5) json-ld (2.1.7)
multi_json (~> 1.12) multi_json (~> 1.12)
rdf (~> 2.2) rdf (~> 2.2, >= 2.2.8)
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.2.0)
jwt (1.5.6) jwt (2.1.0)
kaminari (1.0.1) kaminari (1.1.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.0.1) kaminari-actionview (= 1.1.1)
kaminari-activerecord (= 1.0.1) kaminari-activerecord (= 1.1.1)
kaminari-core (= 1.0.1) kaminari-core (= 1.1.1)
kaminari-actionview (1.0.1) kaminari-actionview (1.1.1)
actionview actionview
kaminari-core (= 1.0.1) kaminari-core (= 1.1.1)
kaminari-activerecord (1.0.1) kaminari-activerecord (1.1.1)
activerecord activerecord
kaminari-core (= 1.0.1) kaminari-core (= 1.1.1)
kaminari-core (1.0.1) kaminari-core (1.1.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)
@ -257,18 +260,19 @@ 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.6.0) lograge (0.7.1)
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.0.3) loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.6) mail (2.7.0)
mime-types (>= 1.16, < 4) mini_mime (>= 0.1.1)
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.8.2) method_source (0.9.0)
microformats (4.0.7) microformats (4.0.7)
json json
nokogiri nokogiri
@ -276,8 +280,8 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
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 (1.0.0)
mini_portile2 (2.2.0) mini_portile2 (2.3.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)
@ -285,8 +289,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.0) nokogiri (1.8.1)
mini_portile2 (~> 2.2.0) mini_portile2 (~> 2.3.0)
nokogumbo (1.4.13) nokogumbo (1.4.13)
nokogiri nokogiri
nsa (0.2.4) nsa (0.2.4)
@ -294,15 +298,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.5) oj (3.3.9)
openssl (2.0.5) openssl (2.0.6)
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.6.0) ox (2.8.2)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
@ -313,27 +317,24 @@ 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.15.0) parallel_tests (2.19.0)
parallel parallel
parser (2.4.0.0) parser (2.4.0.2)
ast (~> 2.2) ast (~> 2.3)
pg (0.21.0) pg (0.21.0)
pghero (1.7.0) pghero (1.7.0)
activerecord activerecord
pkg-config (1.2.7) pkg-config (1.2.8)
powerpack (0.1.1) powerpack (0.1.1)
pry (0.10.4) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.9.0)
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.1)
puma (3.10.0) puma (3.11.0)
pundit (1.1.0) pundit (1.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
rabl (0.13.1)
activesupport (>= 2.3.14)
rack (2.0.3) rack (2.0.3)
rack-attack (5.0.1) rack-attack (5.0.1)
rack rack
@ -342,7 +343,7 @@ GEM
rack rack
rack-proxy (0.6.2) rack-proxy (0.6.2)
rack rack
rack-test (0.7.0) rack-test (0.8.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-timeout (0.4.2) rack-timeout (0.4.2)
rails (5.1.4) rails (5.1.4)
@ -379,31 +380,34 @@ 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.1.0) rake (12.3.0)
rdf (2.2.9) rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
rdf (2.2.12)
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.3) redis (3.3.5)
redis-actionpack (5.0.1) redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6) actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3) redis-rack (>= 1, < 3)
redis-store (>= 1.1.0, < 1.4.0) redis-store (>= 1.1.0, < 2)
redis-activesupport (5.0.3) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (~> 1.3.0) redis-store (>= 1.3, < 2)
redis-namespace (1.5.3) redis-namespace (1.6.0)
redis (~> 3.0, >= 3.0.4) redis (>= 3.0.4)
redis-rack (2.0.2) redis-rack (2.0.3)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 1.4) redis-store (>= 1.2, < 2)
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.3.0) redis-store (1.4.1)
redis (>= 2.2) redis (>= 2.2, < 5)
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)
@ -411,27 +415,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.6.0) rspec-core (3.7.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-expectations (3.6.0) rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-mocks (3.6.0) rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-rails (3.6.1) rspec-rails (3.7.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.6.0) rspec-core (~> 3.7.0)
rspec-expectations (~> 3.6.0) rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.6.0) rspec-mocks (~> 3.7.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.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.6.0) rspec-support (3.7.0)
rubocop (0.50.0) rubocop (0.51.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)
@ -439,7 +443,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.8.3) ruby-progressbar (1.9.0)
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)
@ -447,20 +451,24 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
nokogumbo (~> 1.4.1) nokogumbo (~> 1.4.1)
sass (3.4.25) sass (3.5.3)
scss_lint (0.54.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
scss_lint (0.56.0)
rake (>= 0.9, < 13) rake (>= 0.9, < 13)
sass (~> 3.4.20) sass (~> 3.5.3)
sidekiq (5.0.4) sidekiq (5.0.5)
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, >= 3.3.3) redis (>= 3.3.4, < 5)
sidekiq-bulk (0.1.1) sidekiq-bulk (0.1.1)
activesupport activesupport
sidekiq sidekiq
sidekiq-scheduler (2.1.9) sidekiq-scheduler (2.1.10)
redis (~> 3) redis (>= 3, < 5)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 3) sidekiq (>= 3)
tilt (>= 1.4.0) tilt (>= 1.4.0)
@ -477,7 +485,6 @@ 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)
@ -485,7 +492,7 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sshkit (1.14.0) sshkit (1.15.1)
net-scp (>= 1.1.2) net-scp (>= 1.1.2)
net-ssh (>= 2.8.0) net-ssh (>= 2.8.0)
statsd-ruby (1.2.1) statsd-ruby (1.2.1)
@ -500,9 +507,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.3) tzinfo (1.2.4)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2017.2) tzinfo-data (1.2017.3)
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)
@ -513,20 +520,20 @@ GEM
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
warden (1.2.7) warden (1.2.7)
rack (>= 1.0) rack (>= 1.0)
webmock (3.1.0) webmock (3.1.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpacker (3.0.1) webpacker (3.0.2)
activesupport (>= 4.2) activesupport (>= 4.2)
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 4.2) railties (>= 4.2)
webpush (0.3.2) webpush (0.3.3)
hkdf (~> 0.2) hkdf (~> 0.2)
jwt jwt (~> 2.0)
websocket-driver (0.6.5) websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2) websocket-extensions (0.1.3)
xpath (2.1.0) xpath (2.1.0)
nokogiri (~> 1.3) nokogiri (~> 1.3)
@ -538,19 +545,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)
aws-sdk (~> 2.9) aws-sdk (~> 2.10)
better_errors (~> 2.1) better_errors (~> 2.4)
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.8) capistrano (~> 3.10)
capistrano-rails (~> 1.2) capistrano-rails (~> 1.3)
capistrano-rbenv (~> 2.1) capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0) capistrano-yarn (~> 2.0)
capybara (~> 2.14) capybara (~> 2.15)
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,9 +565,11 @@ 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.16) fabrication (~> 2.18)
faker (~> 1.7) faker (~> 1.7)
fast_blank (~> 1.0) fast_blank (~> 1.0)
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)
@ -574,29 +583,28 @@ DEPENDENCIES
idn-ruby idn-ruby
iso-639 iso-639
json-ld-preloaded (~> 2.2.1) json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0) kaminari (~> 1.1)
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.5) lograge (~> 0.7)
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.7) nokogiri (~> 1.8)
nsa (~> 0.2) nsa (~> 0.2)
oj (~> 3.0) oj (~> 3.3)
ostatus2 (~> 2.0) ostatus2 (~> 2.0)
ox (~> 2.5) ox (~> 2.8)
paperclip (~> 5.1) paperclip (~> 5.1)
paperclip-av-transcoder (~> 0.6) paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.14) parallel_tests (~> 2.17)
pg (~> 0.20) pg (~> 0.20)
pghero (~> 1.7) pghero (~> 1.7)
pkg-config (~> 1.2) pkg-config (~> 1.2)
pry-rails (~> 0.3) pry-rails (~> 0.3)
puma (~> 3.10) puma (~> 3.10)
pundit (~> 1.1) pundit (~> 1.1)
rabl (~> 0.13)
rack-attack (~> 5.0) rack-attack (~> 5.0)
rack-cors (~> 0.4) rack-cors (~> 0.4)
rack-timeout (~> 0.4) rack-timeout (~> 0.4)
@ -609,12 +617,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.6) rspec-rails (~> 3.7)
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.53) scss_lint (~> 0.55)
sidekiq (~> 5.0) sidekiq (~> 5.0)
sidekiq-bulk (~> 0.1.1) sidekiq-bulk (~> 0.1.1)
sidekiq-scheduler (~> 2.1) sidekiq-scheduler (~> 2.1)
@ -635,4 +643,4 @@ RUBY VERSION
ruby 2.4.2p198 ruby 2.4.2p198
BUNDLED WITH BUNDLED WITH
1.15.4 1.16.0

View File

@ -1,8 +1,8 @@
![Mastodon](https://i.imgur.com/NhZc40l.png) ![Mastodon](https://i.imgur.com/NhZc40l.png)
======== ========
[![Build Status](http://img.shields.io/travis/tootsuite/mastodon.svg)][travis] [![Build Status](https://img.shields.io/travis/tootsuite/mastodon.svg)][travis]
[![Code Climate](https://img.shields.io/codeclimate/github/tootsuite/mastodon.svg)][code_climate] [![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[travis]: https://travis-ci.org/tootsuite/mastodon [travis]: https://travis-ci.org/tootsuite/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon [code_climate]: https://codeclimate.com/github/tootsuite/mastodon

View File

@ -1,31 +1,41 @@
# frozen_string_literal: true # frozen_string_literal: true
class Admin::AccountModerationNotesController < Admin::BaseController module Admin
def create class AccountModerationNotesController < BaseController
@account_moderation_note = current_account.account_moderation_notes.new(resource_params) before_action :set_account_moderation_note, only: [:destroy]
if @account_moderation_note.save
@target_account = @account_moderation_note.target_account def create
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg') authorize AccountModerationNote, :create?
else
@account = @account_moderation_note.target_account @account_moderation_note = current_account.account_moderation_notes.new(resource_params)
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show' 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')
else
@account = @account_moderation_note.target_account
@moderation_notes = @account.targeted_moderation_notes.latest
render template: 'admin/accounts/show'
end
end
def destroy
authorize @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')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
def set_account_moderation_note
@account_moderation_note = AccountModerationNote.find(params[:id])
end end
end end
def destroy
@account_moderation_note = AccountModerationNote.find(params[:id])
@target_account = @account_moderation_note.target_account
@account_moderation_note.destroy
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
end
private
def resource_params
params.require(:account_moderation_note).permit(
:content,
:target_account_id
)
end
end end

View File

@ -2,29 +2,57 @@
module Admin module Admin
class AccountsController < BaseController class AccountsController < BaseController
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload] before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
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!
log_action :memorialize, @account
redirect_to admin_account_path(@account.id)
end
def enable
authorize @account.user, :enable?
@account.user.enable!
log_action :enable, @account.user
redirect_to admin_account_path(@account.id)
end
def disable
authorize @account.user, :disable?
@account.user.disable!
log_action :disable, @account.user
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!
@ -42,6 +70,10 @@ 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
@ -57,7 +89,8 @@ module Admin
:username, :username,
:display_name, :display_name,
:email, :email,
:ip :ip,
:staff
) )
end end
end end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Admin
class ActionLogsController < BaseController
def index
@action_logs = Admin::ActionLog.page(params[:page])
end
end
end

View File

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

View File

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

View File

@ -5,47 +5,73 @@ module Admin
before_action :set_custom_emoji, except: [:index, :new, :create] before_action :set_custom_emoji, except: [:index, :new, :create]
def index def index
@custom_emojis = filtered_custom_emojis.page(params[:page]) authorize :custom_emoji, :index?
@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
log_action :create, @custom_emoji
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg') redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
else else
render :new render :new
end end
end end
def update
authorize @custom_emoji, :update?
if @custom_emoji.update(resource_params)
log_action :update, @custom_emoji
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
@custom_emoji.destroy authorize @custom_emoji, :destroy?
@custom_emoji.destroy!
log_action :destroy, @custom_emoji
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
emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image) authorize @custom_emoji, :copy?
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
emoji.image = @custom_emoji.image
if emoji.save if emoji.save
log_action :create, emoji
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(params[:page]) redirect_to admin_custom_emojis_path(page: params[:page])
end end
def enable def enable
authorize @custom_emoji, :enable?
@custom_emoji.update!(disabled: false) @custom_emoji.update!(disabled: false)
log_action :enable, @custom_emoji
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)
log_action :disable, @custom_emoji
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
@ -56,7 +82,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:custom_emoji).permit(:shortcode, :image) params.require(:custom_emoji).permit(:shortcode, :image, :visible_in_picker)
end end
def filtered_custom_emojis def filtered_custom_emojis
@ -66,7 +92,9 @@ module Admin
def filter_params def filter_params
params.permit( params.permit(
:local, :local,
:remote :remote,
:by_domain,
:shortcode
) )
end end
end end

View File

@ -5,28 +5,37 @@ 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
DomainBlockWorker.perform_async(@domain_block.id) DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg') redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
else else
render :new render :new
end end
end end
def show; end def show
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?)
log_action :destroy, @domain_block
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,17 +5,22 @@ 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
log_action :create, @email_domain_block
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg') redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
else else
render :new render :new
@ -23,7 +28,9 @@ module Admin
end end
def destroy def destroy
@email_domain_block.destroy authorize @email_domain_block, :destroy?
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
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,10 +3,12 @@
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

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Admin
class InvitesController < BaseController
def index
authorize :invite, :index?
@invites = filtered_invites.includes(user: :account).page(params[:page])
@invite = Invite.new
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to admin_invites_path
else
@invites = Invite.page(params[:page])
render :index
end
end
def destroy
@invite = Invite.find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to admin_invites_path
end
private
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
def filtered_invites
InviteFilter.new(filter_params).results
end
def filter_params
params.permit(:available, :expired)
end
end
end

View File

@ -2,26 +2,29 @@
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
@form = Form::StatusBatch.new(form_status_batch_params) authorize :status, :update?
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.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
@status.update(status_params) authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
redirect_to admin_report_path(@report) redirect_to admin_report_path(@report)
end end
def destroy def destroy
authorize @status, :destroy? authorize @status, :destroy?
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status render json: @status
end end

View File

@ -5,14 +5,17 @@ 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
@ -22,12 +25,17 @@ module Admin
def process_report def process_report
case params[:outcome].to_s case params[:outcome].to_s
when 'resolve' when 'resolve'
@report.update(action_taken_by_current_attributes) @report.update!(action_taken_by_current_attributes)
log_action :resolve, @report
when 'suspend' when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id) Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report
log_action :suspend, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
when 'silence' when 'silence'
@report.target_account.update(silenced: true) @report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
resolve_all_target_account_reports resolve_all_target_account_reports
else else
raise ActiveRecord::RecordNotFound raise ActiveRecord::RecordNotFound

View File

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

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Admin
class RolesController < BaseController
before_action :set_user
def promote
authorize @user, :promote?
@user.promote!
log_action :promote, @user
redirect_to admin_account_path(@user.account_id)
end
def demote
authorize @user, :demote?
@user.demote!
log_action :demote, @user
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

@ -13,14 +13,17 @@ module Admin
closed_registrations_message closed_registrations_message
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge
bootstrap_timeline_accounts bootstrap_timeline_accounts
thumbnail thumbnail
min_invite_role
).freeze ).freeze
BOOLEAN_SETTINGS = %w( BOOLEAN_SETTINGS = %w(
open_registrations open_registrations
open_deletion open_deletion
timeline_preview timeline_preview
show_staff_badge
).freeze ).freeze
UPLOAD_SETTINGS = %w( UPLOAD_SETTINGS = %w(
@ -28,10 +31,13 @@ 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,12 +5,16 @@ module Admin
before_action :set_account before_action :set_account
def create def create
@account.update(silenced: true) authorize @account, :silence?
@account.update!(silenced: true)
log_action :silence, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
@account.update(silenced: false) authorize @account, :unsilence?
@account.update!(silenced: false)
log_action :unsilence, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@ -2,8 +2,6 @@
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
@ -12,31 +10,39 @@ 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)
@form = Form::StatusBatch.new @statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
@form = Form::StatusBatch.new
end end
def create def create
@form = Form::StatusBatch.new(form_status_batch_params) authorize :status, :update?
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
flash[:alert] = I18n.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
@status.update(status_params) authorize @status, :update?
@status.update!(status_params)
log_action :update, @status
redirect_to admin_account_statuses_path(@account.id, current_params) redirect_to admin_account_statuses_path(@account.id, current_params)
end end
def destroy def destroy
authorize @status, :destroy? authorize @status, :destroy?
RemovalWorker.perform_async(@status.id) RemovalWorker.perform_async(@status.id)
log_action :destroy, @status
render json: @status render json: @status
end end
@ -60,6 +66,7 @@ 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,6 +3,7 @@
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,12 +5,16 @@ 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)
log_action :suspend, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def destroy def destroy
@account.update(suspended: false) authorize @account, :unsuspend?
@account.unsuspend!
log_action :unsuspend, @account
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@ -5,7 +5,9 @@ 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!
log_action :disable_2fa, @user
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end

View File

@ -72,19 +72,4 @@ class Api::BaseController < ApplicationController
def render_empty def render_empty
render json: {}, status: 200 render json: {}, status: 200
end end
def set_maps(statuses) # rubocop:disable Style/AccessorMethodName
if current_account.nil?
@reblogs_map = {}
@favourites_map = {}
@mutes_map = {}
return
end
status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq
conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq
@reblogs_map = Status.reblogs_map(status_ids, current_account)
@favourites_map = Status.favourites_map(status_ids, current_account)
@mutes_map = Status.mutes_map(conversation_ids, current_account)
end
end end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class Api::V1::Accounts::ListsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_account
respond_to :json
def index
@lists = @account.lists.where(account: current_account)
render json: @lists, each_serializer: REST::ListSerializer
end
private
def set_account
@account = Account.find(params[:account_id])
end
end

View File

@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
AccountSearchService.new.call( AccountSearchService.new.call(
params[:q], params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolving_search?, current_account,
current_account resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
) )
end end
def resolving_search? def truthy_param?(key)
params[:resolve] == 'true' params[key] == 'true'
end end
end end

View File

@ -13,9 +13,9 @@ class Api::V1::AccountsController < Api::BaseController
end end
def follow def follow
FollowService.new.call(current_user.account, @account.acct) FollowService.new.call(current_user.account, @account.acct, reblogs: params[:reblogs])
options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } } options = @account.locked? ? {} : { following_map: { @account.id => { reblogs: params[:reblogs] } }, requested_map: { @account.id => false } }
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
end end
@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def mute def mute
MuteService.new.call(current_user.account, @account) MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end end
@ -51,7 +51,7 @@ class Api::V1::AccountsController < Api::BaseController
@account = Account.find(params[:id]) @account = Account.find(params[:id])
end end
def relationships(options = {}) def relationships(**options)
AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options) AccountRelationshipsPresenter.new([@account.id], current_user.account_id, options)
end end
end end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
class Api::V1::Lists::AccountsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:show]
before_action -> { doorkeeper_authorize! :write }, except: [:show]
before_action :require_user!
before_action :set_list
after_action :insert_pagination_headers, only: :show
def show
@accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer
end
def create
ApplicationRecord.transaction do
list_accounts.each do |account|
@list.accounts << account
end
end
render_empty
end
def destroy
ListAccount.where(list: @list, account_id: account_ids).destroy_all
render_empty
end
private
def set_list
@list = List.where(account: current_account).find(params[:list_id])
end
def load_accounts
if unlimited?
@list.accounts.all
else
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def list_accounts
Account.find(account_ids)
end
def account_ids
Array(resource_params[:account_ids])
end
def resource_params
params.permit(account_ids: [])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
return if unlimited?
if records_continue?
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
end
end
def prev_path
return if unlimited?
unless @accounts.empty?
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
end
end
def pagination_max_id
@accounts.last.id
end
def pagination_since_id
@accounts.first.id
end
def records_continue?
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
def unlimited?
params[:limit] == '0'
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class Api::V1::ListsController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
before_action :require_user!
before_action :set_list, except: [:index, :create]
def index
@lists = List.where(account: current_account).all
render json: @lists, each_serializer: REST::ListSerializer
end
def show
render json: @list, serializer: REST::ListSerializer
end
def create
@list = List.create!(list_params.merge(account: current_account))
render json: @list, serializer: REST::ListSerializer
end
def update
@list.update!(list_params)
render json: @list, serializer: REST::ListSerializer
end
def destroy
@list.destroy!
render_empty
end
private
def set_list
@list = List.where(account: current_account).find(params[:id])
end
def list_params
params.permit(:title)
end
end

View File

@ -19,7 +19,7 @@ class Api::V1::ReportsController < Api::BaseController
comment: report_params[:comment] comment: report_params[:comment]
) )
User.admins.includes(:account).each { |u| AdminMailer.new_report(u.account, @report).deliver_later } User.staff.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,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::V1::SearchController < Api::BaseController class Api::V1::SearchController < Api::BaseController
include Authorization
RESULTS_LIMIT = 5 RESULTS_LIMIT = 5
before_action -> { doorkeeper_authorize! :read } before_action -> { doorkeeper_authorize! :read }
@ -9,12 +11,24 @@ class Api::V1::SearchController < Api::BaseController
respond_to :json respond_to :json
def index def index
@search = Search.new(search_results) @search = Search.new(search)
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

@ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end end
def account_home_feed def account_home_feed
Feed.new(:home, current_account) HomeFeed.new(current_account)
end end
def insert_pagination_headers def insert_pagination_headers

View File

@ -0,0 +1,66 @@
# frozen_string_literal: true
class Api::V1::Timelines::ListController < Api::BaseController
before_action -> { doorkeeper_authorize! :read }
before_action :require_user!
before_action :set_list
before_action :set_statuses
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
render json: @statuses,
each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
end
private
def set_list
@list = List.where(account: current_account).find(params[:id])
end
def set_statuses
@statuses = cached_list_statuses
end
def cached_list_statuses
cache_collection list_statuses, Status
end
def list_statuses
list_feed.get(
limit_param(DEFAULT_STATUSES_LIMIT),
params[:max_id],
params[:since_id]
)
end
def list_feed
ListFeed.new(@list)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def pagination_params(core_params)
params.permit(:limit).merge(core_params)
end
def next_path
api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
end
def prev_path
api_v1_timelines_list_url params[:id], 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

@ -28,6 +28,8 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
}, },
} }
data.deep_merge!(params[:data]) if params[:data]
web_subscription = ::Web::PushSubscription.create!( web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint], endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh], key_p256dh: params[:subscription][:keys][:p256dh],

View File

@ -18,6 +18,7 @@ class ApplicationController < ActionController::Base
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?
@ -40,6 +41,10 @@ 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
@ -99,7 +104,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.values.each do |item| uncached.each_value do |item|
Rails.cache.write(item.cache_key, item) Rails.cache.write(item.cache_key, item)
end end
end end

View File

@ -16,13 +16,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def build_resource(hash = nil) def build_resource(hash = nil)
super(hash) super(hash)
resource.locale = I18n.locale
resource.locale = I18n.locale
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
resource.build_account if resource.account.nil? resource.build_account if resource.account.nil?
end end
def configure_sign_up_params def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up) do |u| devise_parameter_sanitizer.permit(:sign_up) do |u|
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation) u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
end end
end end
@ -35,7 +38,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end end
def check_enabled_registrations def check_enabled_registrations
redirect_to root_path if single_user_mode? || !Setting.open_registrations redirect_to root_path if single_user_mode? || !allowed_registrations?
end
def allowed_registrations?
Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
end
def invite_code
if params[:user]
params[:user][:invite_code]
else
params[:invite_code]
end
end end
private private

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 && user.valid_password?(user_params[:password]) elsif user&.valid_password?(user_params[:password])
prompt_for_two_factor(user) prompt_for_two_factor(user)
end end
end end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target)
Admin::ActionLog.create(account: current_account, action: action, target: target)
end
end

View File

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

View File

@ -44,7 +44,8 @@ module RateLimitHeaders
end end
def api_throttle_data def api_throttle_data
request.env['rack.attack.throttle_data']['api'] most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
request.env['rack.attack.throttle_data'][most_limited_type]
end end
def request_time def request_time

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class InvitesController < ApplicationController
include Authorization
layout 'admin'
before_action :authenticate_user!
def index
authorize :invite, :create?
@invites = Invite.where(user: current_user)
@invite = Invite.new(expires_in: 1.day.to_i)
end
def create
authorize :invite, :create?
@invite = Invite.new(resource_params)
@invite.user = current_user
if @invite.save
redirect_to invites_path
else
@invites = Invite.where(user: current_user)
render :index
end
end
def destroy
@invite = Invite.where(user: current_user).find(params[:id])
authorize @invite, :destroy?
@invite.expire!
redirect_to invites_path
end
private
def resource_params
params.require(:invite).permit(:max_uses, :expires_in)
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class Settings::MigrationsController < ApplicationController
layout 'admin'
before_action :authenticate_user!
def show
@migration = Form::Migration.new(account: current_account.moved_to_account)
end
def update
@migration = Form::Migration.new(resource_params)
if @migration.valid? && migration_account_changed?
current_account.update!(moved_to_account: @migration.account)
ActivityPub::UpdateDistributionWorker.perform_async(current_account.id)
redirect_to settings_migration_path, notice: I18n.t('migrations.updated_msg')
else
render :show
end
end
private
def resource_params
params.require(:migration).permit(:acct)
end
def migration_account_changed?
current_account.moved_to_account_id != @migration.account&.id &&
current_account.id != @migration.account&.id
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) interactions: %i(must_be_follower must_be_following must_be_following_dm)
) )
end end
end end

View File

@ -6,12 +6,10 @@ module WellKnown
def show def show
@account = Account.find_local!(username_from_resource) @account = Account.find_local!(username_from_resource)
@canonical_account_uri = @account.to_webfinger_s
@magic_key = pem_to_magic_key(@account.keypair.public_key)
respond_to do |format| respond_to do |format|
format.any(:json, :html) do format.any(:json, :html) do
render formats: :json, content_type: 'application/jrd+json' render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
end end
format.xml do format.xml do
@ -35,21 +33,6 @@ module WellKnown
WebfingerResource.new(resource_user).username WebfingerResource.new(resource_user).username
end end
def pem_to_magic_key(public_key)
modulus, exponent = [public_key.n, public_key.e].map do |component|
result = []
until component.zero?
result << [component % 256].pack('C')
component >>= 8
end
result.reverse.join
end
(['RSA'] + [modulus, exponent].map { |n| Base64.urlsafe_encode64(n) }).join('.')
end
def resource_param def resource_param
params.require(:resource) params.require(:resource)
end end

View File

@ -0,0 +1,103 @@
# frozen_string_literal: true
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
def linkable_log_target(record)
case record.class.name
when 'Account'
link_to record.acct, admin_account_path(record.id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
when 'Report'
link_to "##{record.id}", admin_report_path(record)
when 'DomainBlock', 'EmailDomainBlock'
link_to record.domain, "https://#{record.domain}"
when 'Status'
link_to record.account.acct, TagManager.instance.url_for(record)
end
end
def log_target_from_history(type, attributes)
case type
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'EmailDomainBlock'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
end
end
def relevant_log_changes(log)
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
log.recorded_changes.slice('domain')
elsif log.target_type == 'CustomEmoji' && log.action == :update
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
end
def log_extra_attributes(hash)
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
end
def log_change(val)
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
end
def icon_for_log(log)
case log.target_type
when 'Account', 'User'
'user'
when 'CustomEmoji'
'file'
when 'Report'
'flag'
when 'DomainBlock'
'lock'
when 'EmailDomainBlock'
'envelope'
when 'Status'
'pencil'
end
end
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize
'neutral'
when :demote, :silence, :disable, :suspend
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'
else
''
end
end
private
def opposite_verbs?(log)
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
end
end

View File

@ -1,10 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
module Admin::FilterHelper module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip).freeze ACCOUNT_FILTERS = %i(local remote by_domain silenced suspended recent username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS + INVITE_FILTER + CUSTOM_EMOJI_FILTERS
def filter_link_to(text, link_to_params, link_class_params = link_to_params) def filter_link_to(text, link_to_params, link_class_params = link_to_params)
new_url = filtered_url_for(link_to_params) new_url = filtered_url_for(link_to_params)
@ -12,13 +14,13 @@ module Admin::FilterHelper
link_to text, new_url, class: filter_link_class(new_class) link_to text, new_url, class: filter_link_class(new_class)
end end
def table_link_to(icon, text, path, options = {}) def table_link_to(icon, text, path, **options)
link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link')
end end
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' ? true : false filter_link_class(new_url) == 'selected'
end end
private private

View File

@ -5,7 +5,7 @@ module ApplicationHelper
current_page?(path) ? 'active' : '' current_page?(path) ? 'active' : ''
end end
def active_link_to(label, path, options = {}) def active_link_to(label, path, **options)
link_to label, path, options.merge(class: active_nav_class(path)) link_to label, path, options.merge(class: active_nav_class(path))
end end
@ -35,6 +35,11 @@ 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'
@ -43,6 +48,10 @@ 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,6 +9,28 @@ module JsonLdHelper
value.is_a?(Array) ? value.first : value value.is_a?(Array) ? value.first : value
end end
# The url attribute can be a string, an array of strings, or an array of objects.
# The objects could include a mimeType. Not-included mimeType means it's text/html.
def url_to_href(value, preferred_type = nil)
single_value = if value.is_a?(Array) && !value.first.is_a?(String)
value.find { |link| preferred_type.nil? || ((link['mimeType'].presence || 'text/html') == preferred_type) }
elsif value.is_a?(Array)
value.first
else
value
end
if single_value.nil? || single_value.is_a?(String)
single_value
else
single_value['href']
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

@ -11,7 +11,7 @@ module RoutingHelper
end end
end end
def full_asset_url(source, options = {}) def full_asset_url(source, **options)
source = ActionController::Base.helpers.asset_url(source, options) unless use_storage? source = ActionController::Base.helpers.asset_url(source, options) unless use_storage?
URI.join(root_url, source).to_s URI.join(root_url, source).to_s

View File

@ -10,6 +10,7 @@ module SettingsHelper
eo: 'Esperanto', eo: 'Esperanto',
es: 'Español', es: 'Español',
fa: 'فارسی', fa: 'فارسی',
gl: 'Galego',
fi: 'Suomi', fi: 'Suomi',
fr: 'Français', fr: 'Français',
he: 'עברית', he: 'עברית',

View File

@ -105,12 +105,13 @@ export function fetchAccountFail(id, error) {
}; };
}; };
export function followAccount(id) { export function followAccount(id, reblogs = true) {
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`).then(response => { api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => {
dispatch(followAccountSuccess(response.data)); dispatch(followAccountSuccess(response.data, alreadyFollowing));
}).catch(error => { }).catch(error => {
dispatch(followAccountFail(error)); dispatch(followAccountFail(error));
}); });
@ -136,10 +137,11 @@ export function followAccountRequest(id) {
}; };
}; };
export function followAccountSuccess(relationship) { export function followAccountSuccess(relationship, alreadyFollowing) {
return { return {
type: ACCOUNT_FOLLOW_SUCCESS, type: ACCOUNT_FOLLOW_SUCCESS,
relationship, relationship,
alreadyFollowing,
}; };
}; };
@ -241,11 +243,11 @@ export function unblockAccountFail(error) {
}; };
export function muteAccount(id) { export function muteAccount(id, notifications) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(muteAccountRequest(id)); dispatch(muteAccountRequest(id));
api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => { api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
}).catch(error => { }).catch(error => {

View File

@ -10,6 +10,10 @@ export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FA
export function fetchFavouritedStatuses() { export function fetchFavouritedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
if (getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return;
}
dispatch(fetchFavouritedStatusesRequest()); dispatch(fetchFavouritedStatusesRequest());
api(getState).get('/api/v1/favourites').then(response => { api(getState).get('/api/v1/favourites').then(response => {
@ -46,7 +50,7 @@ export function expandFavouritedStatuses() {
return (dispatch, getState) => { return (dispatch, getState) => {
const url = getState().getIn(['status_lists', 'favourites', 'next'], null); const url = getState().getIn(['status_lists', 'favourites', 'next'], null);
if (url === null) { if (url === null || getState().getIn(['status_lists', 'favourites', 'isLoading'])) {
return; return;
} }

View File

@ -0,0 +1,313 @@
import api from '../api';
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
}
dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(id, err)));
};
export const fetchListRequest = id => ({
type: LIST_FETCH_REQUEST,
id,
});
export const fetchListSuccess = list => ({
type: LIST_FETCH_SUCCESS,
list,
});
export const fetchListFail = (id, error) => ({
type: LIST_FETCH_FAIL,
id,
error,
});
export const fetchLists = () => (dispatch, getState) => {
dispatch(fetchListsRequest());
api(getState).get('/api/v1/lists')
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
export const fetchListsRequest = () => ({
type: LISTS_FETCH_REQUEST,
});
export const fetchListsSuccess = lists => ({
type: LISTS_FETCH_SUCCESS,
lists,
});
export const fetchListsFail = error => ({
type: LISTS_FETCH_FAIL,
error,
});
export const submitListEditor = shouldReset => (dispatch, getState) => {
const listId = getState().getIn(['listEditor', 'listId']);
const title = getState().getIn(['listEditor', 'title']);
if (listId === null) {
dispatch(createList(title, shouldReset));
} else {
dispatch(updateList(listId, title, shouldReset));
}
};
export const setupListEditor = listId => (dispatch, getState) => {
dispatch({
type: LIST_EDITOR_SETUP,
list: getState().getIn(['lists', listId]),
});
dispatch(fetchListAccounts(listId));
};
export const changeListEditorTitle = value => ({
type: LIST_EDITOR_TITLE_CHANGE,
value,
});
export const createList = (title, shouldReset) => (dispatch, getState) => {
dispatch(createListRequest());
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
dispatch(createListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(createListFail(err)));
};
export const createListRequest = () => ({
type: LIST_CREATE_REQUEST,
});
export const createListSuccess = list => ({
type: LIST_CREATE_SUCCESS,
list,
});
export const createListFail = error => ({
type: LIST_CREATE_FAIL,
error,
});
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(updateListFail(id, err)));
};
export const updateListRequest = id => ({
type: LIST_UPDATE_REQUEST,
id,
});
export const updateListSuccess = list => ({
type: LIST_UPDATE_SUCCESS,
list,
});
export const updateListFail = (id, error) => ({
type: LIST_UPDATE_FAIL,
id,
error,
});
export const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
export const deleteList = id => (dispatch, getState) => {
dispatch(deleteListRequest(id));
api(getState).delete(`/api/v1/lists/${id}`)
.then(() => dispatch(deleteListSuccess(id)))
.catch(err => dispatch(deleteListFail(id, err)));
};
export const deleteListRequest = id => ({
type: LIST_DELETE_REQUEST,
id,
});
export const deleteListSuccess = id => ({
type: LIST_DELETE_SUCCESS,
id,
});
export const deleteListFail = (id, error) => ({
type: LIST_DELETE_FAIL,
id,
error,
});
export const fetchListAccounts = listId => (dispatch, getState) => {
dispatch(fetchListAccountsRequest(listId));
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
.then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
.catch(err => dispatch(fetchListAccountsFail(listId, err)));
};
export const fetchListAccountsRequest = id => ({
type: LIST_ACCOUNTS_FETCH_REQUEST,
id,
});
export const fetchListAccountsSuccess = (id, accounts, next) => ({
type: LIST_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
});
export const fetchListAccountsFail = (id, error) => ({
type: LIST_ACCOUNTS_FETCH_FAIL,
id,
error,
});
export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
limit: 4,
following: true,
};
api(getState).get('/api/v1/accounts/search', { params })
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
};
export const fetchListSuggestionsReady = (query, accounts) => ({
type: LIST_EDITOR_SUGGESTIONS_READY,
query,
accounts,
});
export const clearListSuggestions = () => ({
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
});
export const changeListSuggestions = value => ({
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
value,
});
export const addToListEditor = accountId => (dispatch, getState) => {
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const addToList = (listId, accountId) => (dispatch, getState) => {
dispatch(addToListRequest(listId, accountId));
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
.then(() => dispatch(addToListSuccess(listId, accountId)))
.catch(err => dispatch(addToListFail(listId, accountId, err)));
};
export const addToListRequest = (listId, accountId) => ({
type: LIST_EDITOR_ADD_REQUEST,
listId,
accountId,
});
export const addToListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_ADD_SUCCESS,
listId,
accountId,
});
export const addToListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_ADD_FAIL,
listId,
accountId,
error,
});
export const removeFromListEditor = accountId => (dispatch, getState) => {
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
dispatch(removeFromListRequest(listId, accountId));
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
};
export const removeFromListRequest = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_REQUEST,
listId,
accountId,
});
export const removeFromListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_SUCCESS,
listId,
accountId,
});
export const removeFromListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_REMOVE_FAIL,
listId,
accountId,
error,
});

View File

@ -1,5 +1,6 @@
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts'; import { fetchRelationships } from './accounts';
import { openModal } from './modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
@ -9,6 +10,9 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
export function fetchMutes() { export function fetchMutes() {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchMutesRequest()); dispatch(fetchMutesRequest());
@ -80,3 +84,20 @@ export function expandMutesFail(error) {
error, error,
}; };
}; };
export function initMuteModal(account) {
return dispatch => {
dispatch({
type: MUTES_INIT_MODAL,
account,
});
dispatch(openModal('MUTE'));
};
}
export function toggleHideNotifications() {
return dispatch => {
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
};
}

View File

@ -4,12 +4,13 @@ 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());
const accountId = getState().getIn(['meta', 'me']); api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => {
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,5 @@
import axios from 'axios'; import axios from 'axios';
import { pushNotificationsSetting } from '../settings';
export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION';
@ -42,11 +43,15 @@ export function saveSettings() {
const state = getState().get('push_notifications'); const state = getState().get('push_notifications');
const subscription = state.get('subscription'); const subscription = state.get('subscription');
const alerts = state.get('alerts'); const alerts = state.get('alerts');
const data = { alerts };
axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, {
data: { data,
alerts, }).then(() => {
}, const me = getState().getIn(['meta', 'me']);
if (me) {
pushNotificationsSetting.set(me, data);
}
}); });
}; };
} }

View File

@ -1,4 +1,4 @@
import createStream from '../stream'; import { connectStream } from '../stream';
import { import {
updateTimeline, updateTimeline,
deleteFromTimelines, deleteFromTimelines,
@ -12,42 +12,19 @@ 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) => {
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']); return connectStream (path, pollingRefresh, (dispatch, getState) => {
const accessToken = getState().getIn(['meta', 'access_token']);
const locale = getState().getIn(['meta', 'locale']); const locale = getState().getIn(['meta', 'locale']);
let polling = null; return {
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));
}, },
disconnected () { onDisconnect() {
if (pollingRefresh) {
setupPolling();
}
dispatch(disconnectTimeline(timelineId)); dispatch(disconnectTimeline(timelineId));
}, },
received (data) { onReceive (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)));
@ -60,26 +37,8 @@ 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) {
@ -92,3 +51,4 @@ 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 connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View File

@ -118,6 +118,7 @@ export const refreshCommunityTimeline = () => refreshTimeline('community', '/
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}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function refreshTimelineFail(timeline, error, skipLoading) { export function refreshTimelineFail(timeline, error, skipLoading) {
return { return {
@ -158,6 +159,7 @@ export const expandCommunityTimeline = () => expandTimeline('community', '/ap
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}`);
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function expandTimelineRequest(timeline) { export function expandTimelineRequest(timeline) {
return { return {

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { Fragment } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from './avatar'; import Avatar from './avatar';
@ -7,6 +7,7 @@ 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' },
@ -14,6 +15,8 @@ const messages = defineMessages({
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
}); });
@injectIntl @injectIntl
@ -21,7 +24,6 @@ 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,
@ -41,8 +43,16 @@ export default class Account extends ImmutablePureComponent {
this.props.onMute(this.props.account); this.props.onMute(this.props.account);
} }
handleMuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, true);
}
handleUnmuteNotifications = () => {
this.props.onMuteNotifications(this.props.account, false);
}
render () { render () {
const { account, me, intl, hidden } = this.props; const { account, intl, hidden } = this.props;
if (!account) { if (!account) {
return <div />; return <div />;
@ -70,8 +80,19 @@ export default class Account extends ImmutablePureComponent {
} else if (blocking) { } else if (blocking) {
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />; buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
} else if (muting) { } else if (muting) {
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />; let hidingNotificationsButton;
} else { if (account.getIn(['relationship', 'muting_notifications'])) {
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
} else {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<Fragment>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</Fragment>
);
} else if (!account.get('moved')) {
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />; buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
} }
} }

View File

@ -209,6 +209,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
onBlur={this.onBlur} onBlur={this.onBlur}
onPaste={this.onPaste} onPaste={this.onPaste}
style={style} style={style}
aria-autocomplete='list'
/> />
</label> </label>

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class Avatar extends React.PureComponent { export default class Avatar extends React.PureComponent {
@ -8,12 +9,12 @@ export default class Avatar extends React.PureComponent {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
style: PropTypes.object, style: PropTypes.object,
animate: PropTypes.bool,
inline: PropTypes.bool, inline: PropTypes.bool,
animate: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
animate: false, animate: autoPlayGif,
size: 20, size: 20,
inline: false, inline: false,
}; };

View File

@ -1,22 +1,29 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class AvatarOverlay extends React.PureComponent { export default class AvatarOverlay extends React.PureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
friend: ImmutablePropTypes.map.isRequired, friend: ImmutablePropTypes.map.isRequired,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
}; };
render() { render() {
const { account, friend } = this.props; const { account, friend, animate } = this.props;
const baseStyle = { const baseStyle = {
backgroundImage: `url(${account.get('avatar_static')})`, backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
}; };
const overlayStyle = { const overlayStyle = {
backgroundImage: `url(${friend.get('avatar_static')})`, backgroundImage: `url(${friend.get(animate ? 'avatar' : 'avatar_static')})`,
}; };
return ( return (

View File

@ -137,7 +137,9 @@ 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`} />
{title} <span className='column-header__title'>
{title}
</span>
<div className='column-header__buttons'> <div className='column-header__buttons'>
{backButton} {backButton}

View File

@ -110,7 +110,7 @@ export default class Dropdown extends React.PureComponent {
icon: PropTypes.string.isRequired, icon: PropTypes.string.isRequired,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
ariaLabel: PropTypes.string, title: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
@ -120,7 +120,7 @@ export default class Dropdown extends React.PureComponent {
}; };
static defaultProps = { static defaultProps = {
ariaLabel: 'Menu', title: 'Menu',
}; };
state = { state = {
@ -186,14 +186,14 @@ export default class Dropdown extends React.PureComponent {
} }
render () { render () {
const { icon, items, size, ariaLabel, disabled } = this.props; const { icon, items, size, title, disabled } = this.props;
const { expanded } = this.state; const { expanded } = this.state;
return ( return (
<div onKeyDown={this.handleKeyDown}> <div onKeyDown={this.handleKeyDown}>
<IconButton <IconButton
icon={icon} icon={icon}
title={ariaLabel} title={title}
active={expanded} active={expanded}
disabled={disabled} disabled={disabled}
size={size} size={size}

View File

@ -72,6 +72,25 @@ export default class IconButton extends React.PureComponent {
overlayed: overlay, overlayed: overlay,
}); });
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={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}> <Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
{({ rotate }) => {({ rotate }) =>

View File

@ -6,6 +6,7 @@ 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' },
@ -23,11 +24,9 @@ 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,
@ -47,7 +46,7 @@ class Item extends React.PureComponent {
} }
hoverToPlay () { hoverToPlay () {
const { attachment, autoPlayGif } = this.props; const { attachment } = this.props;
return !autoPlayGif && attachment.get('type') === 'gifv'; return !autoPlayGif && attachment.get('type') === 'gifv';
} }
@ -139,7 +138,7 @@ class Item extends React.PureComponent {
</a> </a>
); );
} else if (attachment.get('type') === 'gifv') { } else if (attachment.get('type') === 'gifv') {
const autoPlay = !isIOS() && this.props.autoPlayGif; const autoPlay = !isIOS() && autoPlayGif;
thumbnail = ( thumbnail = (
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}> <div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
@ -181,11 +180,9 @@ 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 +258,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)} autoPlayGif={this.props.autoPlayGif} />; children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
} else { } else {
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} />); children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} 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'; import { ScrollContainer } from 'react-router-scroll-4';
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

@ -36,9 +36,6 @@ 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,9 +51,6 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [ updateOnProps = [
'status', 'status',
'account', 'account',
'me',
'boostModal',
'autoPlayGif',
'muted', 'muted',
'hidden', 'hidden',
] ]
@ -197,7 +191,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} autoPlayGif={this.props.autoPlayGif} />} {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
</Bundle> </Bundle>
); );
} }

View File

@ -5,6 +5,7 @@ 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' },
@ -47,7 +48,6 @@ 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,
}; };
@ -56,7 +56,6 @@ 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',
] ]
@ -116,7 +115,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
} }
render () { render () {
const { status, me, intl, withDismiss } = this.props; const { status, intl, withDismiss } = this.props;
const mutingConversation = status.get('muted'); const mutingConversation = status.get('muted');
const anonymousAccess = !me; const anonymousAccess = !me;
@ -180,7 +179,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
{shareButton} {shareButton}
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} /> <DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
</div> </div>
</div> </div>
); );

View File

@ -12,6 +12,8 @@ import {
unmuteAccount, unmuteAccount,
} from '../actions/accounts'; } from '../actions/accounts';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
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' },
@ -22,8 +24,6 @@ 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 +33,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 (this.unfollowModal) { if (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),
@ -59,10 +59,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(muteAccount(account.get('id'))); dispatch(initMuteModal(account));
} }
}, },
onMuteNotifications (account, notifications) {
dispatch(muteAccount(account.get('id'), notifications));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));

View File

@ -6,15 +6,14 @@ 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 (initialStateContainer !== null) { if (initialState) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }

View File

@ -4,18 +4,19 @@ 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'; import { ScrollContext } from 'react-router-scroll-4';
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 hydrateAction = hydrateStore(JSON.parse(document.getElementById('initial-state').textContent)); const hydrateAction = hydrateStore(initialState);
store.dispatch(hydrateAction); store.dispatch(hydrateAction);
export default class Mastodon extends React.PureComponent { export default class Mastodon extends React.PureComponent {

View File

@ -14,20 +14,18 @@ import {
pin, pin,
unpin, unpin,
} from '../actions/interactions'; } from '../actions/interactions';
import { import { blockAccount } from '../actions/accounts';
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 = () => {
@ -35,10 +33,6 @@ 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;
@ -58,7 +52,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (status.get('reblogged')) { if (status.get('reblogged')) {
dispatch(unreblog(status)); dispatch(unreblog(status));
} else { } else {
if (e.shiftKey || !this.boostModal) { if (e.shiftKey || !boostModal) {
this.onModalReblog(status); this.onModalReblog(status);
} else { } else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog })); dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
@ -87,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onDelete (status) { onDelete (status) {
if (!this.deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.get('id'))); dispatch(deleteStatus(status.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
@ -123,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
onMute (account) { onMute (account) {
dispatch(openModal('CONFIRM', { dispatch(initMuteModal(account));
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,15 +7,14 @@ 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 (initialStateContainer !== null) { if (initialState) {
const initialState = JSON.parse(initialStateContainer.textContent);
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
} }

View File

@ -4,6 +4,7 @@ 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}' },
@ -19,6 +20,8 @@ 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
@ -26,10 +29,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,
@ -44,7 +47,7 @@ export default class ActionBar extends React.PureComponent {
} }
render () { render () {
const { account, me, intl } = this.props; const { account, intl } = this.props;
let menu = []; let menu = [];
let extraInfo = ''; let extraInfo = '';
@ -60,6 +63,14 @@ 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 {
if (account.getIn(['relationship', 'following'])) {
if (account.getIn(['relationship', 'showing_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

@ -5,8 +5,9 @@ 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';
import classNames from 'classnames';
const messages = defineMessages({ const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
@ -14,19 +15,10 @@ 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 = {
@ -44,7 +36,7 @@ class Avatar extends ImmutablePureComponent {
} }
render () { render () {
const { account, autoPlayGif } = this.props; const { account } = this.props;
const { isHovered } = this.state; const { isHovered } = this.state;
return ( return (
@ -71,20 +63,17 @@ 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, me, intl } = this.props; const { account, intl } = this.props;
if (!account) { if (!account) {
return null; return null;
@ -114,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
} }
} }
if (account.get('moved')) {
actionBtn = '';
}
if (account.get('locked')) { if (account.get('locked')) {
lockedIcon = <i className='fa fa-lock' />; lockedIcon = <i className='fa fa-lock' />;
} }
@ -122,9 +115,9 @@ export default class Header extends ImmutablePureComponent {
const displayNameHtml = { __html: account.get('display_name_html') }; const displayNameHtml = { __html: account.get('display_name_html') };
return ( return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
<div> <div>
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} /> <Avatar account={account} />
<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,14 +12,13 @@ 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'; import { ScrollContainer } from 'react-router-scroll-4';
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)
@ -31,7 +30,6 @@ 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 () {
@ -67,7 +65,7 @@ export default class AccountGallery extends ImmutablePureComponent {
} }
render () { render () {
const { medias, autoPlayGif, isLoading, hasMore } = this.props; const { medias, isLoading, hasMore } = this.props;
let loadMore = null; let loadMore = null;
@ -100,7 +98,6 @@ 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

@ -5,15 +5,16 @@ import InnerHeader from '../../account/components/header';
import ActionBar from '../../account/components/action_bar'; import ActionBar from '../../account/components/action_bar';
import MissingIndicator from '../../../components/missing_indicator'; import MissingIndicator from '../../../components/missing_indicator';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import MovedNote from './moved_note';
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,
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,6 +41,10 @@ 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);
} }
@ -61,7 +66,7 @@ export default class Header extends ImmutablePureComponent {
} }
render () { render () {
const { account, me } = this.props; const { account } = this.props;
if (account === null) { if (account === null) {
return <MissingIndicator />; return <MissingIndicator />;
@ -69,17 +74,18 @@ export default class Header extends ImmutablePureComponent {
return ( return (
<div className='account-timeline__header'> <div className='account-timeline__header'>
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
<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

@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AvatarOverlay from '../../../components/avatar_overlay';
import DisplayName from '../../../components/display_name';
export default class MovedNote extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
from: ImmutablePropTypes.map.isRequired,
to: ImmutablePropTypes.map.isRequired,
};
handleAccountClick = e => {
if (e.button === 0) {
e.preventDefault();
this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
}
e.stopPropagation();
}
render () {
const { from, to } = this.props;
const displayNameHtml = { __html: from.get('display_name_html') };
return (
<div className='account__moved-note'>
<div className='account__moved-note__message'>
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} />
</div>
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
<DisplayName account={to} />
</a>
</div>
);
}
}

View File

@ -7,19 +7,19 @@ import {
unfollowAccount, unfollowAccount,
blockAccount, blockAccount,
unblockAccount, unblockAccount,
muteAccount,
unmuteAccount, unmuteAccount,
} from '../../../actions/accounts'; } from '../../../actions/accounts';
import { mentionCompose } from '../../../actions/compose'; import { mentionCompose } from '../../../actions/compose';
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 { 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' },
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
}); });
@ -28,8 +28,6 @@ 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;
@ -39,7 +37,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 (this.unfollowModal) { if (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),
@ -69,6 +67,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onReblogToggle (account) {
if (account.getIn(['relationship', 'showing_reblogs'])) {
dispatch(followAccount(account.get('id'), false));
} else {
dispatch(followAccount(account.get('id'), true));
}
},
onReport (account) { onReport (account) {
dispatch(initReport(account)); dispatch(initReport(account));
}, },
@ -77,11 +83,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'muting'])) {
dispatch(unmuteAccount(account.get('id'))); dispatch(unmuteAccount(account.get('id')));
} else { } else {
dispatch(openModal('CONFIRM', { dispatch(initMuteModal(account));
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'))),
}));
} }
}, },

View File

@ -16,7 +16,6 @@ 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)
@ -28,7 +27,6 @@ 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 () {
@ -50,7 +48,7 @@ export default class AccountTimeline extends ImmutablePureComponent {
} }
render () { render () {
const { statusIds, isLoading, hasMore, me } = this.props; const { statusIds, isLoading, hasMore } = this.props;
if (!statusIds && isLoading) { if (!statusIds && isLoading) {
return ( return (
@ -70,7 +68,6 @@ 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'; import { ScrollContainer } from 'react-router-scroll-4';
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

@ -41,7 +41,6 @@ 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,
@ -157,6 +156,8 @@ export default class ComposeForm extends ImmutablePureComponent {
return ( return (
<div className='compose-form'> <div className='compose-form'>
<WarningContainer />
<Collapsable isVisible={this.props.spoiler} fullHeight={50}> <Collapsable isVisible={this.props.spoiler} fullHeight={50}>
<div className='spoiler-input'> <div className='spoiler-input'>
<label> <label>
@ -166,8 +167,6 @@ export default class ComposeForm extends ImmutablePureComponent {
</div> </div>
</Collapsable> </Collapsable>
<WarningContainer />
<ReplyIndicatorContainer /> <ReplyIndicatorContainer />
<div className='compose-form__autosuggest-wrapper'> <div className='compose-form__autosuggest-wrapper'>
@ -200,11 +199,11 @@ export default class ComposeForm extends ImmutablePureComponent {
<SensitiveButtonContainer /> <SensitiveButtonContainer />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
</div> </div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div>
</div>
<div className='compose-form__publish'> <div className='compose-form__publish'>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={text} /></div> <div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
<div className='compose-form__publish-button-wrapper'><Button text={publishText} onClick={this.handleSubmit} disabled={disabled || this.props.is_uploading || length(text) > 500 || (text.length !== 0 && text.trim().length === 0)} block /></div>
</div>
</div> </div>
</div> </div>
); );

View File

@ -157,7 +157,6 @@ 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 = {
@ -235,7 +234,7 @@ class EmojiPickerMenu extends React.PureComponent {
} }
render () { render () {
const { loading, style, intl, custom_emojis, autoPlay, skinTone, frequentlyUsedEmojis } = this.props; const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) { if (loading) {
return <div style={{ width: 299 }} />; return <div style={{ width: 299 }} />;
@ -250,7 +249,7 @@ class EmojiPickerMenu extends React.PureComponent {
perLine={8} perLine={8}
emojiSize={22} emojiSize={22}
sheetSize={32} sheetSize={32}
custom={buildCustomEmojis(custom_emojis, autoPlay)} custom={buildCustomEmojis(custom_emojis)}
color='' color=''
emoji='' emoji=''
set='twitter' set='twitter'
@ -284,7 +283,6 @@ 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,
@ -346,7 +344,7 @@ export default class EmojiPickerDropdown extends React.PureComponent {
} }
render () { render () {
const { intl, onPickEmoji, autoPlay, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; const { intl, onPickEmoji, 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;
@ -366,7 +364,6 @@ 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

@ -11,7 +11,7 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = { static propTypes = {
account: ImmutablePropTypes.map.isRequired, account: ImmutablePropTypes.map.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func,
}; };
render () { render () {

View File

@ -6,6 +6,7 @@ import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name'; import DisplayName from '../../../components/display_name';
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 { isRtl } from '../../../rtl';
const messages = defineMessages({ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
@ -42,7 +43,10 @@ export default class ReplyIndicator extends ImmutablePureComponent {
return null; return null;
} }
const content = { __html: status.get('contentHtml') }; const content = { __html: status.get('contentHtml') };
const style = {
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
};
return ( return (
<div className='reply-indicator'> <div className='reply-indicator'>
@ -55,7 +59,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
</a> </a>
</div> </div>
<div className='reply-indicator__content' dangerouslySetInnerHTML={content} /> <div className='reply-indicator__content' style={style} dangerouslySetInnerHTML={content} />
</div> </div>
); );
} }

View File

@ -62,13 +62,13 @@ export default class Upload extends ImmutablePureComponent {
render () { render () {
const { intl, media } = this.props; const { intl, media } = this.props;
const active = this.state.hovered || this.state.focused; const active = this.state.hovered || this.state.focused;
const description = this.state.dirtyDescription || media.get('description') || ''; const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || '';
return ( return (
<div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='compose-form__upload' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => ( {({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `translateZ(0) scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}> <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})` }}>
<IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} /> <IconButton icon='times' title={intl.formatMessage(messages.undo)} size={36} onClick={this.handleUndoClick} />
<div className={classNames('compose-form__upload-description', { active })}> <div className={classNames('compose-form__upload-description', { active })}>

View File

@ -22,7 +22,6 @@ 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']),
}); });

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.sort((a, b) => { ], emojis => emojis.filter(e => e.get('visible_in_picker')).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,7 +61,6 @@ 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,9 +1,10 @@
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', state.getIn(['meta', 'me'])]), account: state.getIn(['accounts', me]),
}; };
}; };

View File

@ -47,7 +47,7 @@ class SensitiveButton extends React.PureComponent {
'compose-form__sensitive-button--visible': visible, 'compose-form__sensitive-button--visible': visible,
}); });
return ( return (
<div className={className} style={{ transform: `translateZ(0) scale(${scale})` }}> <div className={className} style={{ transform: `scale(${scale})` }}>
<IconButton <IconButton
className='compose-form__sensitive-button__icon' className='compose-form__sensitive-button__icon'
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}

View File

@ -3,9 +3,10 @@ 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', state.getIn(['meta', 'me']), 'locked']), needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', 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, '@$2'); .replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');
}; };

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