Merge pull request #216 from glitch-soc/merge-upstream-3023725
Merge upstream at commit 3023725
main
commit
ee560abdbe
30
.env.nanobox
30
.env.nanobox
|
@ -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=
|
||||||
|
|
||||||
|
|
|
@ -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/
|
32
Gemfile
32
Gemfile
|
@ -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 'fog-aws', '~> 1.4', 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,14 +40,14 @@ gem 'http', '~> 2.2'
|
||||||
gem 'http_accept_language', '~> 2.1'
|
gem 'http_accept_language', '~> 2.1'
|
||||||
gem 'httplog', '~> 0.99'
|
gem 'httplog', '~> 0.99'
|
||||||
gem 'idn-ruby', require: 'idn'
|
gem 'idn-ruby', require: 'idn'
|
||||||
gem 'kaminari', '~> 1.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 'rabl', '~> 0.13'
|
||||||
gem 'rack-attack', '~> 5.0'
|
gem 'rack-attack', '~> 5.0'
|
||||||
|
@ -75,15 +77,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 +93,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 +107,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
|
||||||
|
|
197
Gemfile.lock
197
Gemfile.lock
|
@ -57,25 +57,17 @@ GEM
|
||||||
encryptor (~> 3.0.0)
|
encryptor (~> 3.0.0)
|
||||||
av (0.9.0)
|
av (0.9.0)
|
||||||
cocaine (~> 0.5.3)
|
cocaine (~> 0.5.3)
|
||||||
aws-sdk (2.10.46)
|
|
||||||
aws-sdk-resources (= 2.10.46)
|
|
||||||
aws-sdk-core (2.10.46)
|
|
||||||
aws-sigv4 (~> 1.0)
|
|
||||||
jmespath (~> 1.0)
|
|
||||||
aws-sdk-resources (2.10.46)
|
|
||||||
aws-sdk-core (= 2.10.46)
|
|
||||||
aws-sigv4 (1.0.2)
|
|
||||||
bcrypt (3.1.11)
|
bcrypt (3.1.11)
|
||||||
better_errors (2.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 +75,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.0)
|
||||||
capistrano (~> 3.1)
|
capistrano (~> 3.1)
|
||||||
capistrano-bundler (~> 1.1)
|
capistrano-bundler (~> 1.1)
|
||||||
capistrano-rbenv (2.1.1)
|
capistrano-rbenv (2.1.2)
|
||||||
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.15.4)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (>= 1.3.3)
|
nokogiri (>= 1.3.3)
|
||||||
|
@ -110,7 +102,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)
|
||||||
|
@ -150,16 +142,21 @@ 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)
|
||||||
ffi (1.9.18)
|
ffi (1.9.18)
|
||||||
|
fog-aws (1.4.1)
|
||||||
|
fog-core (~> 1.38)
|
||||||
|
fog-json (~> 1.0)
|
||||||
|
fog-xml (~> 0.1)
|
||||||
|
ipaddress (~> 0.8)
|
||||||
fog-core (1.45.0)
|
fog-core (1.45.0)
|
||||||
builder
|
builder
|
||||||
excon (~> 0.58)
|
excon (~> 0.58)
|
||||||
|
@ -167,15 +164,20 @@ 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)
|
||||||
|
fog-xml (0.1.3)
|
||||||
|
fog-core
|
||||||
|
nokogiri (>= 1.5.11, < 2.0.0)
|
||||||
formatador (0.2.5)
|
formatador (0.2.5)
|
||||||
fuubar (2.2.0)
|
fuubar (2.2.0)
|
||||||
rspec-core (~> 3.0)
|
rspec-core (~> 3.0)
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (0.4.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)
|
||||||
|
@ -211,7 +213,8 @@ GEM
|
||||||
httplog (0.99.7)
|
httplog (0.99.7)
|
||||||
colorize
|
colorize
|
||||||
rack
|
rack
|
||||||
i18n (0.8.6)
|
i18n (0.9.0)
|
||||||
|
concurrent-ruby (~> 1.0)
|
||||||
i18n-tasks (0.9.18)
|
i18n-tasks (0.9.18)
|
||||||
activesupport (>= 4.0.2)
|
activesupport (>= 4.0.2)
|
||||||
ast (>= 2.1.0)
|
ast (>= 2.1.0)
|
||||||
|
@ -225,29 +228,28 @@ GEM
|
||||||
idn-ruby (0.1.0)
|
idn-ruby (0.1.0)
|
||||||
ipaddress (0.8.3)
|
ipaddress (0.8.3)
|
||||||
iso-639 (0.2.8)
|
iso-639 (0.2.8)
|
||||||
jmespath (1.3.1)
|
|
||||||
json (2.1.0)
|
json (2.1.0)
|
||||||
json-ld (2.1.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.1.3)
|
||||||
jwt (1.5.6)
|
jwt (1.5.6)
|
||||||
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 +259,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.6.6)
|
||||||
mime-types (>= 1.16, < 4)
|
mime-types (>= 1.16, < 4)
|
||||||
mario-redis-lock (1.2.0)
|
mario-redis-lock (1.2.0)
|
||||||
redis (~> 3, >= 3.0.5)
|
redis (~> 3, >= 3.0.5)
|
||||||
method_source (0.8.2)
|
method_source (0.9.0)
|
||||||
microformats (4.0.7)
|
microformats (4.0.7)
|
||||||
json
|
json
|
||||||
nokogiri
|
nokogiri
|
||||||
|
@ -277,7 +280,7 @@ GEM
|
||||||
mime-types-data (3.2016.0521)
|
mime-types-data (3.2016.0521)
|
||||||
mimemagic (0.3.2)
|
mimemagic (0.3.2)
|
||||||
mini_mime (0.1.4)
|
mini_mime (0.1.4)
|
||||||
mini_portile2 (2.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 +288,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 +297,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.1)
|
||||||
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,19 +316,18 @@ 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.17.0)
|
||||||
parallel
|
parallel
|
||||||
parser (2.4.0.0)
|
parser (2.4.0.0)
|
||||||
ast (~> 2.2)
|
ast (~> 2.2)
|
||||||
pg (0.21.0)
|
pg (0.21.0)
|
||||||
pghero (1.7.0)
|
pghero (1.7.0)
|
||||||
activerecord
|
activerecord
|
||||||
pkg-config (1.2.7)
|
pkg-config (1.2.8)
|
||||||
powerpack (0.1.1)
|
powerpack (0.1.1)
|
||||||
pry (0.10.4)
|
pry (0.11.2)
|
||||||
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.0)
|
||||||
|
@ -379,31 +381,31 @@ GEM
|
||||||
thor (>= 0.18.1, < 2.0)
|
thor (>= 0.18.1, < 2.0)
|
||||||
rainbow (2.2.2)
|
rainbow (2.2.2)
|
||||||
rake
|
rake
|
||||||
rake (12.1.0)
|
rake (12.2.1)
|
||||||
rdf (2.2.9)
|
rdf (2.2.11)
|
||||||
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.5.3)
|
||||||
redis (~> 3.0, >= 3.0.4)
|
redis (~> 3.0, >= 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 +413,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.1)
|
||||||
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 +441,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)
|
||||||
|
@ -448,19 +450,19 @@ GEM
|
||||||
nokogiri (>= 1.4.4)
|
nokogiri (>= 1.4.4)
|
||||||
nokogumbo (~> 1.4.1)
|
nokogumbo (~> 1.4.1)
|
||||||
sass (3.4.25)
|
sass (3.4.25)
|
||||||
scss_lint (0.54.0)
|
scss_lint (0.55.0)
|
||||||
rake (>= 0.9, < 13)
|
rake (>= 0.9, < 13)
|
||||||
sass (~> 3.4.20)
|
sass (~> 3.4.20)
|
||||||
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 +479,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)
|
||||||
|
@ -500,9 +501,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)
|
||||||
|
@ -517,7 +518,7 @@ GEM
|
||||||
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)
|
||||||
|
@ -538,19 +539,18 @@ 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)
|
better_errors (~> 2.4)
|
||||||
better_errors (~> 2.1)
|
|
||||||
binding_of_caller (~> 0.7)
|
binding_of_caller (~> 0.7)
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman (~> 4.0)
|
brakeman (~> 4.0)
|
||||||
browser
|
browser
|
||||||
bullet (~> 5.5)
|
bullet (~> 5.5)
|
||||||
bundler-audit (~> 0.6)
|
bundler-audit (~> 0.6)
|
||||||
capistrano (~> 3.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 +558,12 @@ 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-aws (~> 1.4)
|
||||||
|
fog-core (~> 1.45)
|
||||||
|
fog-local (~> 0.4)
|
||||||
fog-openstack (~> 0.1)
|
fog-openstack (~> 0.1)
|
||||||
fuubar (~> 2.2)
|
fuubar (~> 2.2)
|
||||||
goldfinger (~> 2.0)
|
goldfinger (~> 2.0)
|
||||||
|
@ -574,22 +577,22 @@ 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)
|
||||||
|
@ -609,12 +612,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)
|
||||||
|
|
|
@ -1,23 +1,28 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Admin::AccountModerationNotesController < Admin::BaseController
|
module Admin
|
||||||
|
class AccountModerationNotesController < BaseController
|
||||||
|
before_action :set_account_moderation_note, only: [:destroy]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize AccountModerationNote, :create?
|
||||||
|
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(resource_params)
|
@account_moderation_note = current_account.account_moderation_notes.new(resource_params)
|
||||||
|
|
||||||
if @account_moderation_note.save
|
if @account_moderation_note.save
|
||||||
@target_account = @account_moderation_note.target_account
|
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
|
||||||
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.created_msg')
|
|
||||||
else
|
else
|
||||||
@account = @account_moderation_note.target_account
|
@account = @account_moderation_note.target_account
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
|
|
||||||
render template: 'admin/accounts/show'
|
render template: 'admin/accounts/show'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@account_moderation_note = AccountModerationNote.find(params[:id])
|
authorize @account_moderation_note, :destroy?
|
||||||
@target_account = @account_moderation_note.target_account
|
|
||||||
@account_moderation_note.destroy
|
@account_moderation_note.destroy
|
||||||
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
|
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -28,4 +33,9 @@ class Admin::AccountModerationNotesController < Admin::BaseController
|
||||||
:target_account_id
|
:target_account_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_account_moderation_note
|
||||||
|
@account_moderation_note = AccountModerationNote.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,29 +2,54 @@
|
||||||
|
|
||||||
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!
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable
|
||||||
|
authorize @account.user, :enable?
|
||||||
|
@account.user.enable!
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
authorize @account.user, :disable?
|
||||||
|
@account.user.disable!
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
def redownload
|
def redownload
|
||||||
|
authorize @account, :redownload?
|
||||||
|
|
||||||
@account.reset_avatar!
|
@account.reset_avatar!
|
||||||
@account.reset_header!
|
@account.reset_header!
|
||||||
@account.save!
|
@account.save!
|
||||||
|
@ -42,6 +67,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
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class BaseController < ApplicationController
|
class BaseController < ApplicationController
|
||||||
before_action :require_admin!
|
include Authorization
|
||||||
|
|
||||||
|
before_action :require_staff!
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,15 +2,18 @@
|
||||||
|
|
||||||
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!
|
||||||
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
|
||||||
|
|
|
@ -5,14 +5,18 @@ 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
|
||||||
|
@ -23,6 +27,8 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
authorize @custom_emoji, :update?
|
||||||
|
|
||||||
if @custom_emoji.update(resource_params)
|
if @custom_emoji.update(resource_params)
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
||||||
else
|
else
|
||||||
|
@ -31,14 +37,17 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @custom_emoji, :destroy?
|
||||||
@custom_emoji.destroy
|
@custom_emoji.destroy
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy
|
def copy
|
||||||
emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
|
authorize @custom_emoji, :copy?
|
||||||
|
|
||||||
if emoji.save
|
emoji = CustomEmoji.find_or_create_by(domain: nil, shortcode: @custom_emoji.shortcode)
|
||||||
|
|
||||||
|
if emoji.update(image: @custom_emoji.image)
|
||||||
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
|
||||||
else
|
else
|
||||||
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||||
|
@ -48,11 +57,13 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
|
authorize @custom_emoji, :enable?
|
||||||
@custom_emoji.update!(disabled: false)
|
@custom_emoji.update!(disabled: false)
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable
|
def disable
|
||||||
|
authorize @custom_emoji, :disable?
|
||||||
@custom_emoji.update!(disabled: true)
|
@custom_emoji.update!(disabled: true)
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,14 +5,18 @@ 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
|
||||||
|
@ -23,9 +27,12 @@ module Admin
|
||||||
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?)
|
||||||
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
|
||||||
|
|
|
@ -5,14 +5,18 @@ 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
|
||||||
|
@ -23,6 +27,7 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @email_domain_block, :destroy?
|
||||||
@email_domain_block.destroy
|
@email_domain_block.destroy
|
||||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
|
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,19 +2,20 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class ReportedStatusesController < BaseController
|
class ReportedStatusesController < BaseController
|
||||||
include Authorization
|
|
||||||
|
|
||||||
before_action :set_report
|
before_action :set_report
|
||||||
before_action :set_status, only: [:update, :destroy]
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :status, :update?
|
||||||
|
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params)
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
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
|
||||||
|
authorize @status, :update?
|
||||||
@status.update(status_params)
|
@status.update(status_params)
|
||||||
redirect_to admin_report_path(@report)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class RolesController < BaseController
|
||||||
|
before_action :set_user
|
||||||
|
|
||||||
|
def promote
|
||||||
|
authorize @user, :promote?
|
||||||
|
@user.promote!
|
||||||
|
redirect_to admin_account_path(@user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def demote
|
||||||
|
authorize @user, :demote?
|
||||||
|
@user.demote!
|
||||||
|
redirect_to admin_account_path(@user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -28,10 +28,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)
|
||||||
|
|
|
@ -5,11 +5,13 @@ module Admin
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize @account, :silence?
|
||||||
@account.update(silenced: true)
|
@account.update(silenced: true)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @account, :unsilence?
|
||||||
@account.update(silenced: false)
|
@account.update(silenced: false)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -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,24 +10,30 @@ module Admin
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :status, :index?
|
||||||
|
|
||||||
@statuses = @account.statuses
|
@statuses = @account.statuses
|
||||||
|
|
||||||
if params[:media]
|
if params[:media]
|
||||||
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
@statuses.merge!(Status.where(id: account_media_status_ids))
|
@statuses.merge!(Status.where(id: account_media_status_ids))
|
||||||
end
|
end
|
||||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
|
||||||
|
|
||||||
|
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
||||||
@form = Form::StatusBatch.new
|
@form = Form::StatusBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :status, :update?
|
||||||
|
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params)
|
@form = Form::StatusBatch.new(form_status_batch_params)
|
||||||
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
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
|
||||||
|
authorize @status, :update?
|
||||||
@status.update(status_params)
|
@status.update(status_params)
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
end
|
end
|
||||||
|
@ -60,6 +64,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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@ module Admin
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize @account, :suspend?
|
||||||
Admin::SuspensionWorker.perform_async(@account.id)
|
Admin::SuspensionWorker.perform_async(@account.id)
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@account.update(suspended: false)
|
authorize @account, :unsuspend?
|
||||||
|
@account.unsuspend!
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ module Admin
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @user, :disable_2fa?
|
||||||
@user.disable_two_factor!
|
@user.disable_two_factor!
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = 10
|
RESULTS_LIMIT = 10
|
||||||
|
|
||||||
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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
module Authorization
|
module Authorization
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Pundit
|
include Pundit
|
||||||
|
|
||||||
def pundit_user
|
def pundit_user
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -51,6 +51,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import emojify from '../../../mastodon/features/emoji/emoji';
|
import emojify from '../../../mastodon/features/emoji/emoji';
|
||||||
import IconButton from '../../../mastodon/components/icon_button';
|
import IconButton from '../../../mastodon/components/icon_button';
|
||||||
import Avatar from '../../../mastodon/components/avatar';
|
import Avatar from '../../../mastodon/components/avatar';
|
||||||
|
import { me } from '../../../mastodon/initial_state';
|
||||||
|
|
||||||
// Our imports //
|
// Our imports //
|
||||||
import { processBio } from '../../util/bio_metadata';
|
import { processBio } from '../../util/bio_metadata';
|
||||||
|
@ -88,7 +89,6 @@ export default class AccountHeader extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account : ImmutablePropTypes.map,
|
account : ImmutablePropTypes.map,
|
||||||
me : PropTypes.string.isRequired,
|
|
||||||
onFollow : PropTypes.func.isRequired,
|
onFollow : PropTypes.func.isRequired,
|
||||||
intl : PropTypes.object.isRequired,
|
intl : PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -102,7 +102,7 @@ The `render()` function is used to render our component.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
|
import RelativeTimestamp from '../../../mastodon/components/relative_timestamp';
|
||||||
import IconButton from '../../../mastodon/components/icon_button';
|
import IconButton from '../../../mastodon/components/icon_button';
|
||||||
import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../../../mastodon/containers/dropdown_menu_container';
|
||||||
|
import { me } from '../../../mastodon/initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -50,7 +51,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,
|
||||||
};
|
};
|
||||||
|
@ -59,7 +59,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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -119,7 +118,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;
|
||||||
|
|
|
@ -140,12 +140,10 @@ Here are the props we pass to `<Status>`.
|
||||||
return {
|
return {
|
||||||
status : status,
|
status : status,
|
||||||
account : account || ownProps.account,
|
account : account || ownProps.account,
|
||||||
me : state.getIn(['meta', 'me']),
|
|
||||||
settings : state.get('local_settings'),
|
settings : state.get('local_settings'),
|
||||||
prepend : prepend || ownProps.prepend,
|
prepend : prepend || ownProps.prepend,
|
||||||
reblogModal : state.getIn(['meta', 'boost_modal']),
|
reblogModal : state.getIn(['meta', 'boost_modal']),
|
||||||
deleteModal : state.getIn(['meta', 'delete_modal']),
|
deleteModal : state.getIn(['meta', 'delete_modal']),
|
||||||
autoPlayGif : state.getIn(['meta', 'auto_play_gif']),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
// Mastodon imports //
|
// Mastodon imports //
|
||||||
import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
|
||||||
|
import { autoPlayGif } from '../../../mastodon/initial_state';
|
||||||
|
|
||||||
// Our imports //
|
// Our imports //
|
||||||
import StatusPrepend from './prepend';
|
import StatusPrepend from './prepend';
|
||||||
|
@ -89,9 +90,6 @@ few parts:
|
||||||
These are our local settings, fetched from our store. We need this
|
These are our local settings, fetched from our store. We need this
|
||||||
to determine how best to collapse our statuses, among other things.
|
to determine how best to collapse our statuses, among other things.
|
||||||
|
|
||||||
- __`me` (`PropTypes.number`) :__
|
|
||||||
This is the id of the currently-signed-in user.
|
|
||||||
|
|
||||||
- __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`,
|
- __`onFavourite`, `onReblog`, `onModalReblog`, `onDelete`,
|
||||||
`onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
|
`onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
|
||||||
`onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
|
`onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
|
||||||
|
@ -103,9 +101,6 @@ few parts:
|
||||||
reblogging and deleting statuses. They are used by the `onReblog`
|
reblogging and deleting statuses. They are used by the `onReblog`
|
||||||
and `onDelete` functions, but we don't deal with them here.
|
and `onDelete` functions, but we don't deal with them here.
|
||||||
|
|
||||||
- __`autoPlayGif` (`PropTypes.bool`) :__
|
|
||||||
This tells the frontend whether or not to autoplay gifs!
|
|
||||||
|
|
||||||
- __`muted` (`PropTypes.bool`) :__
|
- __`muted` (`PropTypes.bool`) :__
|
||||||
This has nothing to do with a user or conversation mute! "Muted" is
|
This has nothing to do with a user or conversation mute! "Muted" is
|
||||||
what Mastodon internally calls the subdued look of statuses in the
|
what Mastodon internally calls the subdued look of statuses in the
|
||||||
|
@ -160,7 +155,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
account : ImmutablePropTypes.map,
|
account : ImmutablePropTypes.map,
|
||||||
settings : ImmutablePropTypes.map,
|
settings : ImmutablePropTypes.map,
|
||||||
notification : ImmutablePropTypes.map,
|
notification : ImmutablePropTypes.map,
|
||||||
me : PropTypes.string,
|
|
||||||
onFavourite : PropTypes.func,
|
onFavourite : PropTypes.func,
|
||||||
onReblog : PropTypes.func,
|
onReblog : PropTypes.func,
|
||||||
onModalReblog : PropTypes.func,
|
onModalReblog : PropTypes.func,
|
||||||
|
@ -177,7 +171,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenVideo : PropTypes.func,
|
onOpenVideo : PropTypes.func,
|
||||||
reblogModal : PropTypes.bool,
|
reblogModal : PropTypes.bool,
|
||||||
deleteModal : PropTypes.bool,
|
deleteModal : PropTypes.bool,
|
||||||
autoPlayGif : PropTypes.bool,
|
|
||||||
muted : PropTypes.bool,
|
muted : PropTypes.bool,
|
||||||
collapse : PropTypes.bool,
|
collapse : PropTypes.bool,
|
||||||
prepend : PropTypes.string,
|
prepend : PropTypes.string,
|
||||||
|
@ -211,9 +204,7 @@ to remember to specify it here.
|
||||||
'account',
|
'account',
|
||||||
'settings',
|
'settings',
|
||||||
'prepend',
|
'prepend',
|
||||||
'me',
|
|
||||||
'boostModal',
|
'boostModal',
|
||||||
'autoPlayGif',
|
|
||||||
'muted',
|
'muted',
|
||||||
'collapse',
|
'collapse',
|
||||||
'notification',
|
'notification',
|
||||||
|
@ -560,7 +551,6 @@ this operation are further explained in the code below.
|
||||||
intersectionObserverWrapper,
|
intersectionObserverWrapper,
|
||||||
onOpenVideo,
|
onOpenVideo,
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
autoPlayGif,
|
|
||||||
notification,
|
notification,
|
||||||
...other
|
...other
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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' },
|
||||||
|
@ -23,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,
|
||||||
|
@ -52,7 +52,7 @@ export default class Account extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl, hidden } = this.props;
|
const { account, intl, hidden } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <div />;
|
return <div />;
|
||||||
|
|
|
@ -65,6 +65,7 @@ export default class IconButton extends React.PureComponent {
|
||||||
expanded,
|
expanded,
|
||||||
icon,
|
icon,
|
||||||
inverted,
|
inverted,
|
||||||
|
flip,
|
||||||
overlay,
|
overlay,
|
||||||
pressed,
|
pressed,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
|
@ -78,8 +79,8 @@ export default class IconButton extends React.PureComponent {
|
||||||
overlayed: overlay,
|
overlayed: overlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flipDeg = this.props.flip ? -180 : -360;
|
const flipDeg = flip ? -180 : -360;
|
||||||
const rotateDeg = this.props.active ? flipDeg : 0;
|
const rotateDeg = active ? flipDeg : 0;
|
||||||
|
|
||||||
const motionDefaultStyle = {
|
const motionDefaultStyle = {
|
||||||
rotate: rotateDeg,
|
rotate: rotateDeg,
|
||||||
|
@ -93,6 +94,25 @@ export default class IconButton extends React.PureComponent {
|
||||||
rotate: animate ? spring(rotateDeg, springOpts) : 0,
|
rotate: animate ? spring(rotateDeg, springOpts) : 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!animate) {
|
||||||
|
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||||
|
// we actually need to animate.
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label={title}
|
||||||
|
aria-pressed={pressed}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
title={title}
|
||||||
|
className={classes}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
style={style}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
>
|
||||||
|
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
|
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
|
||||||
{({ rotate }) =>
|
{({ rotate }) =>
|
||||||
|
|
|
@ -9,6 +9,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' },
|
||||||
|
@ -26,11 +27,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,
|
||||||
|
@ -50,7 +49,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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +141,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 })}>
|
||||||
|
@ -184,11 +183,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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -264,9 +261,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} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -39,9 +39,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,
|
||||||
|
@ -57,9 +54,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
'status',
|
'status',
|
||||||
'account',
|
'account',
|
||||||
'me',
|
|
||||||
'boostModal',
|
|
||||||
'autoPlayGif',
|
|
||||||
'muted',
|
'muted',
|
||||||
'hidden',
|
'hidden',
|
||||||
]
|
]
|
||||||
|
@ -200,7 +194,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,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' },
|
||||||
|
@ -50,7 +51,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,
|
||||||
};
|
};
|
||||||
|
@ -59,7 +59,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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -119,7 +118,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;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { initMuteModal } from '../actions/mutes';
|
import { initMuteModal } from '../actions/mutes';
|
||||||
|
import { unfollowModal } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
|
@ -23,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;
|
||||||
|
@ -34,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),
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,23 +4,18 @@ 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 initialState = JSON.parse(document.getElementById('initial-state').textContent);
|
|
||||||
try {
|
|
||||||
initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
|
|
||||||
} catch (e) {
|
|
||||||
initialState.local_settings = {};
|
|
||||||
}
|
|
||||||
const hydrateAction = hydrateStore(initialState);
|
const hydrateAction = hydrateStore(initialState);
|
||||||
store.dispatch(hydrateAction);
|
store.dispatch(hydrateAction);
|
||||||
|
|
||||||
|
|
|
@ -17,20 +17,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 = () => {
|
||||||
|
@ -38,10 +36,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;
|
||||||
|
@ -61,7 +55,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 }));
|
||||||
|
@ -90,7 +84,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', {
|
||||||
|
@ -126,11 +120,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) {
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}' },
|
||||||
|
@ -28,7 +29,6 @@ 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,
|
||||||
|
@ -47,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 = '';
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Motion from '../../ui/util/optional_motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { autoPlayGif, me } from '../../../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
|
@ -17,19 +17,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 = {
|
||||||
|
@ -47,7 +38,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 (
|
||||||
|
@ -74,20 +65,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;
|
||||||
|
@ -127,7 +115,7 @@ export default class Header extends ImmutablePureComponent {
|
||||||
return (
|
return (
|
||||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar account={account} 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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -10,7 +10,6 @@ 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,
|
||||||
|
@ -66,7 +65,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 />;
|
||||||
|
@ -76,13 +75,11 @@ export default class Header extends ImmutablePureComponent {
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
<InnerHeader
|
<InnerHeader
|
||||||
account={account}
|
account={account}
|
||||||
me={me}
|
|
||||||
onFollow={this.handleFollow}
|
onFollow={this.handleFollow}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionBar
|
<ActionBar
|
||||||
account={account}
|
account={account}
|
||||||
me={me}
|
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
onReblogToggle={this.handleReblogToggle}
|
onReblogToggle={this.handleReblogToggle}
|
||||||
|
|
|
@ -15,6 +15,7 @@ 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' },
|
||||||
|
@ -27,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;
|
||||||
|
@ -38,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),
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -45,7 +45,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,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}
|
||||||
|
|
|
@ -23,7 +23,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']),
|
||||||
settings: state.get('local_settings'),
|
settings: state.get('local_settings'),
|
||||||
filesAttached: state.getIn(['compose', 'media_attachments']).size > 0,
|
filesAttached: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
|
|
|
@ -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]),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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');
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,5 +57,21 @@ describe('emoji', () => {
|
||||||
it('does an emoji whose filename is irregular', () => {
|
it('does an emoji whose filename is irregular', () => {
|
||||||
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('avoid emojifying on invisible text', () => {
|
||||||
|
expect(emojify('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>'))
|
||||||
|
.toEqual('<a href="http://example.com/test%F0%9F%98%84"><span class="invisible">http://</span><span class="ellipsis">example.com/te</span><span class="invisible">st😄</span></a>');
|
||||||
|
expect(emojify('<span class="invisible">:luigi:</span>', { ':luigi:': { static_url: 'luigi.exe' } }))
|
||||||
|
.toEqual('<span class="invisible">:luigi:</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('avoid emojifying on invisible text with nested tags', () => {
|
||||||
|
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||||
|
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||||
|
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||||
|
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||||
|
expect(emojify('<span class="invisible">😄<br/>😴</span>😇'))
|
||||||
|
.toEqual('<span class="invisible">😄<br/>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg" />');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { autoPlayGif } from '../../initial_state';
|
||||||
import unicodeMapping from './emoji_unicode_mapping_light';
|
import unicodeMapping from './emoji_unicode_mapping_light';
|
||||||
import Trie from 'substring-trie';
|
import Trie from 'substring-trie';
|
||||||
|
|
||||||
|
@ -5,13 +6,13 @@ const trie = new Trie(Object.keys(unicodeMapping));
|
||||||
|
|
||||||
const assetHost = process.env.CDN_HOST || '';
|
const assetHost = process.env.CDN_HOST || '';
|
||||||
|
|
||||||
let allowAnimations = false;
|
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}) => {
|
const emojify = (str, customEmojis = {}) => {
|
||||||
let rtn = '';
|
const tagCharsWithoutEmojis = '<&';
|
||||||
|
const tagCharsWithEmojis = Object.keys(customEmojis).length ? '<&:' : '<&';
|
||||||
|
let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
let match, i = 0, tag;
|
let match, i = 0, tag;
|
||||||
while (i < str.length && (tag = '<&:'.indexOf(str[i])) === -1 && !(match = trie.search(str.slice(i)))) {
|
while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) {
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
||||||
}
|
}
|
||||||
let rend, replacement = '';
|
let rend, replacement = '';
|
||||||
|
@ -27,7 +28,7 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
// now got a replacee as ':shortname:'
|
// now got a replacee as ':shortname:'
|
||||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
// if you want additional emoji handler, add statements below which set replacement and return true.
|
||||||
if (shortname in customEmojis) {
|
if (shortname in customEmojis) {
|
||||||
const filename = allowAnimations ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
||||||
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -35,7 +36,26 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
})()) rend = ++i;
|
})()) rend = ++i;
|
||||||
} else if (tag >= 0) { // <, &
|
} else if (tag >= 0) { // <, &
|
||||||
rend = str.indexOf('>;'[tag], i + 1) + 1;
|
rend = str.indexOf('>;'[tag], i + 1) + 1;
|
||||||
if (!rend) break;
|
if (!rend) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (tag === 0) {
|
||||||
|
if (invisible) {
|
||||||
|
if (str[i + 1] === '/') { // closing tag
|
||||||
|
if (!--invisible) {
|
||||||
|
tagChars = tagCharsWithEmojis;
|
||||||
|
}
|
||||||
|
} else if (str[rend - 2] !== '/') { // opening tag
|
||||||
|
invisible++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (str.startsWith('<span class="invisible">', i)) {
|
||||||
|
// avoid emojifying on invisible text
|
||||||
|
invisible = 1;
|
||||||
|
tagChars = tagCharsWithoutEmojis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
i = rend;
|
i = rend;
|
||||||
} else { // matched to unicode emoji
|
} else { // matched to unicode emoji
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
const { filename, shortCode } = unicodeMapping[match];
|
||||||
|
@ -51,14 +71,12 @@ const emojify = (str, customEmojis = {}) => {
|
||||||
|
|
||||||
export default emojify;
|
export default emojify;
|
||||||
|
|
||||||
export const buildCustomEmojis = (customEmojis, overrideAllowAnimations = false) => {
|
export const buildCustomEmojis = (customEmojis) => {
|
||||||
const emojis = [];
|
const emojis = [];
|
||||||
|
|
||||||
allowAnimations = overrideAllowAnimations;
|
|
||||||
|
|
||||||
customEmojis.forEach(emoji => {
|
customEmojis.forEach(emoji => {
|
||||||
const shortcode = emoji.get('shortcode');
|
const shortcode = emoji.get('shortcode');
|
||||||
const url = allowAnimations ? emoji.get('url') : emoji.get('static_url');
|
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
|
||||||
const name = shortcode.replace(':', '');
|
const name = shortcode.replace(':', '');
|
||||||
|
|
||||||
emojis.push({
|
emojis.push({
|
||||||
|
|
|
@ -64,14 +64,14 @@ Object.keys(emojiMap).forEach(key => {
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
Object.keys(emojiIndex.emojis).forEach(key => {
|
||||||
const { native } = emojiIndex.emojis[key];
|
const { native } = emojiIndex.emojis[key];
|
||||||
const { short_names, search, unified } = emojiMartData.emojis[key];
|
let { short_names, search, unified } = emojiMartData.emojis[key];
|
||||||
if (short_names[0] !== key) {
|
if (short_names[0] !== key) {
|
||||||
throw new Error('The compresser expects the first short_code to be the ' +
|
throw new Error('The compresser expects the first short_code to be the ' +
|
||||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
'key. It may need to be rewritten if the emoji change such that this ' +
|
||||||
'is no longer the case.');
|
'is no longer the case.');
|
||||||
}
|
}
|
||||||
|
|
||||||
short_names.splice(0, 1); // first short name can be inferred from the key
|
short_names = short_names.slice(1); // first short name can be inferred from the key
|
||||||
|
|
||||||
const searchData = [native, short_names, search];
|
const searchData = [native, short_names, search];
|
||||||
if (unicodeToUnifiedName(native) !== unified) {
|
if (unicodeToUnifiedName(native) !== unified) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import { fetchFavourites } from '../../actions/interactions';
|
import { fetchFavourites } from '../../actions/interactions';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
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 AccountAuthorizeContainer from './containers/account_authorize_container';
|
import AccountAuthorizeContainer from './containers/account_authorize_container';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
fetchFollowers,
|
fetchFollowers,
|
||||||
expandFollowers,
|
expandFollowers,
|
||||||
} from '../../actions/accounts';
|
} from '../../actions/accounts';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
fetchFollowing,
|
fetchFollowing,
|
||||||
expandFollowing,
|
expandFollowing,
|
||||||
} from '../../actions/accounts';
|
} from '../../actions/accounts';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import HeaderContainer from '../account_timeline/containers/header_container';
|
import HeaderContainer from '../account_timeline/containers/header_container';
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { openModal } from '../../actions/modal';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { me } from '../../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
|
||||||
|
@ -31,7 +32,7 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
|
myAccount: state.getIn(['accounts', me]),
|
||||||
columns: state.getIn(['settings', 'columns']),
|
columns: state.getIn(['settings', 'columns']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
me: ImmutablePropTypes.map.isRequired,
|
myAccount: ImmutablePropTypes.map.isRequired,
|
||||||
columns: ImmutablePropTypes.list,
|
columns: ImmutablePropTypes.list,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
@ -57,7 +58,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, me, columns, multiColumn } = this.props;
|
const { intl, myAccount, columns, multiColumn } = this.props;
|
||||||
|
|
||||||
let navItems = [];
|
let navItems = [];
|
||||||
|
|
||||||
|
@ -88,7 +89,7 @@ export default class GettingStarted extends ImmutablePureComponent {
|
||||||
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
<ColumnLink key='6' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (me.get('locked')) {
|
if (myAccount.get('locked')) {
|
||||||
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
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';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import { fetchReblogs } from '../../actions/interactions';
|
import { fetchReblogs } from '../../actions/interactions';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import AccountContainer from '../../containers/account_container';
|
import AccountContainer from '../../containers/account_container';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
|
|
|
@ -4,6 +4,7 @@ import IconButton from '../../../components/icon_button';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
|
@ -36,7 +37,6 @@ export default class ActionBar extends React.PureComponent {
|
||||||
onReport: PropTypes.func,
|
onReport: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
me: PropTypes.string.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ export default class ActionBar extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, me, intl } = this.props;
|
const { status, intl } = this.props;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
onOpenVideo: PropTypes.func.isRequired,
|
||||||
autoPlayGif: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
|
@ -76,7 +75,6 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
||||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
||||||
height={250}
|
height={250}
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
onOpenMedia={this.props.onOpenMedia}
|
||||||
autoPlayGif={this.props.autoPlayGif}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
mediaIcon = 'picture-o';
|
mediaIcon = 'picture-o';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { fetchStatus } from '../../actions/statuses';
|
import { fetchStatus } from '../../actions/statuses';
|
||||||
import MissingIndicator from '../../components/missing_indicator';
|
import MissingIndicator from '../../components/missing_indicator';
|
||||||
|
@ -22,13 +23,15 @@ import {
|
||||||
import { deleteStatus } from '../../actions/statuses';
|
import { deleteStatus } from '../../actions/statuses';
|
||||||
import { initReport } from '../../actions/reports';
|
import { initReport } from '../../actions/reports';
|
||||||
import { makeGetStatus } from '../../selectors';
|
import { makeGetStatus } from '../../selectors';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll-4';
|
||||||
import ColumnBackButton from '../../components/column_back_button';
|
import ColumnBackButton from '../../components/column_back_button';
|
||||||
import StatusContainer from '../../../glitch/components/status/container';
|
import StatusContainer from '../../../glitch/components/status/container';
|
||||||
import { openModal } from '../../actions/modal';
|
import { openModal } from '../../actions/modal';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
import { boostModal, deleteModal } from '../../initial_state';
|
||||||
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../../features/ui/util/fullscreen';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
|
@ -43,10 +46,6 @@ const makeMapStateToProps = () => {
|
||||||
settings: state.get('local_settings'),
|
settings: state.get('local_settings'),
|
||||||
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
|
ancestorsIds: state.getIn(['contexts', 'ancestors', props.params.statusId]),
|
||||||
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
|
descendantsIds: state.getIn(['contexts', 'descendants', props.params.statusId]),
|
||||||
me: state.getIn(['meta', 'me']),
|
|
||||||
boostModal: state.getIn(['meta', 'boost_modal']),
|
|
||||||
deleteModal: state.getIn(['meta', 'delete_modal']),
|
|
||||||
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
|
@ -67,17 +66,21 @@ export default class Status extends ImmutablePureComponent {
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
ancestorsIds: ImmutablePropTypes.list,
|
ancestorsIds: ImmutablePropTypes.list,
|
||||||
descendantsIds: ImmutablePropTypes.list,
|
descendantsIds: ImmutablePropTypes.list,
|
||||||
me: PropTypes.string,
|
|
||||||
boostModal: PropTypes.bool,
|
|
||||||
deleteModal: PropTypes.bool,
|
|
||||||
autoPlayGif: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
fullscreen: false,
|
||||||
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
this.props.dispatch(fetchStatus(this.props.params.statusId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
attachFullscreenListener(this.onFullScreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps (nextProps) {
|
||||||
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
|
||||||
this._scrolledIntoView = false;
|
this._scrolledIntoView = false;
|
||||||
|
@ -113,7 +116,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
this.props.dispatch(unreblog(status));
|
this.props.dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
if (e.shiftKey || !this.props.boostModal) {
|
if (e.shiftKey || !boostModal) {
|
||||||
this.handleModalReblog(status);
|
this.handleModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
this.props.dispatch(openModal('BOOST', { status, onReblog: this.handleModalReblog }));
|
||||||
|
@ -124,7 +127,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
handleDeleteClick = (status) => {
|
handleDeleteClick = (status) => {
|
||||||
const { dispatch, intl } = this.props;
|
const { dispatch, intl } = this.props;
|
||||||
|
|
||||||
if (!this.props.deleteModal) {
|
if (!deleteModal) {
|
||||||
dispatch(deleteStatus(status.get('id')));
|
dispatch(deleteStatus(status.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
|
@ -259,9 +262,18 @@ export default class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
detachFullscreenListener(this.onFullScreenChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFullScreenChange = () => {
|
||||||
|
this.setState({ fullscreen: isFullscreen() });
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let ancestors, descendants;
|
let ancestors, descendants;
|
||||||
const { status, settings, ancestorsIds, descendantsIds, me, autoPlayGif } = this.props;
|
const { status, settings, ancestorsIds, descendantsIds } = this.props;
|
||||||
|
const { fullscreen } = this.state;
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return (
|
return (
|
||||||
|
@ -295,7 +307,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
<ColumnBackButton />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<ScrollContainer scrollKey='thread'>
|
<ScrollContainer scrollKey='thread'>
|
||||||
<div className='scrollable detailed-status__wrapper' ref={this.setRef}>
|
<div className={classNames('scrollable', 'detailed-status__wrapper', { fullscreen })} ref={this.setRef}>
|
||||||
{ancestors}
|
{ancestors}
|
||||||
|
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
|
@ -303,15 +315,12 @@ export default class Status extends ImmutablePureComponent {
|
||||||
<DetailedStatus
|
<DetailedStatus
|
||||||
status={status}
|
status={status}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
autoPlayGif={autoPlayGif}
|
|
||||||
me={me}
|
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionBar
|
<ActionBar
|
||||||
status={status}
|
status={status}
|
||||||
me={me}
|
|
||||||
onReply={this.handleReplyClick}
|
onReply={this.handleReplyClick}
|
||||||
onFavourite={this.handleFavouriteClick}
|
onFavourite={this.handleFavouriteClick}
|
||||||
onReblog={this.handleReblogClick}
|
onReblog={this.handleReblogClick}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
import Button from '../../../components/button';
|
import Button from '../../../components/button';
|
||||||
import { closeModal } from '../../../actions/modal';
|
import { closeModal } from '../../../actions/modal';
|
||||||
import { muteAccount } from '../../../actions/accounts';
|
import { muteAccount } from '../../../actions/accounts';
|
||||||
|
@ -80,12 +81,13 @@ export default class MuteModal extends React.PureComponent {
|
||||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div>
|
||||||
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||||
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||||
<input id='mute-modal__hide-notifications-checkbox' type='checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
{' '}
|
||||||
|
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='mute-modal__action-bar'>
|
<div className='mute-modal__action-bar'>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
List as ImmutableList,
|
List as ImmutableList,
|
||||||
Map as ImmutableMap,
|
Map as ImmutableMap,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
const noop = () => { };
|
const noop = () => { };
|
||||||
|
|
||||||
|
@ -43,11 +44,11 @@ PageOne.propTypes = {
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageTwo = ({ me }) => (
|
const PageTwo = ({ myAccount }) => (
|
||||||
<div className='onboarding-modal__page onboarding-modal__page-two'>
|
<div className='onboarding-modal__page onboarding-modal__page-two'>
|
||||||
<div className='figure non-interactive'>
|
<div className='figure non-interactive'>
|
||||||
<div className='pseudo-drawer'>
|
<div className='pseudo-drawer'>
|
||||||
<NavigationBar onClose={noop} account={me} />
|
<NavigationBar onClose={noop} account={myAccount} />
|
||||||
</div>
|
</div>
|
||||||
<ComposeForm
|
<ComposeForm
|
||||||
text='Awoo! #introductions'
|
text='Awoo! #introductions'
|
||||||
|
@ -73,10 +74,10 @@ const PageTwo = ({ me }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
PageTwo.propTypes = {
|
PageTwo.propTypes = {
|
||||||
me: ImmutablePropTypes.map.isRequired,
|
myAccount: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageThree = ({ me }) => (
|
const PageThree = ({ myAccount }) => (
|
||||||
<div className='onboarding-modal__page onboarding-modal__page-three'>
|
<div className='onboarding-modal__page onboarding-modal__page-three'>
|
||||||
<div className='figure non-interactive'>
|
<div className='figure non-interactive'>
|
||||||
<Search
|
<Search
|
||||||
|
@ -88,7 +89,7 @@ const PageThree = ({ me }) => (
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='pseudo-drawer'>
|
<div className='pseudo-drawer'>
|
||||||
<NavigationBar onClose={noop} account={me} />
|
<NavigationBar onClose={noop} account={myAccount} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -98,7 +99,7 @@ const PageThree = ({ me }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
PageThree.propTypes = {
|
PageThree.propTypes = {
|
||||||
me: ImmutablePropTypes.map.isRequired,
|
myAccount: ImmutablePropTypes.map.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PageFour = ({ domain, intl }) => (
|
const PageFour = ({ domain, intl }) => (
|
||||||
|
@ -166,7 +167,7 @@ PageSix.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
me: state.getIn(['accounts', state.getIn(['meta', 'me'])]),
|
myAccount: state.getIn(['accounts', me]),
|
||||||
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
|
admin: state.getIn(['accounts', state.getIn(['meta', 'admin'])]),
|
||||||
domain: state.getIn(['meta', 'domain']),
|
domain: state.getIn(['meta', 'domain']),
|
||||||
});
|
});
|
||||||
|
@ -178,7 +179,7 @@ export default class OnboardingModal extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
me: ImmutablePropTypes.map.isRequired,
|
myAccount: ImmutablePropTypes.map.isRequired,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
admin: ImmutablePropTypes.map,
|
admin: ImmutablePropTypes.map,
|
||||||
};
|
};
|
||||||
|
@ -188,11 +189,11 @@ export default class OnboardingModal extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { me, admin, domain, intl } = this.props;
|
const { myAccount, admin, domain, intl } = this.props;
|
||||||
this.pages = [
|
this.pages = [
|
||||||
<PageOne acct={me.get('acct')} domain={domain} />,
|
<PageOne acct={myAccount.get('acct')} domain={domain} />,
|
||||||
<PageTwo me={me} />,
|
<PageTwo myAccount={myAccount} />,
|
||||||
<PageThree me={me} />,
|
<PageThree myAccount={myAccount} />,
|
||||||
<PageFour domain={domain} intl={intl} />,
|
<PageFour domain={domain} intl={intl} />,
|
||||||
<PageSix admin={admin} domain={domain} />,
|
<PageSix admin={admin} domain={domain} />,
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,13 +4,13 @@ import { scrollTopTimeline } from '../../../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { me } from '../../../initial_state';
|
||||||
|
|
||||||
const makeGetStatusIds = () => createSelector([
|
const makeGetStatusIds = () => createSelector([
|
||||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||||
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
(state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()),
|
||||||
(state) => state.get('statuses'),
|
(state) => state.get('statuses'),
|
||||||
(state) => state.getIn(['meta', 'me']),
|
], (columnSettings, statusIds, statuses) => {
|
||||||
], (columnSettings, statusIds, statuses, me) => {
|
|
||||||
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
|
const rawRegex = columnSettings.getIn(['regex', 'body'], '').trim();
|
||||||
let regex = null;
|
let regex = null;
|
||||||
|
|
||||||
|
|
|
@ -40,18 +40,20 @@ import {
|
||||||
PinnedStatuses,
|
PinnedStatuses,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
import { me } from '../../initial_state';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
import '../../../glitch/components/status';
|
import '../../../glitch/components/status';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
beforeUnload: { id: 'ui.beforeunload', defaultMessage: 'Your draft will be lost if you leave Mastodon.' },
|
||||||
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
systemFontUi: state.getIn(['meta', 'system_font_ui']),
|
|
||||||
layout: state.getIn(['local_settings', 'layout']),
|
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
|
||||||
navbarUnder: state.getIn(['local_settings', 'navbar_under']),
|
|
||||||
me: state.getIn(['meta', 'me']),
|
|
||||||
isComposing: state.getIn(['compose', 'is_composing']),
|
isComposing: state.getIn(['compose', 'is_composing']),
|
||||||
|
hasComposingText: state.getIn(['compose', 'text']) !== '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
|
@ -82,6 +84,7 @@ const keyMap = {
|
||||||
};
|
};
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@injectIntl
|
||||||
@withRouter
|
@withRouter
|
||||||
export default class UI extends React.Component {
|
export default class UI extends React.Component {
|
||||||
|
|
||||||
|
@ -97,8 +100,9 @@ export default class UI extends React.Component {
|
||||||
systemFontUi: PropTypes.bool,
|
systemFontUi: PropTypes.bool,
|
||||||
navbarUnder: PropTypes.bool,
|
navbarUnder: PropTypes.bool,
|
||||||
isComposing: PropTypes.bool,
|
isComposing: PropTypes.bool,
|
||||||
me: PropTypes.string,
|
hasComposingText: PropTypes.bool,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -106,6 +110,17 @@ export default class UI extends React.Component {
|
||||||
draggingOver: false,
|
draggingOver: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleBeforeUnload = (e) => {
|
||||||
|
const { intl, isComposing, hasComposingText } = this.props;
|
||||||
|
|
||||||
|
if (isComposing && hasComposingText) {
|
||||||
|
// Setting returnValue to any string causes confirmation dialog.
|
||||||
|
// Many browsers no longer display this text to users,
|
||||||
|
// but we set user-friendly message for other browsers, e.g. Edge.
|
||||||
|
e.returnValue = intl.formatMessage(messages.beforeUnload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleResize = debounce(() => {
|
handleResize = debounce(() => {
|
||||||
// The cached heights are no longer accurate, invalidate
|
// The cached heights are no longer accurate, invalidate
|
||||||
this.props.dispatch(clearHeight());
|
this.props.dispatch(clearHeight());
|
||||||
|
@ -180,6 +195,7 @@ export default class UI extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
|
@ -222,6 +238,7 @@ export default class UI extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
@ -321,7 +338,7 @@ export default class UI extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHotkeyGoToProfile = () => {
|
handleHotkeyGoToProfile = () => {
|
||||||
this.context.router.history.push(`/accounts/${this.props.me}`);
|
this.context.router.history.push(`/accounts/${me}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHotkeyGoToBlocked = () => {
|
handleHotkeyGoToBlocked = () => {
|
||||||
|
|
|
@ -1,56 +1,5 @@
|
||||||
// Like react-motion's Motion, but checks to see if the user prefers
|
import { reduceMotion } from '../../../initial_state';
|
||||||
// reduced motion and uses a cross-fade in those cases.
|
import ReducedMotion from './reduced_motion';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from 'react-motion/lib/Motion';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const stylesToKeep = ['opacity', 'backgroundOpacity'];
|
export default reduceMotion ? ReducedMotion : Motion;
|
||||||
|
|
||||||
let reduceMotion;
|
|
||||||
|
|
||||||
const extractValue = (value) => {
|
|
||||||
// This is either an object with a "val" property or it's a number
|
|
||||||
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
class OptionalMotion extends React.Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
defaultStyle: PropTypes.object,
|
|
||||||
style: PropTypes.object,
|
|
||||||
children: PropTypes.func,
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const { style, defaultStyle, children } = this.props;
|
|
||||||
|
|
||||||
if (typeof reduceMotion !== 'boolean') {
|
|
||||||
// This never changes without a page reload, so we can just grab it
|
|
||||||
// once from the body classes as opposed to using Redux's connect(),
|
|
||||||
// which would unnecessarily update every state change
|
|
||||||
reduceMotion = document.body.classList.contains('reduce-motion');
|
|
||||||
}
|
|
||||||
if (reduceMotion) {
|
|
||||||
Object.keys(style).forEach(key => {
|
|
||||||
if (stylesToKeep.includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If it's setting an x or height or scale or some other value, we need
|
|
||||||
// to preserve the end-state value without actually animating it
|
|
||||||
style[key] = defaultStyle[key] = extractValue(style[key]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Motion style={style} defaultStyle={defaultStyle}>
|
|
||||||
{children}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default OptionalMotion;
|
|
||||||
|
|
|
@ -7,11 +7,19 @@ import BundleColumnError from '../components/bundle_column_error';
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Small wrapper to pass multiColumn to the route components
|
||||||
export const WrappedSwitch = ({ multiColumn, children }) => (
|
export class WrappedSwitch extends React.PureComponent {
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { multiColumn, children } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
|
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
WrappedSwitch.propTypes = {
|
WrappedSwitch.propTypes = {
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Like react-motion's Motion, but reduces all animations to cross-fades
|
||||||
|
// for the benefit of users with motion sickness.
|
||||||
|
import React from 'react';
|
||||||
|
import Motion from 'react-motion/lib/Motion';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const stylesToKeep = ['opacity', 'backgroundOpacity'];
|
||||||
|
|
||||||
|
const extractValue = (value) => {
|
||||||
|
// This is either an object with a "val" property or it's a number
|
||||||
|
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ReducedMotion extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
defaultStyle: PropTypes.object,
|
||||||
|
style: PropTypes.object,
|
||||||
|
children: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const { style, defaultStyle, children } = this.props;
|
||||||
|
|
||||||
|
Object.keys(style).forEach(key => {
|
||||||
|
if (stylesToKeep.includes(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If it's setting an x or height or scale or some other value, we need
|
||||||
|
// to preserve the end-state value without actually animating it
|
||||||
|
style[key] = defaultStyle[key] = extractValue(style[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Motion style={style} defaultStyle={defaultStyle}>
|
||||||
|
{children}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReducedMotion;
|
|
@ -0,0 +1,21 @@
|
||||||
|
const element = document.getElementById('initial-state');
|
||||||
|
const initialState = element && function () {
|
||||||
|
const result = JSON.parse(element.textContent);
|
||||||
|
try {
|
||||||
|
result.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
|
||||||
|
} catch (e) {
|
||||||
|
result.local_settings = {};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
|
||||||
|
const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop];
|
||||||
|
|
||||||
|
export const reduceMotion = getMeta('reduce_motion');
|
||||||
|
export const autoPlayGif = getMeta('auto_play_gif');
|
||||||
|
export const unfollowModal = getMeta('unfollow_modal');
|
||||||
|
export const boostModal = getMeta('boost_modal');
|
||||||
|
export const deleteModal = getMeta('delete_modal');
|
||||||
|
export const me = getMeta('me');
|
||||||
|
|
||||||
|
export default initialState;
|
|
@ -63,7 +63,7 @@
|
||||||
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
"confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?",
|
||||||
"confirmations.unfollow.confirm": "Unfollow",
|
"confirmations.unfollow.confirm": "Unfollow",
|
||||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||||
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 퍼가세요.",
|
"embed.instructions": "아래의 코드를 복사하여 대화를 원하는 곳으로 공유하세요.",
|
||||||
"embed.preview": "다음과 같이 표시됩니다:",
|
"embed.preview": "다음과 같이 표시됩니다:",
|
||||||
"emoji_button.activity": "활동",
|
"emoji_button.activity": "활동",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "Custom",
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"column.favourites": "Favorits",
|
"column.favourites": "Favorits",
|
||||||
"column.follow_requests": "Demandas d’abonament",
|
"column.follow_requests": "Demandas d’abonament",
|
||||||
"column.home": "Acuèlh",
|
"column.home": "Acuèlh",
|
||||||
"column.mutes": "Personas en silenci",
|
"column.mutes": "Personas rescondudas",
|
||||||
"column.notifications": "Notificacions",
|
"column.notifications": "Notificacions",
|
||||||
"column.pins": "Tuts penjats",
|
"column.pins": "Tuts penjats",
|
||||||
"column.public": "Flux public global",
|
"column.public": "Flux public global",
|
||||||
|
@ -55,12 +55,12 @@
|
||||||
"confirmation_modal.cancel": "Anullar",
|
"confirmation_modal.cancel": "Anullar",
|
||||||
"confirmations.block.confirm": "Blocar",
|
"confirmations.block.confirm": "Blocar",
|
||||||
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
|
"confirmations.block.message": "Sètz segur de voler blocar {name} ?",
|
||||||
"confirmations.delete.confirm": "Suprimir",
|
"confirmations.delete.confirm": "Escafar",
|
||||||
"confirmations.delete.message": "Sètz segur de voler suprimir l’estatut ?",
|
"confirmations.delete.message": "Sètz segur de voler escafar l’estatut ?",
|
||||||
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
|
"confirmations.domain_block.confirm": "Amagar tot lo domeni",
|
||||||
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
"confirmations.domain_block.message": "Sètz segur segur de voler blocar completament {domain} ? De còps cal pas que blocar o rescondre unas personas solament.",
|
||||||
"confirmations.mute.confirm": "Metre en silenci",
|
"confirmations.mute.confirm": "Rescondre",
|
||||||
"confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?",
|
"confirmations.mute.message": "Sètz segur de voler rescondre {name} ?",
|
||||||
"confirmations.unfollow.confirm": "Quitar de sègre",
|
"confirmations.unfollow.confirm": "Quitar de sègre",
|
||||||
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
|
"confirmations.unfollow.message": "Volètz vertadièrament quitar de sègre {name} ?",
|
||||||
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
|
"embed.instructions": "Embarcar aqueste estatut per lo far veire sus un site Internet en copiar lo còdi çai-jos.",
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
|
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
|
||||||
"onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
|
"onboarding.page_four.home": "Lo flux d’acuèlh mòstra los estatuts del mond que seguètz.",
|
||||||
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
|
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un interagís amb vos",
|
||||||
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
|
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per construire un malhum mai larg. Òm los apèla instàncias.",
|
||||||
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
|
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
|
||||||
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
|
"onboarding.page_one.welcome": "Benvengut a Mastodon !",
|
||||||
"onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
|
"onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.",
|
||||||
|
@ -159,11 +159,11 @@
|
||||||
"privacy.public.short": "Public",
|
"privacy.public.short": "Public",
|
||||||
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
|
"privacy.unlisted.long": "Mostrar pas dins los fluxes publics",
|
||||||
"privacy.unlisted.short": "Pas-listat",
|
"privacy.unlisted.short": "Pas-listat",
|
||||||
"relative_time.days": "fa {number}j",
|
"relative_time.days": "fa {number} d",
|
||||||
"relative_time.hours": "fa {number} h",
|
"relative_time.hours": "fa {number} h",
|
||||||
"relative_time.just_now": "ara",
|
"relative_time.just_now": "ara",
|
||||||
"relative_time.minutes": "fa {number} minutas",
|
"relative_time.minutes": "fa {number} min",
|
||||||
"relative_time.seconds": "fa {number} segondas",
|
"relative_time.seconds": "fa {number} s",
|
||||||
"reply_indicator.cancel": "Anullar",
|
"reply_indicator.cancel": "Anullar",
|
||||||
"report.placeholder": "Comentaris addicionals",
|
"report.placeholder": "Comentaris addicionals",
|
||||||
"report.submit": "Mandar",
|
"report.submit": "Mandar",
|
||||||
|
@ -197,7 +197,7 @@
|
||||||
"status.share": "Partejar",
|
"status.share": "Partejar",
|
||||||
"status.show_less": "Tornar plegar",
|
"status.show_less": "Tornar plegar",
|
||||||
"status.show_more": "Desplegar",
|
"status.show_more": "Desplegar",
|
||||||
"status.unmute_conversation": "Conversacions amb silenci levat",
|
"status.unmute_conversation": "Tornar mostrar la conversacion",
|
||||||
"status.unpin": "Tirar del perfil",
|
"status.unpin": "Tirar del perfil",
|
||||||
"tabs_bar.compose": "Compausar",
|
"tabs_bar.compose": "Compausar",
|
||||||
"tabs_bar.federated_timeline": "Flux public global",
|
"tabs_bar.federated_timeline": "Flux public global",
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
"follow_request.reject": "Odrzuć",
|
"follow_request.reject": "Odrzuć",
|
||||||
"getting_started.appsshort": "Aplikacje",
|
"getting_started.appsshort": "Aplikacje",
|
||||||
"getting_started.faq": "FAQ",
|
"getting_started.faq": "FAQ",
|
||||||
"getting_started.heading": "Naucz się korzystać",
|
"getting_started.heading": "Rozpocznij",
|
||||||
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
|
"getting_started.open_source_notice": "Mastodon jest oprogramowaniem o otwartym źródle. Możesz pomóc w rozwoju lub zgłaszać błędy na GitHubie tutaj: {github}.",
|
||||||
"getting_started.userguide": "Podręcznik użytkownika",
|
"getting_started.userguide": "Podręcznik użytkownika",
|
||||||
"home.column_settings.advanced": "Zaawansowane",
|
"home.column_settings.advanced": "Zaawansowane",
|
||||||
|
@ -174,7 +174,7 @@
|
||||||
"search_popout.tips.status": "wpis",
|
"search_popout.tips.status": "wpis",
|
||||||
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
|
"search_popout.tips.text": "Proste wyszukiwanie pasujących pseudonimów, nazw użytkowników i hashtagów",
|
||||||
"search_popout.tips.user": "użytkownik",
|
"search_popout.tips.user": "użytkownik",
|
||||||
"search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}",
|
"search_results.total": "{count, number} {count, plural, one {wynik} few {wyniki} many {wyników} more {wyników}}",
|
||||||
"standalone.public_title": "Spojrzenie w głąb…",
|
"standalone.public_title": "Spojrzenie w głąb…",
|
||||||
"status.cannot_reblog": "Ten wpis nie może zostać podbity",
|
"status.cannot_reblog": "Ten wpis nie może zostać podbity",
|
||||||
"status.delete": "Usuń",
|
"status.delete": "Usuń",
|
||||||
|
|
|
@ -161,7 +161,7 @@
|
||||||
"privacy.unlisted.short": "Não listada",
|
"privacy.unlisted.short": "Não listada",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number}d",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number}h",
|
||||||
"relative_time.just_now": "now",
|
"relative_time.just_now": "agora",
|
||||||
"relative_time.minutes": "{number}m",
|
"relative_time.minutes": "{number}m",
|
||||||
"relative_time.seconds": "{number}s",
|
"relative_time.seconds": "{number}s",
|
||||||
"reply_indicator.cancel": "Cancelar",
|
"reply_indicator.cancel": "Cancelar",
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
{
|
{
|
||||||
"account.block": "屏蔽 @{name}",
|
"account.block": "屏蔽 @{name}",
|
||||||
"account.block_domain": "隐藏一切来自 {domain} 的嘟文",
|
"account.block_domain": "隐藏一切来自 {domain} 的嘟文",
|
||||||
"account.disclaimer_full": "下列资料不一定完整。",
|
"account.disclaimer_full": "此处显示的信息可能不是全部内容。",
|
||||||
"account.edit_profile": "修改个人资料",
|
"account.edit_profile": "修改个人资料",
|
||||||
"account.follow": "关注",
|
"account.follow": "关注",
|
||||||
"account.followers": "关注者",
|
"account.followers": "关注者",
|
||||||
"account.follows": "正关注",
|
"account.follows": "正在关注",
|
||||||
"account.follows_you": "关注你",
|
"account.follows_you": "关注了你",
|
||||||
"account.media": "媒体",
|
"account.media": "媒体",
|
||||||
"account.mention": "提及 @{name}",
|
"account.mention": "提及 @{name}",
|
||||||
"account.mute": "将 @{name} 静音",
|
"account.mute": "静音 @{name}",
|
||||||
"account.posts": "嘟文",
|
"account.posts": "嘟文",
|
||||||
"account.report": "举报 @{name}",
|
"account.report": "举报 @{name}",
|
||||||
"account.requested": "等待审批",
|
"account.requested": "正在等待对方同意。点击以取消发送关注请求",
|
||||||
"account.share": "分享 @{name} 的个人资料",
|
"account.share": "分享 @{name} 的个人资料",
|
||||||
"account.unblock": "解除对 @{name} 的屏蔽",
|
"account.unblock": "不再屏蔽 @{name}",
|
||||||
"account.unblock_domain": "不再隐藏 {domain}",
|
"account.unblock_domain": "不再隐藏 {domain}",
|
||||||
"account.unfollow": "取消关注",
|
"account.unfollow": "取消关注",
|
||||||
"account.unmute": "取消 @{name} 的静音",
|
"account.unmute": "不再静音 @{name}",
|
||||||
"account.view_full_profile": "查看完整资料",
|
"account.view_full_profile": "查看完整资料",
|
||||||
"boost_modal.combo": "如你想在下次路过时显示,请按{combo},",
|
"boost_modal.combo": "下次按住 {combo} 即可跳过此提示",
|
||||||
"bundle_column_error.body": "载入组件出错。",
|
"bundle_column_error.body": "载入组件出错。",
|
||||||
"bundle_column_error.retry": "重试",
|
"bundle_column_error.retry": "重试",
|
||||||
"bundle_column_error.title": "网络错误",
|
"bundle_column_error.title": "网络错误",
|
||||||
|
@ -37,72 +37,72 @@
|
||||||
"column.public": "跨站公共时间轴",
|
"column.public": "跨站公共时间轴",
|
||||||
"column_back_button.label": "返回",
|
"column_back_button.label": "返回",
|
||||||
"column_header.hide_settings": "隐藏设置",
|
"column_header.hide_settings": "隐藏设置",
|
||||||
"column_header.moveLeft_settings": "将栏左移",
|
"column_header.moveLeft_settings": "将此栏左移",
|
||||||
"column_header.moveRight_settings": "将栏右移",
|
"column_header.moveRight_settings": "将此栏右移",
|
||||||
"column_header.pin": "固定",
|
"column_header.pin": "固定",
|
||||||
"column_header.show_settings": "显示设置",
|
"column_header.show_settings": "显示设置",
|
||||||
"column_header.unpin": "取下",
|
"column_header.unpin": "取消固定",
|
||||||
"column_subheading.navigation": "导航",
|
"column_subheading.navigation": "导航",
|
||||||
"column_subheading.settings": "设置",
|
"column_subheading.settings": "设置",
|
||||||
"compose_form.lock_disclaimer": "你的帐户没 {locked}. 任何人可以通过关注你来查看只有关注者可见的嘟文.",
|
"compose_form.lock_disclaimer": "你的帐户没有{locked}。任何人都可以通过关注你来查看仅关注者可见的嘟文。",
|
||||||
"compose_form.lock_disclaimer.lock": "被保护",
|
"compose_form.lock_disclaimer.lock": "被保护",
|
||||||
"compose_form.placeholder": "在想啥?",
|
"compose_form.placeholder": "在想啥?",
|
||||||
"compose_form.publish": "嘟嘟",
|
"compose_form.publish": "嘟嘟",
|
||||||
"compose_form.publish_loud": "{publish}!",
|
"compose_form.publish_loud": "{publish}!",
|
||||||
"compose_form.sensitive": "将媒体文件标示为“敏感内容”",
|
"compose_form.sensitive": "将媒体文件标记为敏感内容",
|
||||||
"compose_form.spoiler": "将部分文本藏于警告消息之后",
|
"compose_form.spoiler": "折叠嘟文内容",
|
||||||
"compose_form.spoiler_placeholder": "敏感内容的警告消息",
|
"compose_form.spoiler_placeholder": "折叠部分的警告消息",
|
||||||
"confirmation_modal.cancel": "取消",
|
"confirmation_modal.cancel": "取消",
|
||||||
"confirmations.block.confirm": "屏蔽",
|
"confirmations.block.confirm": "屏蔽",
|
||||||
"confirmations.block.message": "想好了,真的要屏蔽 {name}?",
|
"confirmations.block.message": "想好了,真的要屏蔽 {name}?",
|
||||||
"confirmations.delete.confirm": "删除",
|
"confirmations.delete.confirm": "删除",
|
||||||
"confirmations.delete.message": "想好了,真的要删除这条嘟文?",
|
"confirmations.delete.message": "想好了,真的要删除这条嘟文?",
|
||||||
"confirmations.domain_block.confirm": "隐藏整个网站",
|
"confirmations.domain_block.confirm": "隐藏整个网站",
|
||||||
"confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain} ?多数情况下,封锁或静音几个特定目标就好。",
|
"confirmations.domain_block.message": "你真的真的确定要隐藏整个 {domain}?多数情况下,屏蔽或静音几个特定的用户就应该能满足你的需要了。",
|
||||||
"confirmations.mute.confirm": "静音",
|
"confirmations.mute.confirm": "静音",
|
||||||
"confirmations.mute.message": "想好了,真的要静音 {name}?",
|
"confirmations.mute.message": "想好了,真的要静音 {name}?",
|
||||||
"confirmations.unfollow.confirm": "取消关注",
|
"confirmations.unfollow.confirm": "取消关注",
|
||||||
"confirmations.unfollow.message": "确定要取消关注 {name}吗?",
|
"confirmations.unfollow.message": "确定要取消关注 {name} 吗?",
|
||||||
"embed.instructions": "要内嵌此嘟文,请将以下代码贴进你的网站。",
|
"embed.instructions": "要在你的网站上嵌入这条嘟文,请复制以下代码。",
|
||||||
"embed.preview": "到时大概长这样:",
|
"embed.preview": "它会像这样显示出来:",
|
||||||
"emoji_button.activity": "活动",
|
"emoji_button.activity": "活动",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "自定义",
|
||||||
"emoji_button.flags": "旗帜",
|
"emoji_button.flags": "旗帜",
|
||||||
"emoji_button.food": "食物和饮料",
|
"emoji_button.food": "食物和饮料",
|
||||||
"emoji_button.label": "加入表情符号",
|
"emoji_button.label": "加入表情符号",
|
||||||
"emoji_button.nature": "自然",
|
"emoji_button.nature": "自然",
|
||||||
"emoji_button.not_found": "No emojos!! (╯°□°)╯︵ ┻━┻",
|
"emoji_button.not_found": "木有这个表情符号!(╯°□°)╯︵ ┻━┻",
|
||||||
"emoji_button.objects": "物体",
|
"emoji_button.objects": "物体",
|
||||||
"emoji_button.people": "人物",
|
"emoji_button.people": "人物",
|
||||||
"emoji_button.recent": "Frequently used",
|
"emoji_button.recent": "常用",
|
||||||
"emoji_button.search": "搜索…",
|
"emoji_button.search": "搜索…",
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "搜索结果",
|
||||||
"emoji_button.symbols": "符号",
|
"emoji_button.symbols": "符号",
|
||||||
"emoji_button.travel": "旅途和地点",
|
"emoji_button.travel": "旅行和地点",
|
||||||
"empty_column.community": "本站时间轴暂时未有内容,快嘟几个来抢头香啊!",
|
"empty_column.community": "本站时间轴暂时没有内容,快嘟几个来抢头香啊!",
|
||||||
"empty_column.hashtag": "这个标签暂时未有内容。",
|
"empty_column.hashtag": "这个话题标签下暂时没有内容。",
|
||||||
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
|
"empty_column.home": "你还没有关注任何用户。快看看{public},向其他用户搭讪吧。",
|
||||||
"empty_column.home.public_timeline": "公共时间轴",
|
"empty_column.home.public_timeline": "公共时间轴",
|
||||||
"empty_column.notifications": "你没有任何通知纪录,快向其他用户搭讪吧。",
|
"empty_column.notifications": "你还没有收到过通知信息,快向其他用户搭讪吧。",
|
||||||
"empty_column.public": "跨站公共时间轴暂时没有内容!快写一些公共的嘟文,或者关注另一些服务器实例的用户吧!你和本站、友站的交流,将决定这里出现的内容。",
|
"empty_column.public": "这里神马都没有!写一些公开的嘟文,或者关注其他实例的用户,这里就会有嘟文出现了哦!",
|
||||||
"follow_request.authorize": "批准",
|
"follow_request.authorize": "同意",
|
||||||
"follow_request.reject": "拒绝",
|
"follow_request.reject": "拒绝",
|
||||||
"getting_started.appsshort": "Apps",
|
"getting_started.appsshort": "应用",
|
||||||
"getting_started.faq": "FAQ",
|
"getting_started.faq": "常见问题",
|
||||||
"getting_started.heading": "开始使用",
|
"getting_started.heading": "开始使用",
|
||||||
"getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。",
|
"getting_started.open_source_notice": "Mastodon 是一个开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。",
|
||||||
"getting_started.userguide": "用户指南",
|
"getting_started.userguide": "用户指南",
|
||||||
"home.column_settings.advanced": "高端",
|
"home.column_settings.advanced": "高级设置",
|
||||||
"home.column_settings.basic": "基本",
|
"home.column_settings.basic": "基本设置",
|
||||||
"home.column_settings.filter_regex": "使用正则表达式 (regex) 过滤",
|
"home.column_settings.filter_regex": "使用正则表达式(regex)过滤",
|
||||||
"home.column_settings.show_reblogs": "显示被转的嘟文",
|
"home.column_settings.show_reblogs": "显示转嘟",
|
||||||
"home.column_settings.show_replies": "显示回应嘟文",
|
"home.column_settings.show_replies": "显示回复",
|
||||||
"home.settings": "字段设置",
|
"home.settings": "栏目设置",
|
||||||
"lightbox.close": "关闭",
|
"lightbox.close": "关闭",
|
||||||
"lightbox.next": "下一步",
|
"lightbox.next": "下一步",
|
||||||
"lightbox.previous": "上一步",
|
"lightbox.previous": "上一步",
|
||||||
"loading_indicator.label": "加载中……",
|
"loading_indicator.label": "加载中……",
|
||||||
"media_gallery.toggle_visible": "打开或关上",
|
"media_gallery.toggle_visible": "切换显示/隐藏",
|
||||||
"missing_indicator.label": "找不到内容",
|
"missing_indicator.label": "找不到内容",
|
||||||
"navigation_bar.blocks": "被屏蔽的用户",
|
"navigation_bar.blocks": "被屏蔽的用户",
|
||||||
"navigation_bar.community_timeline": "本站时间轴",
|
"navigation_bar.community_timeline": "本站时间轴",
|
||||||
|
@ -119,9 +119,9 @@
|
||||||
"notification.follow": "{name} 开始关注你",
|
"notification.follow": "{name} 开始关注你",
|
||||||
"notification.mention": "{name} 提及你",
|
"notification.mention": "{name} 提及你",
|
||||||
"notification.reblog": "{name} 转嘟了你的嘟文",
|
"notification.reblog": "{name} 转嘟了你的嘟文",
|
||||||
"notifications.clear": "清空通知纪录",
|
"notifications.clear": "清空通知列表",
|
||||||
"notifications.clear_confirmation": "你确定要清空通知纪录吗?",
|
"notifications.clear_confirmation": "你确定要清空通知列表吗?",
|
||||||
"notifications.column_settings.alert": "显示桌面通知",
|
"notifications.column_settings.alert": "桌面通知",
|
||||||
"notifications.column_settings.favourite": "你的嘟文被收藏:",
|
"notifications.column_settings.favourite": "你的嘟文被收藏:",
|
||||||
"notifications.column_settings.follow": "关注你:",
|
"notifications.column_settings.follow": "关注你:",
|
||||||
"notifications.column_settings.mention": "提及你:",
|
"notifications.column_settings.mention": "提及你:",
|
||||||
|
@ -132,90 +132,91 @@
|
||||||
"notifications.column_settings.sound": "播放音效",
|
"notifications.column_settings.sound": "播放音效",
|
||||||
"onboarding.done": "出发!",
|
"onboarding.done": "出发!",
|
||||||
"onboarding.next": "下一步",
|
"onboarding.next": "下一步",
|
||||||
"onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。",
|
"onboarding.page_five.public_timelines": "本站时间轴显示的是由本站({domain})用户发布的所有公开嘟文。跨站公共时间轴显示的的是由本站用户关注对象所发布的所有公开嘟文。这些就是寻人好去处的公共时间轴啦。",
|
||||||
"onboarding.page_four.home": "你的主时间轴上是你关注的用户的嘟文.",
|
"onboarding.page_four.home": "你的主页上的时间轴上显示的是你关注对象的嘟文。",
|
||||||
"onboarding.page_four.notifications": "如果你和他人产生了互动,便会出现在通知列上啦~",
|
"onboarding.page_four.notifications": "如果有人与你互动,便会出现在通知栏中哦~",
|
||||||
"onboarding.page_one.federation": "Mastodon是由一系列独立的服务器共同打造的强大的社交网络,我们将这些独立但又相互连接的服务器叫做服务器实例。",
|
"onboarding.page_one.federation": "Mastodon 是由一系列独立的服务器共同打造的强大的社交网络,我们将这些各自独立但又相互连接的服务器叫做实例。",
|
||||||
"onboarding.page_one.handle": "你在 {domain}, {handle} 就是你的完整帐户名称。",
|
"onboarding.page_one.handle": "你在 {domain},{handle} 就是你的完整帐户名称。",
|
||||||
"onboarding.page_one.welcome": "欢迎来到 Mastodon!",
|
"onboarding.page_one.welcome": "欢迎来到 Mastodon!",
|
||||||
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
|
"onboarding.page_six.admin": "{admin} 是你所在服务器实例的管理员.",
|
||||||
"onboarding.page_six.almost_done": "差不多了…",
|
"onboarding.page_six.almost_done": "差不多了……",
|
||||||
"onboarding.page_six.appetoot": "嗷呜~",
|
"onboarding.page_six.appetoot": "嗷呜~",
|
||||||
"onboarding.page_six.apps_available": "也有适用于 iOS, Android 和其它平台的 {apps} 咯~",
|
"onboarding.page_six.apps_available": "我们还有适用于 iOS、Android 和其它平台的{apps}哦~",
|
||||||
"onboarding.page_six.github": "Mastodon 是自由的开放源代码软件。欢迎来 {github} 报告问题,提交功能请求,或者贡献代码 :-)",
|
"onboarding.page_six.github": "Mastodon 是自由的开源软件。欢迎前往 {github} 反馈问题、提出对新功能的建议或贡献代码 :-)",
|
||||||
"onboarding.page_six.guidelines": "社区指南",
|
"onboarding.page_six.guidelines": "社区指南",
|
||||||
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!",
|
"onboarding.page_six.read_guidelines": "别忘了看看 {domain} 的{guidelines}!",
|
||||||
"onboarding.page_six.various_app": "移动应用程序",
|
"onboarding.page_six.various_app": "移动设备应用",
|
||||||
"onboarding.page_three.profile": "修改你的个人资料,比如头像、简介、和昵称等等。在那还可以找到其它首选项。",
|
"onboarding.page_three.profile": "你可以修改你的个人资料,比如头像、简介和昵称等偏好设置。",
|
||||||
"onboarding.page_three.search": "用搜索来找人和标签吧,比如 {illustration} 或者 {introductions}。想找其它服务器实例上的人,用完整帐户名称(用户名@域名)啦。",
|
"onboarding.page_three.search": "你可以通过搜索功能寻找用户和话题标签,比如{illustration}或者{introductions}。如果你想搜索其他实例上的用户,就需要输入完整帐户名称(用户名@域名)哦。",
|
||||||
"onboarding.page_two.compose": "从这里开始嘟!上面的按钮提供了上传图片,修改隐私设置和提示敏感内容等多种功能。.",
|
"onboarding.page_two.compose": "在撰写栏中开始嘟嘟吧!下方的按钮分别用来上传图片,修改嘟文可见范围,以及添加警告信息。",
|
||||||
"onboarding.skip": "好啦好啦我知道啦",
|
"onboarding.skip": "跳过",
|
||||||
"privacy.change": "调整隐私设置",
|
"privacy.change": "设置嘟文可见范围",
|
||||||
"privacy.direct.long": "只有提及的用户能看到",
|
"privacy.direct.long": "只有被提及的用户能看到",
|
||||||
"privacy.direct.short": "私人消息",
|
"privacy.direct.short": "私信",
|
||||||
"privacy.private.long": "只有关注你用户能看到",
|
"privacy.private.long": "只有关注你的用户能看到",
|
||||||
"privacy.private.short": "关注者",
|
"privacy.private.short": "仅关注者",
|
||||||
"privacy.public.long": "在公共时间轴显示",
|
"privacy.public.long": "所有人可见,并会出现在公共时间轴上",
|
||||||
"privacy.public.short": "公共",
|
"privacy.public.short": "公开",
|
||||||
"privacy.unlisted.long": "公开,但不在公共时间轴显示",
|
"privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上",
|
||||||
"privacy.unlisted.short": "公开",
|
"privacy.unlisted.short": "不公开",
|
||||||
"relative_time.days": "{number}d",
|
"relative_time.days": "{number} 天",
|
||||||
"relative_time.hours": "{number}h",
|
"relative_time.hours": "{number} 时",
|
||||||
"relative_time.just_now": "now",
|
"relative_time.just_now": "刚刚",
|
||||||
"relative_time.minutes": "{number}m",
|
"relative_time.minutes": "{number} 分",
|
||||||
"relative_time.seconds": "{number}s",
|
"relative_time.seconds": "{number} 秒",
|
||||||
"reply_indicator.cancel": "取消",
|
"reply_indicator.cancel": "取消",
|
||||||
"report.placeholder": "额外消息",
|
"report.placeholder": "附言",
|
||||||
"report.submit": "提交",
|
"report.submit": "提交",
|
||||||
"report.target": "Reporting",
|
"report.target": "举报 {target}",
|
||||||
"search.placeholder": "搜索",
|
"search.placeholder": "搜索",
|
||||||
"search_popout.search_format": "Advanced search format",
|
"search_popout.search_format": "高级搜索格式",
|
||||||
"search_popout.tips.hashtag": "hashtag",
|
"search_popout.tips.hashtag": "话题标签",
|
||||||
"search_popout.tips.status": "status",
|
"search_popout.tips.status": "嘟文",
|
||||||
"search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags",
|
"search_popout.tips.text": "使用普通字符进行搜索将会返回昵称、用户名和话题标签",
|
||||||
"search_popout.tips.user": "user",
|
"search_popout.tips.user": "用户",
|
||||||
"search_results.total": "{count, number} {count, plural, one {result} other {results}}",
|
"search_results.total": "共 {count, number} 个结果",
|
||||||
"standalone.public_title": "大家都在干啥?",
|
"standalone.public_title": "大家都在干啥?",
|
||||||
"status.cannot_reblog": "没法转嘟这条嘟文啦……",
|
"status.cannot_reblog": "无法转嘟这条嘟文",
|
||||||
"status.delete": "删除",
|
"status.delete": "删除",
|
||||||
"status.embed": "嵌入",
|
"status.embed": "嵌入",
|
||||||
"status.favourite": "收藏",
|
"status.favourite": "收藏",
|
||||||
"status.load_more": "加载更多",
|
"status.load_more": "加载更多",
|
||||||
"status.media_hidden": "隐藏媒体内容",
|
"status.media_hidden": "隐藏媒体内容",
|
||||||
"status.mention": "提及 @{name}",
|
"status.mention": "提及 @{name}",
|
||||||
"status.more": "More",
|
"status.more": "更多",
|
||||||
"status.mute_conversation": "静音对话",
|
"status.mute_conversation": "静音此对话",
|
||||||
"status.open": "展开嘟文",
|
"status.open": "展开嘟文",
|
||||||
"status.pin": "置顶到资料",
|
"status.pin": "在个人资料页面置顶",
|
||||||
"status.reblog": "转嘟",
|
"status.reblog": "转嘟",
|
||||||
"status.reblogged_by": "{name} 转嘟",
|
"status.reblogged_by": "{name} 转嘟了",
|
||||||
"status.reply": "回应",
|
"status.reply": "回复",
|
||||||
"status.replyAll": "回应整串",
|
"status.replyAll": "回复所有人",
|
||||||
"status.report": "举报 @{name}",
|
"status.report": "举报 @{name}",
|
||||||
"status.sensitive_toggle": "点击显示",
|
"status.sensitive_toggle": "点击显示",
|
||||||
"status.sensitive_warning": "敏感内容",
|
"status.sensitive_warning": "敏感内容",
|
||||||
"status.share": "Share",
|
"status.share": "分享",
|
||||||
"status.show_less": "减少显示",
|
"status.show_less": "隐藏内容",
|
||||||
"status.show_more": "显示更多",
|
"status.show_more": "显示内容",
|
||||||
"status.unmute_conversation": "解禁对话",
|
"status.unmute_conversation": "不再静音此对话",
|
||||||
"status.unpin": "解除置顶",
|
"status.unpin": "在个人资料页面取消置顶",
|
||||||
"tabs_bar.compose": "撰写",
|
"tabs_bar.compose": "撰写",
|
||||||
"tabs_bar.federated_timeline": "跨站",
|
"tabs_bar.federated_timeline": "跨站",
|
||||||
"tabs_bar.home": "主页",
|
"tabs_bar.home": "主页",
|
||||||
"tabs_bar.local_timeline": "本站",
|
"tabs_bar.local_timeline": "本站",
|
||||||
"tabs_bar.notifications": "通知",
|
"tabs_bar.notifications": "通知",
|
||||||
"upload_area.title": "将文件拖放至此上传",
|
"ui.beforeunload": "如果你现在离开 Mastodon,你的草稿内容将会被丢弃。",
|
||||||
|
"upload_area.title": "将文件拖放到此处开始上传",
|
||||||
"upload_button.label": "上传媒体文件",
|
"upload_button.label": "上传媒体文件",
|
||||||
"upload_form.description": "Describe for the visually impaired",
|
"upload_form.description": "为视觉障碍人士添加文字说明",
|
||||||
"upload_form.undo": "还原",
|
"upload_form.undo": "取消上传",
|
||||||
"upload_progress.label": "上传中……",
|
"upload_progress.label": "上传中…",
|
||||||
"video.close": "关闭影片",
|
"video.close": "关闭视频",
|
||||||
"video.exit_fullscreen": "退出全屏",
|
"video.exit_fullscreen": "退出全屏",
|
||||||
"video.expand": "展开影片",
|
"video.expand": "展开视频",
|
||||||
"video.fullscreen": "全屏",
|
"video.fullscreen": "全屏",
|
||||||
"video.hide": "隐藏影片",
|
"video.hide": "隐藏视频",
|
||||||
"video.mute": "静音",
|
"video.mute": "静音",
|
||||||
"video.pause": "暂停",
|
"video.pause": "暂停",
|
||||||
"video.play": "播放",
|
"video.play": "播放",
|
||||||
"video.unmute": "解除静音"
|
"video.unmute": "取消静音"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
import uuid from '../uuid';
|
import uuid from '../uuid';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
mounted: false,
|
mounted: false,
|
||||||
|
@ -54,7 +55,6 @@ const initialState = ImmutableMap({
|
||||||
media_attachments: ImmutableList(),
|
media_attachments: ImmutableList(),
|
||||||
suggestion_token: null,
|
suggestion_token: null,
|
||||||
suggestions: ImmutableList(),
|
suggestions: ImmutableList(),
|
||||||
me: null,
|
|
||||||
default_advanced_options: ImmutableMap({
|
default_advanced_options: ImmutableMap({
|
||||||
do_not_federate: false,
|
do_not_federate: false,
|
||||||
}),
|
}),
|
||||||
|
@ -77,7 +77,6 @@ const initialState = ImmutableMap({
|
||||||
|
|
||||||
function statusToTextMentions(state, status) {
|
function statusToTextMentions(state, status) {
|
||||||
let set = ImmutableOrderedSet([]);
|
let set = ImmutableOrderedSet([]);
|
||||||
let me = state.get('me');
|
|
||||||
|
|
||||||
if (status.getIn(['account', 'id']) !== me) {
|
if (status.getIn(['account', 'id']) !== me) {
|
||||||
set = set.add(`@${status.getIn(['account', 'acct'])} `);
|
set = set.add(`@${status.getIn(['account', 'acct'])} `);
|
||||||
|
|
|
@ -8,7 +8,7 @@ const initialState = ImmutableList();
|
||||||
export default function custom_emojis(state = initialState, action) {
|
export default function custom_emojis(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case STORE_HYDRATE:
|
case STORE_HYDRATE:
|
||||||
emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', []), action.state.getIn(['meta', 'auto_play_gif'], false)) });
|
emojiSearch('', { custom: buildCustomEmojis(action.state.get('custom_emojis', [])) });
|
||||||
return action.state.get('custom_emojis');
|
return action.state.get('custom_emojis');
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { Map as ImmutableMap } from 'immutable';
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
streaming_api_base_url: null,
|
streaming_api_base_url: null,
|
||||||
access_token: null,
|
access_token: null,
|
||||||
me: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function meta(state = initialState, action) {
|
export default function meta(state = initialState, action) {
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default function mutes(state = initialState, action) {
|
||||||
state.setIn(['new', 'notifications'], true);
|
state.setIn(['new', 'notifications'], true);
|
||||||
});
|
});
|
||||||
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
||||||
return state.setIn(['new', 'notifications'], !state.getIn(['new', 'notifications']));
|
return state.updateIn(['new', 'notifications'], (old) => !old);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,66 @@
|
||||||
import WebSocketClient from 'websocket.js';
|
import WebSocketClient from 'websocket.js';
|
||||||
|
|
||||||
|
export function connectStream(path, pollingRefresh = null, callbacks = () => ({ onConnect() {}, onDisconnect() {}, onReceive() {} })) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const streamingAPIBaseURL = getState().getIn(['meta', 'streaming_api_base_url']);
|
||||||
|
const accessToken = getState().getIn(['meta', 'access_token']);
|
||||||
|
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
||||||
|
let polling = null;
|
||||||
|
|
||||||
|
const setupPolling = () => {
|
||||||
|
polling = setInterval(() => {
|
||||||
|
pollingRefresh(dispatch);
|
||||||
|
}, 20000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPolling = () => {
|
||||||
|
if (polling) {
|
||||||
|
clearInterval(polling);
|
||||||
|
polling = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscription = getStream(streamingAPIBaseURL, accessToken, path, {
|
||||||
|
connected () {
|
||||||
|
if (pollingRefresh) {
|
||||||
|
clearPolling();
|
||||||
|
}
|
||||||
|
onConnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
disconnected () {
|
||||||
|
if (pollingRefresh) {
|
||||||
|
setupPolling();
|
||||||
|
}
|
||||||
|
onDisconnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
received (data) {
|
||||||
|
onReceive(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
reconnected () {
|
||||||
|
if (pollingRefresh) {
|
||||||
|
clearPolling();
|
||||||
|
pollingRefresh(dispatch);
|
||||||
|
}
|
||||||
|
onConnect();
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const disconnect = () => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.close();
|
||||||
|
}
|
||||||
|
clearPolling();
|
||||||
|
};
|
||||||
|
|
||||||
|
return disconnect;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
export default function getStream(streamingAPIBaseURL, accessToken, stream, { connected, received, disconnected, reconnected }) {
|
||||||
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?access_token=${accessToken}&stream=${stream}`);
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
require('../styles/custom.scss');
|
|
|
@ -253,6 +253,15 @@
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg {
|
||||||
|
margin: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis {
|
.ellipsis {
|
||||||
|
@ -555,6 +564,7 @@
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
padding-top: 5px;
|
||||||
|
|
||||||
&.status__content--with-spoiler {
|
&.status__content--with-spoiler {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -565,8 +575,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
width: 18px;
|
width: 20px;
|
||||||
height: 18px;
|
height: 20px;
|
||||||
|
margin: -5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
|
@ -671,7 +682,7 @@
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: lighten($ui-base-color, 4%);
|
background: lighten($ui-base-color, 4%);
|
||||||
|
|
||||||
&.status-direct {
|
.status.status-direct {
|
||||||
background: lighten($ui-base-color, 12%);
|
background: lighten($ui-base-color, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,6 +701,12 @@
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
|
@supports (-ms-overflow-style: -ms-autohiding-scrollbar) {
|
||||||
|
// Add margin to avoid Edge auto-hiding scrollbar appearing over content.
|
||||||
|
// On Edge 16 this is 16px and Edge <=15 it's 12px, so aim for 16px.
|
||||||
|
padding-right: 26px; // 10px + 16px
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fade {
|
@keyframes fade {
|
||||||
0% { opacity: 0; }
|
0% { opacity: 0; }
|
||||||
100% { opacity: 1; }
|
100% { opacity: 1; }
|
||||||
|
@ -920,8 +937,9 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
|
||||||
.emojione {
|
.emojione {
|
||||||
width: 22px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
|
margin: -5px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2908,7 +2926,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
color: $primary-text-color;
|
color: $primary-text-color;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
right: 10px;
|
left: 10px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -2923,7 +2941,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
.account--action-button {
|
.account--action-button {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
left: 20px;
|
right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-toggle {
|
.setting-toggle {
|
||||||
|
@ -3973,6 +3991,14 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mute-modal {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mute-modal .react-toggle {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.report-modal__statuses,
|
.report-modal__statuses,
|
||||||
.report-modal__comment {
|
.report-modal__comment {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.landing-strip {
|
.landing-strip,
|
||||||
|
.memoriam-strip {
|
||||||
background: rgba(darken($ui-base-color, 7%), 0.8);
|
background: rgba(darken($ui-base-color, 7%), 0.8);
|
||||||
color: $ui-primary-color;
|
color: $ui-primary-color;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
@ -29,3 +30,7 @@
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.memoriam-strip {
|
||||||
|
background: rgba($base-shadow-color, 0.7);
|
||||||
|
}
|
||||||
|
|
|
@ -89,20 +89,28 @@ class Formatter
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def count_tag_nesting(tag)
|
||||||
|
if tag[1] == '/' then -1
|
||||||
|
elsif tag[-2] == '/' then 0
|
||||||
|
else 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def encode_custom_emojis(html, emojis)
|
def encode_custom_emojis(html, emojis)
|
||||||
return html if emojis.empty?
|
return html if emojis.empty?
|
||||||
|
|
||||||
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
|
emoji_map = emojis.map { |e| [e.shortcode, full_asset_url(e.image.url(:static))] }.to_h
|
||||||
|
|
||||||
i = -1
|
i = -1
|
||||||
inside_tag = false
|
tag_open_index = nil
|
||||||
inside_shortname = false
|
inside_shortname = false
|
||||||
shortname_start_index = -1
|
shortname_start_index = -1
|
||||||
|
invisible_depth = 0
|
||||||
|
|
||||||
while i + 1 < html.size
|
while i + 1 < html.size
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
if inside_shortname && html[i] == ':'
|
if invisible_depth.zero? && inside_shortname && html[i] == ':'
|
||||||
shortcode = html[shortname_start_index + 1..i - 1]
|
shortcode = html[shortname_start_index + 1..i - 1]
|
||||||
emoji = emoji_map[shortcode]
|
emoji = emoji_map[shortcode]
|
||||||
|
|
||||||
|
@ -116,12 +124,18 @@ class Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
inside_shortname = false
|
inside_shortname = false
|
||||||
elsif inside_tag && html[i] == '>'
|
elsif tag_open_index && html[i] == '>'
|
||||||
inside_tag = false
|
tag = html[tag_open_index..i]
|
||||||
|
tag_open_index = nil
|
||||||
|
if invisible_depth.positive?
|
||||||
|
invisible_depth += count_tag_nesting(tag)
|
||||||
|
elsif tag == '<span class="invisible">'
|
||||||
|
invisible_depth = 1
|
||||||
|
end
|
||||||
elsif html[i] == '<'
|
elsif html[i] == '<'
|
||||||
inside_tag = true
|
tag_open_index = i
|
||||||
inside_shortname = false
|
inside_shortname = false
|
||||||
elsif !inside_tag && html[i] == ':'
|
elsif !tag_open_index && html[i] == ':'
|
||||||
inside_shortname = true
|
inside_shortname = true
|
||||||
shortname_start_index = i
|
shortname_start_index = i
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,6 +7,8 @@ class NotificationMailer < ApplicationMailer
|
||||||
@me = recipient
|
@me = recipient
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
|
return if @me.user.disabled?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.mention.subject', name: @status.account.acct)
|
||||||
|
@ -17,6 +19,8 @@ class NotificationMailer < ApplicationMailer
|
||||||
@me = recipient
|
@me = recipient
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
|
|
||||||
|
return if @me.user.disabled?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
||||||
end
|
end
|
||||||
|
@ -27,6 +31,8 @@ class NotificationMailer < ApplicationMailer
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
|
return if @me.user.disabled?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.favourite.subject', name: @account.acct)
|
||||||
|
@ -38,6 +44,8 @@ class NotificationMailer < ApplicationMailer
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
@status = notification.target_status
|
@status = notification.target_status
|
||||||
|
|
||||||
|
return if @me.user.disabled?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
thread_by_conversation(@status.conversation)
|
thread_by_conversation(@status.conversation)
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.reblog.subject', name: @account.acct)
|
||||||
|
@ -48,6 +56,8 @@ class NotificationMailer < ApplicationMailer
|
||||||
@me = recipient
|
@me = recipient
|
||||||
@account = notification.from_account
|
@account = notification.from_account
|
||||||
|
|
||||||
|
return if @me.user.disabled?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
||||||
end
|
end
|
||||||
|
@ -59,15 +69,11 @@ class NotificationMailer < ApplicationMailer
|
||||||
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
|
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since)
|
||||||
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
|
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
|
||||||
|
|
||||||
return if @notifications.empty?
|
return if @me.user.disabled? || @notifications.empty?
|
||||||
|
|
||||||
locale_for_account(@me) do
|
locale_for_account(@me) do
|
||||||
mail to: @me.user.email,
|
mail to: @me.user.email,
|
||||||
subject: I18n.t(
|
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications.size)
|
||||||
:subject,
|
|
||||||
scope: [:notification_mailer, :digest],
|
|
||||||
count: @notifications.size
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ class UserMailer < Devise::Mailer
|
||||||
@token = token
|
@token = token
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
return if @resource.disabled?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
|
mail to: @resource.unconfirmed_email.blank? ? @resource.email : @resource.unconfirmed_email, subject: I18n.t('devise.mailer.confirmation_instructions.subject', instance: @instance)
|
||||||
end
|
end
|
||||||
|
@ -20,6 +22,8 @@ class UserMailer < Devise::Mailer
|
||||||
@token = token
|
@token = token
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
return if @resource.disabled?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
||||||
end
|
end
|
||||||
|
@ -29,6 +33,8 @@ class UserMailer < Devise::Mailer
|
||||||
@resource = user
|
@resource = user
|
||||||
@instance = Rails.configuration.x.local_domain
|
@instance = Rails.configuration.x.local_domain
|
||||||
|
|
||||||
|
return if @resource.disabled?
|
||||||
|
|
||||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||||
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
#
|
#
|
||||||
# Table name: accounts
|
# Table name: accounts
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :bigint not null, primary key
|
||||||
# username :string default(""), not null
|
# username :string default(""), not null
|
||||||
# domain :string
|
# domain :string
|
||||||
# secret :string default(""), not null
|
# secret :string default(""), not null
|
||||||
|
@ -41,10 +41,11 @@
|
||||||
# shared_inbox_url :string default(""), not null
|
# shared_inbox_url :string default(""), not null
|
||||||
# followers_url :string default(""), not null
|
# followers_url :string default(""), not null
|
||||||
# protocol :integer default("ostatus"), not null
|
# protocol :integer default("ostatus"), not null
|
||||||
|
# memorial :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
MENTION_RE = /(?:^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
MENTION_RE = /(?<=^|[^\/[:word:]])@(([a-z0-9_]+)(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
|
||||||
|
|
||||||
include AccountAvatar
|
include AccountAvatar
|
||||||
include AccountFinderConcern
|
include AccountFinderConcern
|
||||||
|
@ -152,6 +153,20 @@ class Account < ApplicationRecord
|
||||||
ResolveRemoteAccountService.new.call(acct)
|
ResolveRemoteAccountService.new.call(acct)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unsuspend!
|
||||||
|
transaction do
|
||||||
|
user&.enable! if local?
|
||||||
|
update!(suspended: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def memorialize!
|
||||||
|
transaction do
|
||||||
|
user&.disable! if local?
|
||||||
|
update!(memorial: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def keypair
|
def keypair
|
||||||
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
|
@keypair ||= OpenSSL::PKey::RSA.new(private_key || public_key)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
# domain :string
|
# domain :string
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :integer
|
# account_id :bigint
|
||||||
# id :integer not null, primary key
|
# id :bigint not null, primary key
|
||||||
#
|
#
|
||||||
|
|
||||||
class AccountDomainBlock < ApplicationRecord
|
class AccountDomainBlock < ApplicationRecord
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
#
|
#
|
||||||
# Table name: account_moderation_notes
|
# Table name: account_moderation_notes
|
||||||
#
|
#
|
||||||
# id :integer not null, primary key
|
# id :bigint not null, primary key
|
||||||
# content :text not null
|
# content :text not null
|
||||||
# account_id :integer not null
|
# account_id :bigint not null
|
||||||
# target_account_id :integer not null
|
# target_account_id :bigint not null
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
#
|
#
|
||||||
# created_at :datetime not null
|
# created_at :datetime not null
|
||||||
# updated_at :datetime not null
|
# updated_at :datetime not null
|
||||||
# account_id :integer not null
|
# account_id :bigint not null
|
||||||
# id :integer not null, primary key
|
# id :bigint not null, primary key
|
||||||
# target_account_id :integer not null
|
# target_account_id :bigint not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Block < ApplicationRecord
|
class Block < ApplicationRecord
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue