mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-13 15:58:50 +00:00
Compare commits
213 Commits
autocollap
...
hotkeys-gl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a48efe16c | ||
|
|
f3c3df62ab | ||
|
|
63d47e04c4 | ||
|
|
c6b7c77229 | ||
|
|
e20258a2e5 | ||
|
|
7fb850e987 | ||
|
|
e77c3996a5 | ||
|
|
1c5b0e3334 | ||
|
|
740f8a95a9 | ||
|
|
5de42665d7 | ||
|
|
0ea4478b68 | ||
|
|
fd87e5a53b | ||
|
|
57fe4102ea | ||
|
|
bf7757cbbc | ||
|
|
1266c66f79 | ||
|
|
d07983b56d | ||
|
|
86f4f8e158 | ||
|
|
662b8eefe8 | ||
|
|
520d147803 | ||
|
|
32987004c9 | ||
|
|
31ac5f0e00 | ||
|
|
269a445c0b | ||
|
|
2b51b4094c | ||
|
|
1104ac35d3 | ||
|
|
a78f66c069 | ||
|
|
8c0e77d688 | ||
|
|
7a45d382ea | ||
|
|
5a551b530a | ||
|
|
167fe2ab08 | ||
|
|
e84fecb7e9 | ||
|
|
faad820458 | ||
|
|
801eee0ff3 | ||
|
|
41c71565c2 | ||
|
|
bc4a726c24 | ||
|
|
2585649b20 | ||
|
|
12c0011fee | ||
|
|
2a5af9c10c | ||
|
|
3c83b7e06e | ||
|
|
fc2155019b | ||
|
|
b7a7eb84dd | ||
|
|
1a07b83c69 | ||
|
|
53b7b81b43 | ||
|
|
8f3e5f6128 | ||
|
|
6f609dc4b4 | ||
|
|
3dce6cbbd7 | ||
|
|
a36a2c1796 | ||
|
|
2bcc81700c | ||
|
|
53e95c4efc | ||
|
|
e45cb0837b | ||
|
|
d083f7741a | ||
|
|
08deec4c84 | ||
|
|
2590aac863 | ||
|
|
3d1d3d9a20 | ||
|
|
8f638a2bf2 | ||
|
|
9d9b1aff1e | ||
|
|
bfdcf76a64 | ||
|
|
bcda3f85ce | ||
|
|
b380e9d2cb | ||
|
|
92cc79be72 | ||
|
|
08a01dd037 | ||
|
|
672ace5a20 | ||
|
|
f51f7b0e06 | ||
|
|
58cede4808 | ||
|
|
dec960c828 | ||
|
|
eed50514be | ||
|
|
6be72a3ec6 | ||
|
|
2864e5e077 | ||
|
|
a618df7998 | ||
|
|
fc2c8b50dd | ||
|
|
e19fc6a9f8 | ||
|
|
45c44989c8 | ||
|
|
5a9982b425 | ||
|
|
284e2cde81 | ||
|
|
130aa90d55 | ||
|
|
1ab12ba38e | ||
|
|
24cafd73a2 | ||
|
|
6f8ccbfcdf | ||
|
|
dc16d73bf5 | ||
|
|
b006bb82af | ||
|
|
94c5a11cda | ||
|
|
45f18b8f49 | ||
|
|
4a2fc2d444 | ||
|
|
e1ebf36352 | ||
|
|
ae6dd08121 | ||
|
|
f6355f6ffb | ||
|
|
e28b33c89c | ||
|
|
2a386ad88d | ||
|
|
fbf76feb37 | ||
|
|
392945f9a3 | ||
|
|
4dd19054d6 | ||
|
|
585758a373 | ||
|
|
b28b405b97 | ||
|
|
dc6e031364 | ||
|
|
9dd5e329ab | ||
|
|
3e90987c8b | ||
|
|
2151fd3150 | ||
|
|
ad207456d6 | ||
|
|
9e3d24a150 | ||
|
|
ee560abdbe | ||
|
|
35fbdc36f9 | ||
|
|
556c07df1f | ||
|
|
88627fd7aa | ||
|
|
c2a92dffc9 | ||
|
|
7e17e764a5 | ||
|
|
3023725936 | ||
|
|
08652baab0 | ||
|
|
8fc54890e5 | ||
|
|
cb4ef24ac9 | ||
|
|
431503bae2 | ||
|
|
04508868b0 | ||
|
|
3e4b01b47d | ||
|
|
19e8b861a2 | ||
|
|
7d7df877ef | ||
|
|
c73a1fb537 | ||
|
|
f6bc6399e2 | ||
|
|
031a5a8f92 | ||
|
|
6d7e05ec1f | ||
|
|
58bca7b1e4 | ||
|
|
1c25853842 | ||
|
|
546257bc7f | ||
|
|
fbef909c2a | ||
|
|
c3ec1e87b8 | ||
|
|
48e27c47a7 | ||
|
|
1f1838420f | ||
|
|
20150659e6 | ||
|
|
8087aa83d4 | ||
|
|
ca5440b93d | ||
|
|
dae8916544 | ||
|
|
d11b1a1aa7 | ||
|
|
a4dcabc11b | ||
|
|
6cd192b9fb | ||
|
|
249b0fe107 | ||
|
|
a6682a3000 | ||
|
|
4112a0631f | ||
|
|
0e6c4cb796 | ||
|
|
bfd9230d61 | ||
|
|
656d54e945 | ||
|
|
92aaa55f06 | ||
|
|
5df8e30415 | ||
|
|
60f247c2e7 | ||
|
|
cf7e840990 | ||
|
|
252d0fe020 | ||
|
|
2fb722397d | ||
|
|
9a42f7cbed | ||
|
|
07f7192bc3 | ||
|
|
48c705bbad | ||
|
|
fcb9533549 | ||
|
|
5128c4261e | ||
|
|
7bb8b0b2fc | ||
|
|
2b1190065c | ||
|
|
b95c48748c | ||
|
|
56720ba590 | ||
|
|
e5aa4128f6 | ||
|
|
f9e7336296 | ||
|
|
4944515020 | ||
|
|
07cca6e364 | ||
|
|
54b42901df | ||
|
|
d200e041fe | ||
|
|
49a285ce15 | ||
|
|
cfd7b7a0b7 | ||
|
|
36376b5e23 | ||
|
|
eb97bd8af6 | ||
|
|
4c0a85ef9b | ||
|
|
64cc129225 | ||
|
|
97fc2da2e0 | ||
|
|
889ada5ee2 | ||
|
|
3f16caaa50 | ||
|
|
5d5c0f4f43 | ||
|
|
1032f3994f | ||
|
|
cbbeec05be | ||
|
|
e618edf85a | ||
|
|
b6e2e999bd | ||
|
|
782224c991 | ||
|
|
84cfee2488 | ||
|
|
7bea1530f4 | ||
|
|
47b0c61853 | ||
|
|
864c4d869f | ||
|
|
d8cd9000d9 | ||
|
|
e1b7785788 | ||
|
|
bc8532359b | ||
|
|
d307ee79e9 | ||
|
|
cf01326cc1 | ||
|
|
a617060dfc | ||
|
|
d48779cf7b | ||
|
|
8a588145d5 | ||
|
|
8abe9e9058 | ||
|
|
15c0f6ae56 | ||
|
|
da3adc0a73 | ||
|
|
0338c16f9f | ||
|
|
38d072446b | ||
|
|
8ae9bd0eea | ||
|
|
5521e94e24 | ||
|
|
763a2f8511 | ||
|
|
60f962eedc | ||
|
|
47d56438da | ||
|
|
0692991b54 | ||
|
|
6705463ed0 | ||
|
|
a2a4bf4e78 | ||
|
|
b254e6ca5f | ||
|
|
29609fbb6a | ||
|
|
d37a56c07c | ||
|
|
2cea4592a3 | ||
|
|
512feab222 | ||
|
|
5e111ce16d | ||
|
|
4080569c2d | ||
|
|
2cbb8e8cd1 | ||
|
|
3e9236b343 | ||
|
|
89c77fe225 | ||
|
|
e843f62f47 | ||
|
|
ec487166db | ||
|
|
37b267e2ab | ||
|
|
3de22a82bf | ||
|
|
e4080772b5 |
@@ -1,21 +1,36 @@
|
|||||||
engines:
|
version: "2"
|
||||||
|
checks:
|
||||||
|
argument-count:
|
||||||
|
enabled: false
|
||||||
|
complex-logic:
|
||||||
|
enabled: false
|
||||||
|
file-lines:
|
||||||
|
enabled: false
|
||||||
|
method-complexity:
|
||||||
|
enabled: false
|
||||||
|
method-count:
|
||||||
|
enabled: false
|
||||||
|
method-lines:
|
||||||
|
enabled: false
|
||||||
|
nested-control-flow:
|
||||||
|
enabled: false
|
||||||
|
return-statements:
|
||||||
|
enabled: false
|
||||||
|
similar-code:
|
||||||
|
enabled: false
|
||||||
|
identical-code:
|
||||||
|
enabled: false
|
||||||
|
plugins:
|
||||||
brakeman:
|
brakeman:
|
||||||
enabled: true
|
enabled: true
|
||||||
bundler-audit:
|
bundler-audit:
|
||||||
enabled: true
|
enabled: true
|
||||||
duplication:
|
|
||||||
enabled: false
|
|
||||||
eslint:
|
eslint:
|
||||||
enabled: true
|
enabled: true
|
||||||
rubocop:
|
rubocop:
|
||||||
enabled: true
|
enabled: true
|
||||||
scss-lint:
|
scss-lint:
|
||||||
enabled: true
|
enabled: true
|
||||||
ratings:
|
exclude_patterns:
|
||||||
paths:
|
|
||||||
- "**.rb"
|
|
||||||
- "**.js"
|
|
||||||
- "**.scss"
|
|
||||||
exclude_paths:
|
|
||||||
- spec/
|
- spec/
|
||||||
- vendor/asset
|
- vendor/asset
|
||||||
|
|||||||
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=
|
||||||
|
|
||||||
|
|||||||
@@ -134,3 +134,6 @@ STREAMING_CLUSTER_NUM=1
|
|||||||
# If you use Docker, you may want to assign UID/GID manually.
|
# If you use Docker, you may want to assign UID/GID manually.
|
||||||
# UID=1000
|
# UID=1000
|
||||||
# GID=1000
|
# GID=1000
|
||||||
|
|
||||||
|
# Maximum allowed character count
|
||||||
|
# MAX_TOOT_CHARS=500
|
||||||
|
|||||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at beatrix.bitrot@gmail.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/
|
||||||
@@ -48,7 +48,7 @@ RUN apk -U upgrade \
|
|||||||
&& rm yarn.tar.gz \
|
&& rm yarn.tar.gz \
|
||||||
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
|
&& mv /tmp/src/yarn-v$YARN_VERSION /opt/yarn \
|
||||||
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
|
&& ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
|
||||||
&& wget -O libiconv.tar.gz "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
|
&& wget -O libiconv.tar.gz "https://ftp.gnu.org/pub/gnu/libiconv/libiconv-$LIBICONV_VERSION.tar.gz" \
|
||||||
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
|
&& echo "$LIBICONV_DOWNLOAD_SHA256 *libiconv.tar.gz" | sha256sum -c - \
|
||||||
&& tar -xzf libiconv.tar.gz -C /tmp/src \
|
&& tar -xzf libiconv.tar.gz -C /tmp/src \
|
||||||
&& rm libiconv.tar.gz \
|
&& rm libiconv.tar.gz \
|
||||||
|
|||||||
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(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
|
||||||
redirect_to admin_account_path(@target_account.id), notice: I18n.t('admin.account_moderation_notes.destroyed_msg')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -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,57 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class AccountsController < BaseController
|
class AccountsController < BaseController
|
||||||
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload]
|
before_action :set_account, only: [:show, :subscribe, :unsubscribe, :redownload, :enable, :disable, :memorialize]
|
||||||
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
before_action :require_remote_account!, only: [:subscribe, :unsubscribe, :redownload]
|
||||||
|
before_action :require_local_account!, only: [:enable, :disable, :memorialize]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :account, :index?
|
||||||
@accounts = filtered_accounts.page(params[:page])
|
@accounts = filtered_accounts.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
authorize @account, :show?
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe
|
def subscribe
|
||||||
|
authorize @account, :subscribe?
|
||||||
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
|
Pubsubhubbub::SubscribeWorker.perform_async(@account.id)
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsubscribe
|
def unsubscribe
|
||||||
|
authorize @account, :unsubscribe?
|
||||||
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
|
Pubsubhubbub::UnsubscribeWorker.perform_async(@account.id)
|
||||||
redirect_to admin_account_path(@account.id)
|
redirect_to admin_account_path(@account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def memorialize
|
||||||
|
authorize @account, :memorialize?
|
||||||
|
@account.memorialize!
|
||||||
|
log_action :memorialize, @account
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def enable
|
||||||
|
authorize @account.user, :enable?
|
||||||
|
@account.user.enable!
|
||||||
|
log_action :enable, @account.user
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disable
|
||||||
|
authorize @account.user, :disable?
|
||||||
|
@account.user.disable!
|
||||||
|
log_action :disable, @account.user
|
||||||
|
redirect_to admin_account_path(@account.id)
|
||||||
|
end
|
||||||
|
|
||||||
def redownload
|
def redownload
|
||||||
|
authorize @account, :redownload?
|
||||||
|
|
||||||
@account.reset_avatar!
|
@account.reset_avatar!
|
||||||
@account.reset_header!
|
@account.reset_header!
|
||||||
@account.save!
|
@account.save!
|
||||||
@@ -42,6 +70,10 @@ module Admin
|
|||||||
redirect_to admin_account_path(@account.id) if @account.local?
|
redirect_to admin_account_path(@account.id) if @account.local?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def require_local_account!
|
||||||
|
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
|
||||||
|
end
|
||||||
|
|
||||||
def filtered_accounts
|
def filtered_accounts
|
||||||
AccountFilter.new(filter_params).results
|
AccountFilter.new(filter_params).results
|
||||||
end
|
end
|
||||||
|
|||||||
9
app/controllers/admin/action_logs_controller.rb
Normal file
9
app/controllers/admin/action_logs_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class ActionLogsController < BaseController
|
||||||
|
def index
|
||||||
|
@action_logs = Admin::ActionLog.page(params[:page])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class BaseController < ApplicationController
|
class BaseController < ApplicationController
|
||||||
before_action :require_admin!
|
include Authorization
|
||||||
|
include AccountableConcern
|
||||||
|
|
||||||
|
before_action :require_staff!
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,15 +2,19 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class ConfirmationsController < BaseController
|
class ConfirmationsController < BaseController
|
||||||
|
before_action :set_user
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account_user.confirm
|
authorize @user, :confirm?
|
||||||
|
@user.confirm!
|
||||||
|
log_action :confirm, @user
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def account_user
|
def set_user
|
||||||
Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ module Admin
|
|||||||
before_action :set_custom_emoji, except: [:index, :new, :create]
|
before_action :set_custom_emoji, except: [:index, :new, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@custom_emojis = filtered_custom_emojis.page(params[:page])
|
authorize :custom_emoji, :index?
|
||||||
|
@custom_emojis = filtered_custom_emojis.eager_load(:local_counterpart).page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
authorize :custom_emoji, :create?
|
||||||
@custom_emoji = CustomEmoji.new
|
@custom_emoji = CustomEmoji.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :custom_emoji, :create?
|
||||||
|
|
||||||
@custom_emoji = CustomEmoji.new(resource_params)
|
@custom_emoji = CustomEmoji.new(resource_params)
|
||||||
|
|
||||||
if @custom_emoji.save
|
if @custom_emoji.save
|
||||||
|
log_action :create, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.created_msg')
|
||||||
else
|
else
|
||||||
render :new
|
render :new
|
||||||
@@ -23,7 +28,10 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
authorize @custom_emoji, :update?
|
||||||
|
|
||||||
if @custom_emoji.update(resource_params)
|
if @custom_emoji.update(resource_params)
|
||||||
|
log_action :update, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.updated_msg')
|
||||||
else
|
else
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.update_failed_msg')
|
||||||
@@ -31,14 +39,20 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@custom_emoji.destroy
|
authorize @custom_emoji, :destroy?
|
||||||
|
@custom_emoji.destroy!
|
||||||
|
log_action :destroy, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
def copy
|
def copy
|
||||||
emoji = CustomEmoji.new(domain: nil, shortcode: @custom_emoji.shortcode, image: @custom_emoji.image)
|
authorize @custom_emoji, :copy?
|
||||||
|
|
||||||
|
emoji = CustomEmoji.find_or_initialize_by(domain: nil, shortcode: @custom_emoji.shortcode)
|
||||||
|
emoji.image = @custom_emoji.image
|
||||||
|
|
||||||
if emoji.save
|
if emoji.save
|
||||||
|
log_action :create, emoji
|
||||||
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
|
flash[:notice] = I18n.t('admin.custom_emojis.copied_msg')
|
||||||
else
|
else
|
||||||
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
flash[:alert] = I18n.t('admin.custom_emojis.copy_failed_msg')
|
||||||
@@ -48,12 +62,16 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def enable
|
def enable
|
||||||
|
authorize @custom_emoji, :enable?
|
||||||
@custom_emoji.update!(disabled: false)
|
@custom_emoji.update!(disabled: false)
|
||||||
|
log_action :enable, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.enabled_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable
|
def disable
|
||||||
|
authorize @custom_emoji, :disable?
|
||||||
@custom_emoji.update!(disabled: true)
|
@custom_emoji.update!(disabled: true)
|
||||||
|
log_action :disable, @custom_emoji
|
||||||
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
redirect_to admin_custom_emojis_path, notice: I18n.t('admin.custom_emojis.disabled_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,28 +5,37 @@ module Admin
|
|||||||
before_action :set_domain_block, only: [:show, :destroy]
|
before_action :set_domain_block, only: [:show, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :domain_block, :index?
|
||||||
@domain_blocks = DomainBlock.page(params[:page])
|
@domain_blocks = DomainBlock.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
authorize :domain_block, :create?
|
||||||
@domain_block = DomainBlock.new
|
@domain_block = DomainBlock.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :domain_block, :create?
|
||||||
|
|
||||||
@domain_block = DomainBlock.new(resource_params)
|
@domain_block = DomainBlock.new(resource_params)
|
||||||
|
|
||||||
if @domain_block.save
|
if @domain_block.save
|
||||||
DomainBlockWorker.perform_async(@domain_block.id)
|
DomainBlockWorker.perform_async(@domain_block.id)
|
||||||
|
log_action :create, @domain_block
|
||||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
|
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
render :new
|
render :new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show
|
||||||
|
authorize @domain_block, :show?
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @domain_block, :destroy?
|
||||||
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
|
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
|
||||||
|
log_action :destroy, @domain_block
|
||||||
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
redirect_to admin_domain_blocks_path, notice: I18n.t('admin.domain_blocks.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,17 +5,22 @@ module Admin
|
|||||||
before_action :set_email_domain_block, only: [:show, :destroy]
|
before_action :set_email_domain_block, only: [:show, :destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :email_domain_block, :index?
|
||||||
@email_domain_blocks = EmailDomainBlock.page(params[:page])
|
@email_domain_blocks = EmailDomainBlock.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
authorize :email_domain_block, :create?
|
||||||
@email_domain_block = EmailDomainBlock.new
|
@email_domain_block = EmailDomainBlock.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize :email_domain_block, :create?
|
||||||
|
|
||||||
@email_domain_block = EmailDomainBlock.new(resource_params)
|
@email_domain_block = EmailDomainBlock.new(resource_params)
|
||||||
|
|
||||||
if @email_domain_block.save
|
if @email_domain_block.save
|
||||||
|
log_action :create, @email_domain_block
|
||||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
|
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.created_msg')
|
||||||
else
|
else
|
||||||
render :new
|
render :new
|
||||||
@@ -23,7 +28,9 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@email_domain_block.destroy
|
authorize @email_domain_block, :destroy?
|
||||||
|
@email_domain_block.destroy!
|
||||||
|
log_action :destroy, @email_domain_block
|
||||||
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
|
redirect_to admin_email_domain_blocks_path, notice: I18n.t('admin.email_domain_blocks.destroyed_msg')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
33
app/controllers/admin/invites_controller.rb
Normal file
33
app/controllers/admin/invites_controller.rb
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class InvitesController < BaseController
|
||||||
|
def index
|
||||||
|
authorize :invite, :index?
|
||||||
|
|
||||||
|
@invites = Invite.includes(user: :account).page(params[:page])
|
||||||
|
@invite = Invite.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :invite, :create?
|
||||||
|
|
||||||
|
@invite = Invite.new(resource_params)
|
||||||
|
@invite.user = current_user
|
||||||
|
|
||||||
|
if @invite.save
|
||||||
|
redirect_to admin_invites_path
|
||||||
|
else
|
||||||
|
@invites = Invite.page(params[:page])
|
||||||
|
render :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@invite = Invite.find(params[:id])
|
||||||
|
authorize @invite, :destroy?
|
||||||
|
@invite.expire!
|
||||||
|
redirect_to admin_invites_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,26 +2,29 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class ReportedStatusesController < BaseController
|
class ReportedStatusesController < BaseController
|
||||||
include Authorization
|
|
||||||
|
|
||||||
before_action :set_report
|
before_action :set_report
|
||||||
before_action :set_status, only: [:update, :destroy]
|
before_action :set_status, only: [:update, :destroy]
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params)
|
authorize :status, :update?
|
||||||
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
|
||||||
|
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
redirect_to admin_report_path(@report)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@status.update(status_params)
|
authorize @status, :update?
|
||||||
|
@status.update!(status_params)
|
||||||
|
log_action :update, @status
|
||||||
redirect_to admin_report_path(@report)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
RemovalWorker.perform_async(@status.id)
|
RemovalWorker.perform_async(@status.id)
|
||||||
|
log_action :destroy, @status
|
||||||
render json: @status
|
render json: @status
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ module Admin
|
|||||||
before_action :set_report, except: [:index]
|
before_action :set_report, except: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :report, :index?
|
||||||
@reports = filtered_reports.page(params[:page])
|
@reports = filtered_reports.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
authorize @report, :show?
|
||||||
@form = Form::StatusBatch.new
|
@form = Form::StatusBatch.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
authorize @report, :update?
|
||||||
process_report
|
process_report
|
||||||
redirect_to admin_report_path(@report)
|
redirect_to admin_report_path(@report)
|
||||||
end
|
end
|
||||||
@@ -22,12 +25,17 @@ module Admin
|
|||||||
def process_report
|
def process_report
|
||||||
case params[:outcome].to_s
|
case params[:outcome].to_s
|
||||||
when 'resolve'
|
when 'resolve'
|
||||||
@report.update(action_taken_by_current_attributes)
|
@report.update!(action_taken_by_current_attributes)
|
||||||
|
log_action :resolve, @report
|
||||||
when 'suspend'
|
when 'suspend'
|
||||||
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
Admin::SuspensionWorker.perform_async(@report.target_account.id)
|
||||||
|
log_action :resolve, @report
|
||||||
|
log_action :suspend, @report.target_account
|
||||||
resolve_all_target_account_reports
|
resolve_all_target_account_reports
|
||||||
when 'silence'
|
when 'silence'
|
||||||
@report.target_account.update(silenced: true)
|
@report.target_account.update!(silenced: true)
|
||||||
|
log_action :resolve, @report
|
||||||
|
log_action :silence, @report.target_account
|
||||||
resolve_all_target_account_reports
|
resolve_all_target_account_reports
|
||||||
else
|
else
|
||||||
raise ActiveRecord::RecordNotFound
|
raise ActiveRecord::RecordNotFound
|
||||||
|
|||||||
@@ -2,17 +2,19 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class ResetsController < BaseController
|
class ResetsController < BaseController
|
||||||
before_action :set_account
|
before_action :set_user
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@account.user.send_reset_password_instructions
|
authorize @user, :reset_password?
|
||||||
|
@user.send_reset_password_instructions
|
||||||
|
log_action :reset_password, @user
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_account
|
def set_user
|
||||||
@account = Account.find(params[:account_id])
|
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
27
app/controllers/admin/roles_controller.rb
Normal file
27
app/controllers/admin/roles_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin
|
||||||
|
class RolesController < BaseController
|
||||||
|
before_action :set_user
|
||||||
|
|
||||||
|
def promote
|
||||||
|
authorize @user, :promote?
|
||||||
|
@user.promote!
|
||||||
|
log_action :promote, @user
|
||||||
|
redirect_to admin_account_path(@user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def demote
|
||||||
|
authorize @user, :demote?
|
||||||
|
@user.demote!
|
||||||
|
log_action :demote, @user
|
||||||
|
redirect_to admin_account_path(@user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,14 +13,17 @@ module Admin
|
|||||||
closed_registrations_message
|
closed_registrations_message
|
||||||
open_deletion
|
open_deletion
|
||||||
timeline_preview
|
timeline_preview
|
||||||
|
show_staff_badge
|
||||||
bootstrap_timeline_accounts
|
bootstrap_timeline_accounts
|
||||||
thumbnail
|
thumbnail
|
||||||
|
min_invite_role
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
BOOLEAN_SETTINGS = %w(
|
BOOLEAN_SETTINGS = %w(
|
||||||
open_registrations
|
open_registrations
|
||||||
open_deletion
|
open_deletion
|
||||||
timeline_preview
|
timeline_preview
|
||||||
|
show_staff_badge
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
UPLOAD_SETTINGS = %w(
|
UPLOAD_SETTINGS = %w(
|
||||||
@@ -28,10 +31,13 @@ module Admin
|
|||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
def edit
|
def edit
|
||||||
|
authorize :settings, :show?
|
||||||
@admin_settings = Form::AdminSettings.new
|
@admin_settings = Form::AdminSettings.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
authorize :settings, :update?
|
||||||
|
|
||||||
settings_params.each do |key, value|
|
settings_params.each do |key, value|
|
||||||
if UPLOAD_SETTINGS.include?(key)
|
if UPLOAD_SETTINGS.include?(key)
|
||||||
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
|
upload = SiteUpload.where(var: key).first_or_initialize(var: key)
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ module Admin
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@account.update(silenced: true)
|
authorize @account, :silence?
|
||||||
|
@account.update!(silenced: true)
|
||||||
|
log_action :silence, @account
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@account.update(silenced: false)
|
authorize @account, :unsilence?
|
||||||
|
@account.update!(silenced: false)
|
||||||
|
log_action :unsilence, @account
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class StatusesController < BaseController
|
class StatusesController < BaseController
|
||||||
include Authorization
|
|
||||||
|
|
||||||
helper_method :current_params
|
helper_method :current_params
|
||||||
|
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
@@ -12,31 +10,39 @@ module Admin
|
|||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
authorize :status, :index?
|
||||||
|
|
||||||
@statuses = @account.statuses
|
@statuses = @account.statuses
|
||||||
|
|
||||||
if params[:media]
|
if params[:media]
|
||||||
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct
|
||||||
@statuses.merge!(Status.where(id: account_media_status_ids))
|
@statuses.merge!(Status.where(id: account_media_status_ids))
|
||||||
end
|
end
|
||||||
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)
|
|
||||||
|
|
||||||
|
@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
|
||||||
@form = Form::StatusBatch.new(form_status_batch_params)
|
authorize :status, :update?
|
||||||
flash[:alert] = t('admin.statuses.failed_to_execute') unless @form.save
|
|
||||||
|
@form = Form::StatusBatch.new(form_status_batch_params.merge(current_account: current_account))
|
||||||
|
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
|
||||||
|
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@status.update(status_params)
|
authorize @status, :update?
|
||||||
|
@status.update!(status_params)
|
||||||
|
log_action :update, @status
|
||||||
redirect_to admin_account_statuses_path(@account.id, current_params)
|
redirect_to admin_account_statuses_path(@account.id, current_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @status, :destroy?
|
authorize @status, :destroy?
|
||||||
RemovalWorker.perform_async(@status.id)
|
RemovalWorker.perform_async(@status.id)
|
||||||
|
log_action :destroy, @status
|
||||||
render json: @status
|
render json: @status
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -60,6 +66,7 @@ module Admin
|
|||||||
|
|
||||||
def current_params
|
def current_params
|
||||||
page = (params[:page] || 1).to_i
|
page = (params[:page] || 1).to_i
|
||||||
|
|
||||||
{
|
{
|
||||||
media: params[:media],
|
media: params[:media],
|
||||||
page: page > 1 && page,
|
page: page > 1 && page,
|
||||||
|
|||||||
@@ -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,16 @@ module Admin
|
|||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
authorize @account, :suspend?
|
||||||
Admin::SuspensionWorker.perform_async(@account.id)
|
Admin::SuspensionWorker.perform_async(@account.id)
|
||||||
|
log_action :suspend, @account
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@account.update(suspended: false)
|
authorize @account, :unsuspend?
|
||||||
|
@account.unsuspend!
|
||||||
|
log_action :unsuspend, @account
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ module Admin
|
|||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
authorize @user, :disable_2fa?
|
||||||
@user.disable_two_factor!
|
@user.disable_two_factor!
|
||||||
|
log_action :disable_2fa, @user
|
||||||
redirect_to admin_accounts_path
|
redirect_to admin_accounts_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def follow
|
def follow
|
||||||
FollowService.new.call(current_user.account, @account.acct)
|
reblogs_arg = { reblogs: params[:reblogs] }
|
||||||
|
|
||||||
options = @account.locked? ? {} : { following_map: { @account.id => true }, requested_map: { @account.id => false } }
|
FollowService.new.call(current_user.account, @account.acct, reblogs_arg)
|
||||||
|
|
||||||
|
options = @account.locked? ? {} : { following_map: { @account.id => reblogs_arg }, requested_map: { @account.id => false } }
|
||||||
|
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships(options)
|
||||||
end
|
end
|
||||||
|
|||||||
81
app/controllers/api/v1/lists/accounts_controller.rb
Normal file
81
app/controllers/api/v1/lists/accounts_controller.rb
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Lists::AccountsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read }, only: [:show]
|
||||||
|
before_action -> { doorkeeper_authorize! :write }, except: [:show]
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_list
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :show
|
||||||
|
|
||||||
|
def show
|
||||||
|
@accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||||
|
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
list_accounts.each do |account|
|
||||||
|
@list.accounts << account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
ListAccount.where(list: @list, account_id: account_ids).destroy_all
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_list
|
||||||
|
@list = List.where(account: current_account).find(params[:list_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_accounts
|
||||||
|
Account.find(account_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_ids
|
||||||
|
Array(resource_params[:account_ids])
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(account_ids: [])
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
if records_continue?
|
||||||
|
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
unless @accounts.empty?
|
||||||
|
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@accounts.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@accounts.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.permit(:limit).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
79
app/controllers/api/v1/lists_controller.rb
Normal file
79
app/controllers/api/v1/lists_controller.rb
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::ListsController < Api::BaseController
|
||||||
|
LISTS_LIMIT = 50
|
||||||
|
|
||||||
|
before_action -> { doorkeeper_authorize! :read }, only: [:index, :show]
|
||||||
|
before_action -> { doorkeeper_authorize! :write }, except: [:index, :show]
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_list, except: [:index, :create]
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
@lists = List.where(account: current_account).paginate_by_max_id(limit_param(LISTS_LIMIT), params[:max_id], params[:since_id])
|
||||||
|
render json: @lists, each_serializer: REST::ListSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @list, serializer: REST::ListSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@list = List.create!(list_params.merge(account: current_account))
|
||||||
|
render json: @list, serializer: REST::ListSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@list.update!(list_params)
|
||||||
|
render json: @list, serializer: REST::ListSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@list.destroy!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_list
|
||||||
|
@list = List.where(account: current_account).find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_params
|
||||||
|
params.permit(:title)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
if records_continue?
|
||||||
|
api_v1_lists_url pagination_params(max_id: pagination_max_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
unless @lists.empty?
|
||||||
|
api_v1_lists_url pagination_params(since_id: pagination_since_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@lists.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@lists.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def records_continue?
|
||||||
|
@lists.size == limit_param(LISTS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.permit(:limit).merge(core_params)
|
||||||
|
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],
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Api::V1::Timelines::HomeController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_home_feed
|
def account_home_feed
|
||||||
Feed.new(:home, current_account)
|
HomeFeed.new(current_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_pagination_headers
|
def insert_pagination_headers
|
||||||
|
|||||||
66
app/controllers/api/v1/timelines/list_controller.rb
Normal file
66
app/controllers/api/v1/timelines/list_controller.rb
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Timelines::ListController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_list
|
||||||
|
before_action :set_statuses
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @statuses,
|
||||||
|
each_serializer: REST::StatusSerializer,
|
||||||
|
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_list
|
||||||
|
@list = List.where(account: current_account).find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_statuses
|
||||||
|
@statuses = cached_list_statuses
|
||||||
|
end
|
||||||
|
|
||||||
|
def cached_list_statuses
|
||||||
|
cache_collection list_statuses, Status
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_statuses
|
||||||
|
list_feed.get(
|
||||||
|
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||||
|
params[:max_id],
|
||||||
|
params[:since_id]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_feed
|
||||||
|
ListFeed.new(@list)
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.permit(:limit).merge(core_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_timelines_list_url params[:id], pagination_params(max_id: pagination_max_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_timelines_list_url params[:id], pagination_params(since_id: pagination_since_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@statuses.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@statuses.first.id
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,11 +13,13 @@ class ApplicationController < ActionController::Base
|
|||||||
helper_method :current_account
|
helper_method :current_account
|
||||||
helper_method :current_session
|
helper_method :current_session
|
||||||
helper_method :current_theme
|
helper_method :current_theme
|
||||||
|
helper_method :theme_data
|
||||||
helper_method :single_user_mode?
|
helper_method :single_user_mode?
|
||||||
|
|
||||||
rescue_from ActionController::RoutingError, with: :not_found
|
rescue_from ActionController::RoutingError, with: :not_found
|
||||||
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
||||||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||||
|
rescue_from Mastodon::NotPermittedError, with: :forbidden
|
||||||
|
|
||||||
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
|
||||||
before_action :check_suspension, if: :user_signed_in?
|
before_action :check_suspension, if: :user_signed_in?
|
||||||
@@ -40,6 +42,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
|
||||||
@@ -83,6 +89,10 @@ class ApplicationController < ActionController::Base
|
|||||||
current_user.setting_theme
|
current_user.setting_theme
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def theme_data
|
||||||
|
Themes.instance.get(current_theme)
|
||||||
|
end
|
||||||
|
|
||||||
def cache_collection(raw, klass)
|
def cache_collection(raw, klass)
|
||||||
return raw unless klass.respond_to?(:with_includes)
|
return raw unless klass.respond_to?(:with_includes)
|
||||||
|
|
||||||
@@ -99,7 +109,7 @@ class ApplicationController < ActionController::Base
|
|||||||
unless uncached_ids.empty?
|
unless uncached_ids.empty?
|
||||||
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
|
uncached = klass.where(id: uncached_ids).with_includes.map { |item| [item.id, item] }.to_h
|
||||||
|
|
||||||
uncached.values.each do |item|
|
uncached.each_value do |item|
|
||||||
Rails.cache.write(item.cache_key, item)
|
Rails.cache.write(item.cache_key, item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
|
|
||||||
def build_resource(hash = nil)
|
def build_resource(hash = nil)
|
||||||
super(hash)
|
super(hash)
|
||||||
|
|
||||||
resource.locale = I18n.locale
|
resource.locale = I18n.locale
|
||||||
|
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||||
|
|
||||||
resource.build_account if resource.account.nil?
|
resource.build_account if resource.account.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_sign_up_params
|
def configure_sign_up_params
|
||||||
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
devise_parameter_sanitizer.permit(:sign_up) do |u|
|
||||||
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation)
|
u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,7 +38,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def check_enabled_registrations
|
def check_enabled_registrations
|
||||||
redirect_to root_path if single_user_mode? || !Setting.open_registrations
|
redirect_to root_path if single_user_mode? || !allowed_registrations?
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed_registrations?
|
||||||
|
Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def invite_code
|
||||||
|
if params[:user]
|
||||||
|
params[:user][:invite_code]
|
||||||
|
else
|
||||||
|
params[:invite_code]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class Auth::SessionsController < Devise::SessionsController
|
|||||||
|
|
||||||
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
if user_params[:otp_attempt].present? && session[:otp_user_id]
|
||||||
authenticate_with_two_factor_via_otp(user)
|
authenticate_with_two_factor_via_otp(user)
|
||||||
elsif user && user.valid_password?(user_params[:password])
|
elsif user&.valid_password?(user_params[:password])
|
||||||
prompt_for_two_factor(user)
|
prompt_for_two_factor(user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
9
app/controllers/concerns/accountable_concern.rb
Normal file
9
app/controllers/concerns/accountable_concern.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AccountableConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def log_action(action, target)
|
||||||
|
Admin::ActionLog.create(account: current_account, action: action, target: target)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
module Authorization
|
module Authorization
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include Pundit
|
include Pundit
|
||||||
|
|
||||||
def pundit_user
|
def pundit_user
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ class HomeController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@body_classes = 'app-body'
|
@body_classes = 'app-body'
|
||||||
@frontend = (params[:frontend] and Rails.configuration.x.available_frontends.include? params[:frontend] + '.js') ? params[:frontend] : 'mastodon'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
43
app/controllers/invites_controller.rb
Normal file
43
app/controllers/invites_controller.rb
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class InvitesController < ApplicationController
|
||||||
|
include Authorization
|
||||||
|
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :invite, :create?
|
||||||
|
|
||||||
|
@invites = Invite.where(user: current_user)
|
||||||
|
@invite = Invite.new(expires_in: 1.day.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize :invite, :create?
|
||||||
|
|
||||||
|
@invite = Invite.new(resource_params)
|
||||||
|
@invite.user = current_user
|
||||||
|
|
||||||
|
if @invite.save
|
||||||
|
redirect_to invites_path
|
||||||
|
else
|
||||||
|
@invites = Invite.where(user: current_user)
|
||||||
|
render :index
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@invite = Invite.where(user: current_user).find(params[:id])
|
||||||
|
authorize @invite, :destroy?
|
||||||
|
@invite.expire!
|
||||||
|
redirect_to invites_path
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.require(:invite).permit(:max_uses, :expires_in)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
|
|||||||
103
app/helpers/admin/action_logs_helper.rb
Normal file
103
app/helpers/admin/action_logs_helper.rb
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::ActionLogsHelper
|
||||||
|
def log_target(log)
|
||||||
|
if log.target
|
||||||
|
linkable_log_target(log.target)
|
||||||
|
else
|
||||||
|
log_target_from_history(log.target_type, log.recorded_changes)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def linkable_log_target(record)
|
||||||
|
case record.class.name
|
||||||
|
when 'Account'
|
||||||
|
link_to record.acct, admin_account_path(record.id)
|
||||||
|
when 'User'
|
||||||
|
link_to record.account.acct, admin_account_path(record.account_id)
|
||||||
|
when 'CustomEmoji'
|
||||||
|
record.shortcode
|
||||||
|
when 'Report'
|
||||||
|
link_to "##{record.id}", admin_report_path(record)
|
||||||
|
when 'DomainBlock', 'EmailDomainBlock'
|
||||||
|
link_to record.domain, "https://#{record.domain}"
|
||||||
|
when 'Status'
|
||||||
|
link_to record.account.acct, TagManager.instance.url_for(record)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_target_from_history(type, attributes)
|
||||||
|
case type
|
||||||
|
when 'CustomEmoji'
|
||||||
|
attributes['shortcode']
|
||||||
|
when 'DomainBlock', 'EmailDomainBlock'
|
||||||
|
link_to attributes['domain'], "https://#{attributes['domain']}"
|
||||||
|
when 'Status'
|
||||||
|
tmp_status = Status.new(attributes)
|
||||||
|
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relevant_log_changes(log)
|
||||||
|
if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
|
||||||
|
log.recorded_changes.slice('domain')
|
||||||
|
elsif log.target_type == 'CustomEmoji' && log.action == :update
|
||||||
|
log.recorded_changes.slice('domain', 'visible_in_picker')
|
||||||
|
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
|
||||||
|
log.recorded_changes.slice('moderator', 'admin')
|
||||||
|
elsif log.target_type == 'DomainBlock'
|
||||||
|
log.recorded_changes.slice('severity', 'reject_media')
|
||||||
|
elsif log.target_type == 'Status' && log.action == :update
|
||||||
|
log.recorded_changes.slice('sensitive')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_extra_attributes(hash)
|
||||||
|
safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_change(val)
|
||||||
|
return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
|
||||||
|
safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
|
||||||
|
end
|
||||||
|
|
||||||
|
def icon_for_log(log)
|
||||||
|
case log.target_type
|
||||||
|
when 'Account', 'User'
|
||||||
|
'user'
|
||||||
|
when 'CustomEmoji'
|
||||||
|
'file'
|
||||||
|
when 'Report'
|
||||||
|
'flag'
|
||||||
|
when 'DomainBlock'
|
||||||
|
'lock'
|
||||||
|
when 'EmailDomainBlock'
|
||||||
|
'envelope'
|
||||||
|
when 'Status'
|
||||||
|
'pencil'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_for_log_icon(log)
|
||||||
|
case log.action
|
||||||
|
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
|
||||||
|
'positive'
|
||||||
|
when :create
|
||||||
|
opposite_verbs?(log) ? 'negative' : 'positive'
|
||||||
|
when :update, :reset_password, :disable_2fa, :memorialize
|
||||||
|
'neutral'
|
||||||
|
when :demote, :silence, :disable, :suspend
|
||||||
|
'negative'
|
||||||
|
when :destroy
|
||||||
|
opposite_verbs?(log) ? 'positive' : 'negative'
|
||||||
|
else
|
||||||
|
''
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def opposite_verbs?(log)
|
||||||
|
%w(DomainBlock EmailDomainBlock).include?(log.target_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,7 +18,7 @@ module Admin::FilterHelper
|
|||||||
|
|
||||||
def selected?(more_params)
|
def selected?(more_params)
|
||||||
new_url = filtered_url_for(more_params)
|
new_url = filtered_url_for(more_params)
|
||||||
filter_link_class(new_url) == 'selected' ? true : false
|
filter_link_class(new_url) == 'selected'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`actions/local_settings`
|
|
||||||
========================
|
|
||||||
|
|
||||||
> For more information on the contents of this file, please contact:
|
|
||||||
>
|
|
||||||
> - kibigo! [@kibi@glitch.social]
|
|
||||||
|
|
||||||
This file provides our Redux actions related to local settings. It
|
|
||||||
consists of the following:
|
|
||||||
|
|
||||||
- __`changesLocalSetting(key, value)` :__
|
|
||||||
Changes the local setting with the given `key` to the given
|
|
||||||
`value`. `key` **MUST** be an array of strings, as required by
|
|
||||||
`Immutable.Map.prototype.getIn()`.
|
|
||||||
|
|
||||||
- __`saveLocalSettings()` :__
|
|
||||||
Saves the local settings to `localStorage` as a JSON object. We
|
|
||||||
shouldn't ever need to call this ourselves.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Constants:
|
|
||||||
----------
|
|
||||||
|
|
||||||
We provide the following constants:
|
|
||||||
|
|
||||||
- __`LOCAL_SETTING_CHANGE` :__
|
|
||||||
This string constant is used to dispatch a setting change to our
|
|
||||||
reducer in `reducers/local_settings`, where the setting is
|
|
||||||
actually changed.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
`changeLocalSetting(key, value)`:
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
Changes the local setting with the given `key` to the given `value`.
|
|
||||||
`key` **MUST** be an array of strings, as required by
|
|
||||||
`Immutable.Map.prototype.getIn()`.
|
|
||||||
|
|
||||||
To accomplish this, we just dispatch a `LOCAL_SETTING_CHANGE` to our
|
|
||||||
reducer in `reducers/local_settings`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function changeLocalSetting(key, value) {
|
|
||||||
return dispatch => {
|
|
||||||
dispatch({
|
|
||||||
type: LOCAL_SETTING_CHANGE,
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(saveLocalSettings());
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
`saveLocalSettings()`:
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
Saves the local settings to `localStorage` as a JSON object.
|
|
||||||
`changeLocalSetting()` calls this whenever it changes a setting. We
|
|
||||||
shouldn't ever need to call this ourselves.
|
|
||||||
|
|
||||||
> __TODO :__
|
|
||||||
> Right now `saveLocalSettings()` doesn't keep track of which user
|
|
||||||
> is currently signed in, but it might be better to give each user
|
|
||||||
> their *own* local settings.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function saveLocalSettings() {
|
|
||||||
return (_, getState) => {
|
|
||||||
const localSettings = getState().get('local_settings').toJS();
|
|
||||||
localStorage.setItem('mastodon-settings', JSON.stringify(localSettings));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<AccountHeader>`
|
|
||||||
=================
|
|
||||||
|
|
||||||
> For more information on the contents of this file, please contact:
|
|
||||||
>
|
|
||||||
> - kibigo! [@kibi@glitch.social]
|
|
||||||
|
|
||||||
Original file by @gargron@mastodon.social et al as part of
|
|
||||||
tootsuite/mastodon. We've expanded it in order to handle user bio
|
|
||||||
frontmatter.
|
|
||||||
|
|
||||||
The `<AccountHeader>` component provides the header for account
|
|
||||||
timelines. It is a fairly simple component which mostly just consists
|
|
||||||
of a `render()` method.
|
|
||||||
|
|
||||||
__Props:__
|
|
||||||
|
|
||||||
- __`account` (`ImmutablePropTypes.map`) :__
|
|
||||||
The account to render a header for.
|
|
||||||
|
|
||||||
- __`me` (`PropTypes.number.isRequired`) :__
|
|
||||||
The id of the currently-signed-in account.
|
|
||||||
|
|
||||||
- __`onFollow` (`PropTypes.func.isRequired`) :__
|
|
||||||
The function to call when the user clicks the "follow" button.
|
|
||||||
|
|
||||||
- __`intl` (`PropTypes.object.isRequired`) :__
|
|
||||||
Our internationalization object, inserted by `@injectIntl`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import emojify from '../../../mastodon/features/emoji/emoji';
|
|
||||||
import IconButton from '../../../mastodon/components/icon_button';
|
|
||||||
import Avatar from '../../../mastodon/components/avatar';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import { processBio } from '../../util/bio_metadata';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Inital setup:
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The `messages` constant is used to define any messages that we need
|
|
||||||
from inside props. In our case, these are the `unfollow`, `follow`, and
|
|
||||||
`requested` messages used in the `title` of our buttons.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
|
||||||
});
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
---------------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
export default class AccountHeader extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account : ImmutablePropTypes.map,
|
|
||||||
me : PropTypes.string.isRequired,
|
|
||||||
onFollow : PropTypes.func.isRequired,
|
|
||||||
intl : PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
### `render()`
|
|
||||||
|
|
||||||
The `render()` function is used to render our component.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, me, intl } = this.props;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
If no `account` is provided, then we can't render a header. Otherwise,
|
|
||||||
we get the `displayName` for the account, if available. If it's blank,
|
|
||||||
then we set the `displayName` to just be the `username` of the account.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let displayName = account.get('display_name_html');
|
|
||||||
let info = '';
|
|
||||||
let actionBtn = '';
|
|
||||||
let following = false;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Next, we handle the account relationships. If the account follows the
|
|
||||||
user, then we add an `info` message. If the user has requested a
|
|
||||||
follow, then we disable the `actionBtn` and display an hourglass.
|
|
||||||
Otherwise, if the account isn't blocked, we set the `actionBtn` to the
|
|
||||||
appropriate icon.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
|
||||||
if (account.getIn(['relationship', 'followed_by'])) {
|
|
||||||
info = (
|
|
||||||
<span className='account--follows-info'>
|
|
||||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
actionBtn = (
|
|
||||||
<div className='account--action-button'>
|
|
||||||
<IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
|
||||||
following = account.getIn(['relationship', 'following']);
|
|
||||||
actionBtn = (
|
|
||||||
<div className='account--action-button'>
|
|
||||||
<IconButton
|
|
||||||
size={26}
|
|
||||||
icon={following ? 'user-times' : 'user-plus'}
|
|
||||||
active={following}
|
|
||||||
title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
|
|
||||||
onClick={this.props.onFollow}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
we extract the `text` and
|
|
||||||
`metadata` from our account's `note` using `processBio()`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { text, metadata } = processBio(account.get('note'));
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Here, we render our component using all the things we've defined above.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='account__header__wrapper'>
|
|
||||||
<div
|
|
||||||
className='account__header'
|
|
||||||
style={{ backgroundImage: `url(${account.get('header')})` }}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<a href={account.get('url')} target='_blank' rel='noopener'>
|
|
||||||
<span className='account__header__avatar'>
|
|
||||||
<Avatar account={account} size={90} />
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className='account__header__display-name'
|
|
||||||
dangerouslySetInnerHTML={{ __html: displayName }}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<span className='account__header__username'>
|
|
||||||
@{account.get('acct')}
|
|
||||||
{account.get('locked') ? <i className='fa fa-lock' /> : null}
|
|
||||||
</span>
|
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
|
|
||||||
|
|
||||||
{info}
|
|
||||||
{actionBtn}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{metadata.length && (
|
|
||||||
<table className='account__metadata'>
|
|
||||||
<tbody>
|
|
||||||
{(() => {
|
|
||||||
let data = [];
|
|
||||||
for (let i = 0; i < metadata.length; i++) {
|
|
||||||
data.push(
|
|
||||||
<tr key={i}>
|
|
||||||
<th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th>
|
|
||||||
<td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
) || null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<ComposeAdvancedOptionsContainer>`
|
|
||||||
===================================
|
|
||||||
|
|
||||||
This container connects `<ComposeAdvancedOptions>` to the Redux store.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import ComposeAdvancedOptions from '.';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
State mapping:
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The `mapStateToProps()` function maps various state properties to the
|
|
||||||
props of our component. The only property we care about is
|
|
||||||
`compose.advanced_options`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
values: state.getIn(['compose', 'advanced_options']),
|
|
||||||
});
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Dispatch mapping:
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The `mapDispatchToProps()` function maps dispatches to our store to the
|
|
||||||
various props of our component. We just need to provide a dispatch for
|
|
||||||
when an advanced option toggle changes.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
|
|
||||||
onChange (option) {
|
|
||||||
dispatch(toggleComposeAdvancedOption(option));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions);
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<ComposeAdvancedOptions>`
|
|
||||||
==========================
|
|
||||||
|
|
||||||
> For more information on the contents of this file, please contact:
|
|
||||||
>
|
|
||||||
> - surinna [@srn@dev.glitch.social]
|
|
||||||
|
|
||||||
This adds an advanced options dropdown to the toot compose box, for
|
|
||||||
toggles that don't necessarily fit elsewhere.
|
|
||||||
|
|
||||||
__Props:__
|
|
||||||
|
|
||||||
- __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__
|
|
||||||
An Immutable map with the following values:
|
|
||||||
|
|
||||||
- __`do_not_federate` (`PropTypes.bool.isRequired`) :__
|
|
||||||
Specifies whether or not to federate the status.
|
|
||||||
|
|
||||||
- __`onChange` (`PropTypes.func.isRequired`) :__
|
|
||||||
The function to call when a toggle is changed. We pass this from
|
|
||||||
our container to the toggle.
|
|
||||||
|
|
||||||
- __`intl` (`PropTypes.object.isRequired`) :__
|
|
||||||
Our internationalization object, inserted by `@injectIntl`.
|
|
||||||
|
|
||||||
__State:__
|
|
||||||
|
|
||||||
- __`open` :__
|
|
||||||
This tells whether the dropdown is currently open or closed.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import ComposeAdvancedOptionsToggle from './toggle';
|
|
||||||
import ComposeDropdown from '../dropdown/index';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Inital setup:
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The `messages` constant is used to define any messages that we need
|
|
||||||
from inside props. These are the various titles and labels on our
|
|
||||||
toggles.
|
|
||||||
|
|
||||||
`iconStyle` styles the icon used for the dropdown button.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
local_only_short :
|
|
||||||
{ id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' },
|
|
||||||
local_only_long :
|
|
||||||
{ id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' },
|
|
||||||
advanced_options_icon_title :
|
|
||||||
{ id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' },
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
---------------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
export default class ComposeAdvancedOptions extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
values : ImmutablePropTypes.contains({
|
|
||||||
do_not_federate : PropTypes.bool.isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
onChange : PropTypes.func.isRequired,
|
|
||||||
intl : PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
### `render()`
|
|
||||||
|
|
||||||
`render()` actually puts our component on the screen.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, values } = this.props;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
The `options` array provides all of the available advanced options
|
|
||||||
alongside their icon, text, and name.
|
|
||||||
|
|
||||||
*/
|
|
||||||
const options = [
|
|
||||||
{ icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
`anyEnabled` tells us if any of our advanced options have been enabled.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const anyEnabled = values.some((enabled) => enabled);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
`optionElems` takes our `options` and creates
|
|
||||||
`<ComposeAdvancedOptionsToggle>`s out of them. We use the `name` of the
|
|
||||||
toggle as its `key` so that React can keep track of it.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const optionElems = options.map((option) => {
|
|
||||||
return (
|
|
||||||
<ComposeAdvancedOptionsToggle
|
|
||||||
onChange={this.props.onChange}
|
|
||||||
active={values.get(option.name)}
|
|
||||||
key={option.name}
|
|
||||||
name={option.name}
|
|
||||||
shortText={intl.formatMessage(option.shortText)}
|
|
||||||
longText={intl.formatMessage(option.longText)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Finally, we can render our component.
|
|
||||||
|
|
||||||
*/
|
|
||||||
return (
|
|
||||||
<ComposeDropdown
|
|
||||||
title={intl.formatMessage(messages.advanced_options_icon_title)}
|
|
||||||
icon='home'
|
|
||||||
highlight={anyEnabled}
|
|
||||||
>
|
|
||||||
{optionElems}
|
|
||||||
</ComposeDropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<ComposeAdvancedOptionsToggle>`
|
|
||||||
================================
|
|
||||||
|
|
||||||
> For more information on the contents of this file, please contact:
|
|
||||||
>
|
|
||||||
> - surinna [@srn@dev.glitch.social]
|
|
||||||
|
|
||||||
This creates the toggle used by `<ComposeAdvancedOptions>`.
|
|
||||||
|
|
||||||
__Props:__
|
|
||||||
|
|
||||||
- __`onChange` (`PropTypes.func`) :__
|
|
||||||
This provides the function to call when the toggle is
|
|
||||||
(de-?)activated.
|
|
||||||
|
|
||||||
- __`active` (`PropTypes.bool`) :__
|
|
||||||
This prop controls whether the toggle is currently active or not.
|
|
||||||
|
|
||||||
- __`name` (`PropTypes.string`) :__
|
|
||||||
This identifies the toggle, and is sent to `onChange()` when it is
|
|
||||||
called.
|
|
||||||
|
|
||||||
- __`shortText` (`PropTypes.string`) :__
|
|
||||||
This is a short string used as the title of the toggle.
|
|
||||||
|
|
||||||
- __`longText` (`PropTypes.string`) :__
|
|
||||||
This is a longer string used as a subtitle for the toggle.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
---------------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class ComposeAdvancedOptionsToggle extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
active: PropTypes.bool.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
shortText: PropTypes.string.isRequired,
|
|
||||||
longText: PropTypes.string.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
### `onToggle()`
|
|
||||||
|
|
||||||
The `onToggle()` function simply calls the `onChange()` prop with the
|
|
||||||
toggle's `name`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
onToggle = () => {
|
|
||||||
this.props.onChange(this.props.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
### `render()`
|
|
||||||
|
|
||||||
The `render()` function is used to render our component. We just render
|
|
||||||
a `<Toggle>` and place next to it our text.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { active, shortText, longText } = this.props;
|
|
||||||
return (
|
|
||||||
<div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}>
|
|
||||||
<div className='advanced-options-dropdown__option__toggle'>
|
|
||||||
<Toggle checked={active} onChange={this.onToggle} />
|
|
||||||
</div>
|
|
||||||
<div className='advanced-options-dropdown__option__content'>
|
|
||||||
<strong>{shortText}</strong>
|
|
||||||
{longText}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import { closeModal } from '../../../mastodon/actions/modal';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import { changeLocalSetting } from '../../../glitch/actions/local_settings';
|
|
||||||
import LocalSettings from '.';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
settings: state.get('local_settings'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onChange (setting, value) {
|
|
||||||
dispatch(changeLocalSetting(setting, value));
|
|
||||||
},
|
|
||||||
onClose () {
|
|
||||||
dispatch(closeModal());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(LocalSettings);
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<NotificationContainer>`
|
|
||||||
=========================
|
|
||||||
|
|
||||||
This container connects `<Notification>`s to the Redux store.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import Notification from '.';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
// replace account id with object
|
|
||||||
let leNotif = props.notification.set('account', state.getIn(['accounts', props.notification.get('account')]));
|
|
||||||
|
|
||||||
// populate markedForDelete from state - is mysteriously lost somewhere
|
|
||||||
for (let n of state.getIn(['notifications', 'items'])) {
|
|
||||||
if (n.get('id') === props.notification.get('id')) {
|
|
||||||
leNotif = leNotif.set('markedForDelete', n.get('markedForDelete'));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ({
|
|
||||||
notification: leNotif,
|
|
||||||
settings: state.get('local_settings'),
|
|
||||||
notifCleaning: state.getIn(['notifications', 'cleaningMode']),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(Notification);
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
// `<NotificationFollow>`
|
|
||||||
// ======================
|
|
||||||
|
|
||||||
// * * * * * * * //
|
|
||||||
|
|
||||||
// Imports
|
|
||||||
// -------
|
|
||||||
|
|
||||||
// Package imports.
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
// Mastodon imports.
|
|
||||||
import Permalink from '../../../mastodon/components/permalink';
|
|
||||||
import AccountContainer from '../../../mastodon/containers/account_container';
|
|
||||||
|
|
||||||
// Our imports.
|
|
||||||
import NotificationOverlayContainer from '../notification/overlay/container';
|
|
||||||
|
|
||||||
// * * * * * * * //
|
|
||||||
|
|
||||||
// Implementation
|
|
||||||
// --------------
|
|
||||||
|
|
||||||
export default class NotificationFollow extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
id : PropTypes.string.isRequired,
|
|
||||||
account : ImmutablePropTypes.map.isRequired,
|
|
||||||
notification : ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, notification } = this.props;
|
|
||||||
|
|
||||||
// Links to the display name.
|
|
||||||
const displayName = account.get('display_name_html') || account.get('username');
|
|
||||||
const link = (
|
|
||||||
<Permalink
|
|
||||||
className='notification__display-name'
|
|
||||||
href={account.get('url')}
|
|
||||||
title={account.get('acct')}
|
|
||||||
to={`/accounts/${account.get('id')}`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: displayName }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Renders.
|
|
||||||
return (
|
|
||||||
<div className='notification notification-follow'>
|
|
||||||
<div className='notification__message'>
|
|
||||||
<div className='notification__favourite-icon-wrapper'>
|
|
||||||
<i className='fa fa-fw fa-user-plus' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormattedMessage
|
|
||||||
id='notification.follow'
|
|
||||||
defaultMessage='{name} followed you'
|
|
||||||
values={{ name: link }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} withNote={false} />
|
|
||||||
<NotificationOverlayContainer notification={notification} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import StatusContainer from '../status/container';
|
|
||||||
import NotificationFollow from './follow';
|
|
||||||
|
|
||||||
export default class Notification extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
notification: ImmutablePropTypes.map.isRequired,
|
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
renderFollow (notification) {
|
|
||||||
return (
|
|
||||||
<NotificationFollow
|
|
||||||
id={notification.get('id')}
|
|
||||||
account={notification.get('account')}
|
|
||||||
notification={notification}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderMention (notification) {
|
|
||||||
return (
|
|
||||||
<StatusContainer
|
|
||||||
id={notification.get('status')}
|
|
||||||
notification={notification}
|
|
||||||
withDismiss
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFavourite (notification) {
|
|
||||||
return (
|
|
||||||
<StatusContainer
|
|
||||||
id={notification.get('status')}
|
|
||||||
account={notification.get('account')}
|
|
||||||
prepend='favourite'
|
|
||||||
muted
|
|
||||||
notification={notification}
|
|
||||||
withDismiss
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderReblog (notification) {
|
|
||||||
return (
|
|
||||||
<StatusContainer
|
|
||||||
id={notification.get('status')}
|
|
||||||
account={notification.get('account')}
|
|
||||||
prepend='reblog'
|
|
||||||
muted
|
|
||||||
notification={notification}
|
|
||||||
withDismiss
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { notification } = this.props;
|
|
||||||
|
|
||||||
switch(notification.get('type')) {
|
|
||||||
case 'follow':
|
|
||||||
return this.renderFollow(notification);
|
|
||||||
case 'mention':
|
|
||||||
return this.renderMention(notification);
|
|
||||||
case 'favourite':
|
|
||||||
return this.renderFavourite(notification);
|
|
||||||
case 'reblog':
|
|
||||||
return this.renderReblog(notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<NotificationOverlayContainer>`
|
|
||||||
=========================
|
|
||||||
|
|
||||||
This container connects `<NotificationOverlay>`s to the Redux store.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import NotificationOverlay from './notification_overlay';
|
|
||||||
import { markNotificationForDelete } from '../../../../mastodon/actions/notifications';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Dispatch mapping:
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The `mapDispatchToProps()` function maps dispatches to our store to the
|
|
||||||
various props of our component. We only need to provide a dispatch for
|
|
||||||
deleting notifications.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
onMarkForDelete(id, yes) {
|
|
||||||
dispatch(markNotificationForDelete(id, yes));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
show: state.getIn(['notifications', 'cleaningMode']),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(NotificationOverlay);
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<StatusContainer>`
|
|
||||||
===================
|
|
||||||
|
|
||||||
Original file by @gargron@mastodon.social et al as part of
|
|
||||||
tootsuite/mastodon. Documentation by @kibi@glitch.social. The code
|
|
||||||
detecting reblogs has been moved here from <Status>.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import {
|
|
||||||
defineMessages,
|
|
||||||
injectIntl,
|
|
||||||
FormattedMessage,
|
|
||||||
} from 'react-intl';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import { makeGetStatus } from '../../../mastodon/selectors';
|
|
||||||
import {
|
|
||||||
replyCompose,
|
|
||||||
mentionCompose,
|
|
||||||
} from '../../../mastodon/actions/compose';
|
|
||||||
import {
|
|
||||||
reblog,
|
|
||||||
favourite,
|
|
||||||
unreblog,
|
|
||||||
unfavourite,
|
|
||||||
pin,
|
|
||||||
unpin,
|
|
||||||
} from '../../../mastodon/actions/interactions';
|
|
||||||
import { blockAccount } from '../../../mastodon/actions/accounts';
|
|
||||||
import { initMuteModal } from '../../../mastodon/actions/mutes';
|
|
||||||
import {
|
|
||||||
muteStatus,
|
|
||||||
unmuteStatus,
|
|
||||||
deleteStatus,
|
|
||||||
} from '../../../mastodon/actions/statuses';
|
|
||||||
import { initReport } from '../../../mastodon/actions/reports';
|
|
||||||
import { openModal } from '../../../mastodon/actions/modal';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import Status from '.';
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Inital setup:
|
|
||||||
-------------
|
|
||||||
|
|
||||||
The `messages` constant is used to define any messages that we will
|
|
||||||
need in our component. In our case, these are the various confirmation
|
|
||||||
messages used with statuses.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
deleteConfirm : {
|
|
||||||
id : 'confirmations.delete.confirm',
|
|
||||||
defaultMessage : 'Delete',
|
|
||||||
},
|
|
||||||
deleteMessage : {
|
|
||||||
id : 'confirmations.delete.message',
|
|
||||||
defaultMessage : 'Are you sure you want to delete this status?',
|
|
||||||
},
|
|
||||||
blockConfirm : {
|
|
||||||
id : 'confirmations.block.confirm',
|
|
||||||
defaultMessage : 'Block',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
State mapping:
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The `mapStateToProps()` function maps various state properties to the
|
|
||||||
props of our component. We wrap this in a `makeMapStateToProps()`
|
|
||||||
function to give us closure and preserve `getStatus()` across function
|
|
||||||
calls.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
|
||||||
|
|
||||||
let status = getStatus(state, ownProps.id);
|
|
||||||
|
|
||||||
if(status === null) {
|
|
||||||
console.error(`ERROR! NULL STATUS! ${ownProps.id}`);
|
|
||||||
// work-around: find first good status
|
|
||||||
for (let k of state.get('statuses').keys()) {
|
|
||||||
status = getStatus(state, k);
|
|
||||||
if (status !== null) break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let reblogStatus = status.get('reblog', null);
|
|
||||||
let account = undefined;
|
|
||||||
let prepend = undefined;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Here we process reblogs. If our status is a reblog, then we create a
|
|
||||||
`prependMessage` to pass along to our `<Status>` along with the
|
|
||||||
reblogger's `account`, and set `coreStatus` (the one we will actually
|
|
||||||
render) to the status which has been reblogged.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (reblogStatus !== null && typeof reblogStatus === 'object') {
|
|
||||||
account = status.get('account');
|
|
||||||
status = reblogStatus;
|
|
||||||
prepend = 'reblogged_by';
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Here are the props we pass to `<Status>`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
return {
|
|
||||||
status : status,
|
|
||||||
account : account || ownProps.account,
|
|
||||||
me : state.getIn(['meta', 'me']),
|
|
||||||
settings : state.get('local_settings'),
|
|
||||||
prepend : prepend || ownProps.prepend,
|
|
||||||
reblogModal : state.getIn(['meta', 'boost_modal']),
|
|
||||||
deleteModal : state.getIn(['meta', 'delete_modal']),
|
|
||||||
autoPlayGif : state.getIn(['meta', 'auto_play_gif']),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Dispatch mapping:
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
The `mapDispatchToProps()` function maps dispatches to our store to the
|
|
||||||
various props of our component. We need to provide dispatches for all
|
|
||||||
of the things you can do with a status: reply, reblog, favourite, et
|
|
||||||
cetera.
|
|
||||||
|
|
||||||
For a few of these dispatches, we open up confirmation modals; the rest
|
|
||||||
just immediately execute their corresponding actions.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
|
||||||
|
|
||||||
onReply (status, router) {
|
|
||||||
dispatch(replyCompose(status, router));
|
|
||||||
},
|
|
||||||
|
|
||||||
onModalReblog (status) {
|
|
||||||
dispatch(reblog(status));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblog (status, e) {
|
|
||||||
if (status.get('reblogged')) {
|
|
||||||
dispatch(unreblog(status));
|
|
||||||
} else {
|
|
||||||
if (e.shiftKey || !this.reblogModal) {
|
|
||||||
this.onModalReblog(status);
|
|
||||||
} else {
|
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status) {
|
|
||||||
if (status.get('favourited')) {
|
|
||||||
dispatch(unfavourite(status));
|
|
||||||
} else {
|
|
||||||
dispatch(favourite(status));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onPin (status) {
|
|
||||||
if (status.get('pinned')) {
|
|
||||||
dispatch(unpin(status));
|
|
||||||
} else {
|
|
||||||
dispatch(pin(status));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onEmbed (status) {
|
|
||||||
dispatch(openModal('EMBED', { url: status.get('url') }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDelete (status) {
|
|
||||||
if (!this.deleteModal) {
|
|
||||||
dispatch(deleteStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: intl.formatMessage(messages.deleteMessage),
|
|
||||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
|
||||||
onConfirm: () => dispatch(deleteStatus(status.get('id'))),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onMention (account, router) {
|
|
||||||
dispatch(mentionCompose(account, router));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenMedia (media, index) {
|
|
||||||
dispatch(openModal('MEDIA', { media, index }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenVideo (media, time) {
|
|
||||||
dispatch(openModal('VIDEO', { media, time }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlock (account) {
|
|
||||||
dispatch(openModal('CONFIRM', {
|
|
||||||
message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.blockConfirm),
|
|
||||||
onConfirm: () => dispatch(blockAccount(account.get('id'))),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReport (status) {
|
|
||||||
dispatch(initReport(status.get('account'), status));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMute (account) {
|
|
||||||
dispatch(initMuteModal(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMuteConversation (status) {
|
|
||||||
if (status.get('muted')) {
|
|
||||||
dispatch(unmuteStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(muteStatus(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(
|
|
||||||
connect(makeMapStateToProps, mapDispatchToProps)(Status)
|
|
||||||
);
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import IconButton from '../../../../mastodon/components/icon_button';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import StatusGalleryItem from './item';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
|
||||||
});
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
export default class StatusGallery extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
sensitive: PropTypes.bool,
|
|
||||||
media: ImmutablePropTypes.list.isRequired,
|
|
||||||
letterbox: PropTypes.bool,
|
|
||||||
fullwidth: PropTypes.bool,
|
|
||||||
height: PropTypes.number.isRequired,
|
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
autoPlayGif: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
visible: !this.props.sensitive,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpen = () => {
|
|
||||||
this.setState({ visible: !this.state.visible });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = (index) => {
|
|
||||||
this.props.onOpenMedia(this.props.media, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { media, intl, sensitive, letterbox, fullwidth } = this.props;
|
|
||||||
|
|
||||||
let children;
|
|
||||||
|
|
||||||
if (!this.state.visible) {
|
|
||||||
let warning;
|
|
||||||
|
|
||||||
if (sensitive) {
|
|
||||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
|
||||||
} else {
|
|
||||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
children = (
|
|
||||||
<div role='button' tabIndex='0' className='media-spoiler' onClick={this.handleOpen}>
|
|
||||||
<span className='media-spoiler__warning'>{warning}</span>
|
|
||||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const size = media.take(4).size;
|
|
||||||
children = media.take(4).map((attachment, i) => <StatusGalleryItem key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} letterbox={letterbox} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`media-gallery ${fullwidth ? 'full-width' : ''}`} style={{ height: `${this.props.height}px` }}>
|
|
||||||
<div className={`spoiler-button ${this.state.visible ? 'spoiler-button--visible' : ''}`}>
|
|
||||||
<IconButton title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} overlay onClick={this.handleOpen} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import { isIOS } from '../../../../mastodon/is_mobile';
|
|
||||||
|
|
||||||
export default class StatusGalleryItem extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
attachment: ImmutablePropTypes.map.isRequired,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
letterbox: PropTypes.bool,
|
|
||||||
onClick: PropTypes.func.isRequired,
|
|
||||||
autoPlayGif: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseEnter = (e) => {
|
|
||||||
if (this.hoverToPlay()) {
|
|
||||||
e.target.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseLeave = (e) => {
|
|
||||||
if (this.hoverToPlay()) {
|
|
||||||
e.target.pause();
|
|
||||||
e.target.currentTime = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hoverToPlay () {
|
|
||||||
const { attachment, autoPlayGif } = this.props;
|
|
||||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = (e) => {
|
|
||||||
const { index, onClick } = this.props;
|
|
||||||
|
|
||||||
if (e.button === 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
onClick(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { attachment, index, size, letterbox } = this.props;
|
|
||||||
|
|
||||||
let width = 50;
|
|
||||||
let height = 100;
|
|
||||||
let top = 'auto';
|
|
||||||
let left = 'auto';
|
|
||||||
let bottom = 'auto';
|
|
||||||
let right = 'auto';
|
|
||||||
|
|
||||||
if (size === 1) {
|
|
||||||
width = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 4 || (size === 3 && index > 0)) {
|
|
||||||
height = 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size === 2) {
|
|
||||||
if (index === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 3) {
|
|
||||||
if (index === 0) {
|
|
||||||
right = '2px';
|
|
||||||
} else if (index > 0) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === 1) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else if (index > 1) {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
} else if (size === 4) {
|
|
||||||
if (index === 0 || index === 2) {
|
|
||||||
right = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index === 1 || index === 3) {
|
|
||||||
left = '2px';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 2) {
|
|
||||||
bottom = '2px';
|
|
||||||
} else {
|
|
||||||
top = '2px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let thumbnail = '';
|
|
||||||
|
|
||||||
if (attachment.get('type') === 'image') {
|
|
||||||
const previewUrl = attachment.get('preview_url');
|
|
||||||
const previewWidth = attachment.getIn(['meta', 'small', 'width']);
|
|
||||||
|
|
||||||
const originalUrl = attachment.get('url');
|
|
||||||
const originalWidth = attachment.getIn(['meta', 'original', 'width']);
|
|
||||||
|
|
||||||
const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`;
|
|
||||||
const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`;
|
|
||||||
|
|
||||||
thumbnail = (
|
|
||||||
<a
|
|
||||||
className='media-gallery__item-thumbnail'
|
|
||||||
href={attachment.get('remote_url') || originalUrl}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
target='_blank'
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={letterbox ? 'letterbox' : ''}
|
|
||||||
src={previewUrl} srcSet={srcSet}
|
|
||||||
sizes={sizes}
|
|
||||||
alt={attachment.get('description')}
|
|
||||||
title={attachment.get('description')}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
|
||||||
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
|
||||||
|
|
||||||
thumbnail = (
|
|
||||||
<div className={`media-gallery__gifv ${autoPlay ? 'autoplay' : ''}`}>
|
|
||||||
<video
|
|
||||||
className={`media-gallery__item-gifv-thumbnail${letterbox ? ' letterbox' : ''}`}
|
|
||||||
role='application'
|
|
||||||
src={attachment.get('url')}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
autoPlay={autoPlay}
|
|
||||||
loop
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className='media-gallery__gifv__label'>GIF</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='media-gallery__item' key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
|
|
||||||
{thumbnail}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,770 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`<Status>`
|
|
||||||
==========
|
|
||||||
|
|
||||||
Original file by @gargron@mastodon.social et al as part of
|
|
||||||
tootsuite/mastodon. *Heavily* rewritten (and documented!) by
|
|
||||||
@kibi@glitch.social as a part of glitch-soc/mastodon. The following
|
|
||||||
features have been added:
|
|
||||||
|
|
||||||
- Better separating the "guts" of statuses from their wrapper(s)
|
|
||||||
- Collapsing statuses
|
|
||||||
- Moving images inside of CWs
|
|
||||||
|
|
||||||
A number of aspects of this original file have been split off into
|
|
||||||
their own components for better maintainance; for these, see:
|
|
||||||
|
|
||||||
- <StatusHeader>
|
|
||||||
- <StatusPrepend>
|
|
||||||
|
|
||||||
…And, of course, the other <Status>-related components as well.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import scheduleIdleTask from '../../../mastodon/features/ui/util/schedule_idle_task';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import StatusPrepend from './prepend';
|
|
||||||
import StatusHeader from './header';
|
|
||||||
import StatusContent from './content';
|
|
||||||
import StatusActionBar from './action_bar';
|
|
||||||
import StatusGallery from './gallery';
|
|
||||||
import StatusPlayer from './player';
|
|
||||||
import NotificationOverlayContainer from '../notification/overlay/container';
|
|
||||||
|
|
||||||
/* * * * */
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
The `<Status>` component:
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
The `<Status>` component is a container for statuses. It consists of a
|
|
||||||
few parts:
|
|
||||||
|
|
||||||
- The `<StatusPrepend>`, which contains tangential information about
|
|
||||||
the status, such as who reblogged it.
|
|
||||||
- The `<StatusHeader>`, which contains the avatar and username of the
|
|
||||||
status author, as well as a media icon and the "collapse" toggle.
|
|
||||||
- The `<StatusContent>`, which contains the content of the status.
|
|
||||||
- The `<StatusActionBar>`, which provides actions to be performed
|
|
||||||
on statuses, like reblogging or sending a reply.
|
|
||||||
|
|
||||||
### Context
|
|
||||||
|
|
||||||
- __`router` (`PropTypes.object`) :__
|
|
||||||
We need to get our router from the surrounding React context.
|
|
||||||
|
|
||||||
### Props
|
|
||||||
|
|
||||||
- __`id` (`PropTypes.number`) :__
|
|
||||||
The id of the status.
|
|
||||||
|
|
||||||
- __`status` (`ImmutablePropTypes.map`) :__
|
|
||||||
The status object, straight from the store.
|
|
||||||
|
|
||||||
- __`account` (`ImmutablePropTypes.map`) :__
|
|
||||||
Don't be confused by this one! This is **not** the account which
|
|
||||||
posted the status, but the associated account with any further
|
|
||||||
action (eg, a reblog or a favourite).
|
|
||||||
|
|
||||||
- __`settings` (`ImmutablePropTypes.map`) :__
|
|
||||||
These are our local settings, fetched from our store. We need this
|
|
||||||
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`,
|
|
||||||
`onMention`, `onMute`, `onMuteConversation`, onBlock`, `onReport`,
|
|
||||||
`onOpenMedia`, `onOpenVideo` (`PropTypes.func`) :__
|
|
||||||
These are all functions passed through from the
|
|
||||||
`<StatusContainer>`. We don't deal with them directly here.
|
|
||||||
|
|
||||||
- __`reblogModal`, `deleteModal` (`PropTypes.bool`) :__
|
|
||||||
These tell whether or not the user has modals activated for
|
|
||||||
reblogging and deleting statuses. They are used by the `onReblog`
|
|
||||||
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`) :__
|
|
||||||
This has nothing to do with a user or conversation mute! "Muted" is
|
|
||||||
what Mastodon internally calls the subdued look of statuses in the
|
|
||||||
notifications column. This should be `true` for notifications, and
|
|
||||||
`false` otherwise.
|
|
||||||
|
|
||||||
- __`collapse` (`PropTypes.bool`) :__
|
|
||||||
This prop signals a directive from a higher power to (un)collapse
|
|
||||||
a status. Most of the time it should be `undefined`, in which case
|
|
||||||
we do nothing.
|
|
||||||
|
|
||||||
- __`prepend` (`PropTypes.string`) :__
|
|
||||||
The type of prepend: `'reblogged_by'`, `'reblog'`, or
|
|
||||||
`'favourite'`.
|
|
||||||
|
|
||||||
- __`withDismiss` (`PropTypes.bool`) :__
|
|
||||||
Whether or not the status can be dismissed. Used for notifications.
|
|
||||||
|
|
||||||
- __`intersectionObserverWrapper` (`PropTypes.object`) :__
|
|
||||||
This holds our intersection observer. In Mastodon parlance,
|
|
||||||
an "intersection" is just when the status is viewable onscreen.
|
|
||||||
|
|
||||||
### State
|
|
||||||
|
|
||||||
- __`isExpanded` :__
|
|
||||||
Should be either `true`, `false`, or `null`. The meanings of
|
|
||||||
these values are as follows:
|
|
||||||
|
|
||||||
- __`true` :__ The status contains a CW and the CW is expanded.
|
|
||||||
- __`false` :__ The status is collapsed.
|
|
||||||
- __`null` :__ The status is not collapsed or expanded.
|
|
||||||
|
|
||||||
- __`isIntersecting` :__
|
|
||||||
This boolean tells us whether or not the status is currently
|
|
||||||
onscreen.
|
|
||||||
|
|
||||||
- __`isHidden` :__
|
|
||||||
This boolean tells us if the status has been unrendered to save
|
|
||||||
CPUs.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default class Status extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router : PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
id : PropTypes.string,
|
|
||||||
status : ImmutablePropTypes.map,
|
|
||||||
account : ImmutablePropTypes.map,
|
|
||||||
settings : ImmutablePropTypes.map,
|
|
||||||
notification : ImmutablePropTypes.map,
|
|
||||||
me : PropTypes.string,
|
|
||||||
onFavourite : PropTypes.func,
|
|
||||||
onReblog : PropTypes.func,
|
|
||||||
onModalReblog : PropTypes.func,
|
|
||||||
onDelete : PropTypes.func,
|
|
||||||
onPin : PropTypes.func,
|
|
||||||
onMention : PropTypes.func,
|
|
||||||
onMute : PropTypes.func,
|
|
||||||
onMuteConversation : PropTypes.func,
|
|
||||||
onBlock : PropTypes.func,
|
|
||||||
onEmbed : PropTypes.func,
|
|
||||||
onHeightChange : PropTypes.func,
|
|
||||||
onReport : PropTypes.func,
|
|
||||||
onOpenMedia : PropTypes.func,
|
|
||||||
onOpenVideo : PropTypes.func,
|
|
||||||
reblogModal : PropTypes.bool,
|
|
||||||
deleteModal : PropTypes.bool,
|
|
||||||
autoPlayGif : PropTypes.bool,
|
|
||||||
muted : PropTypes.bool,
|
|
||||||
collapse : PropTypes.bool,
|
|
||||||
prepend : PropTypes.string,
|
|
||||||
withDismiss : PropTypes.bool,
|
|
||||||
intersectionObserverWrapper : PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
isExpanded : null,
|
|
||||||
isIntersecting : true,
|
|
||||||
isHidden : false,
|
|
||||||
markedForDelete : false,
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
### Implementation
|
|
||||||
|
|
||||||
#### `updateOnProps` and `updateOnStates`.
|
|
||||||
|
|
||||||
`updateOnProps` and `updateOnStates` tell the component when to update.
|
|
||||||
We specify them explicitly because some of our props are dynamically=
|
|
||||||
generated functions, which would otherwise always trigger an update.
|
|
||||||
Of course, this means that if we add an important prop, we will need
|
|
||||||
to remember to specify it here.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
updateOnProps = [
|
|
||||||
'status',
|
|
||||||
'account',
|
|
||||||
'settings',
|
|
||||||
'prepend',
|
|
||||||
'me',
|
|
||||||
'boostModal',
|
|
||||||
'autoPlayGif',
|
|
||||||
'muted',
|
|
||||||
'collapse',
|
|
||||||
'notification',
|
|
||||||
]
|
|
||||||
|
|
||||||
updateOnStates = [
|
|
||||||
'isExpanded',
|
|
||||||
'markedForDelete',
|
|
||||||
]
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `componentWillReceiveProps()`.
|
|
||||||
|
|
||||||
If our settings have changed to disable collapsed statuses, then we
|
|
||||||
need to make sure that we uncollapse every one. We do that by watching
|
|
||||||
for changes to `settings.collapsed.enabled` in
|
|
||||||
`componentWillReceiveProps()`.
|
|
||||||
|
|
||||||
We also need to watch for changes on the `collapse` prop---if this
|
|
||||||
changes to anything other than `undefined`, then we need to collapse or
|
|
||||||
uncollapse our status accordingly.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
|
||||||
if (!nextProps.settings.getIn(['collapsed', 'enabled'])) {
|
|
||||||
if (this.state.isExpanded === false) {
|
|
||||||
this.setExpansion(null);
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
nextProps.collapse !== this.props.collapse &&
|
|
||||||
nextProps.collapse !== undefined
|
|
||||||
) this.setExpansion(nextProps.collapse ? false : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `componentDidMount()`.
|
|
||||||
|
|
||||||
When mounting, we just check to see if our status should be collapsed,
|
|
||||||
and collapse it if so. We don't need to worry about whether collapsing
|
|
||||||
is enabled here, because `setExpansion()` already takes that into
|
|
||||||
account.
|
|
||||||
|
|
||||||
The cases where a status should be collapsed are:
|
|
||||||
|
|
||||||
- The `collapse` prop has been set to `true`
|
|
||||||
- The user has decided in local settings to collapse all statuses.
|
|
||||||
- The user has decided to collapse all notifications ('muted'
|
|
||||||
statuses).
|
|
||||||
- The user has decided to collapse long statuses and the status is
|
|
||||||
over 400px (without media, or 650px with).
|
|
||||||
- The status is a reply and the user has decided to collapse all
|
|
||||||
replies.
|
|
||||||
- The status contains media and the user has decided to collapse all
|
|
||||||
statuses with media.
|
|
||||||
|
|
||||||
We also start up our intersection observer to monitor our statuses.
|
|
||||||
`componentMounted` lets us know that everything has been set up
|
|
||||||
properly and our intersection observer is good to go.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { node, handleIntersection } = this;
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
settings,
|
|
||||||
collapse,
|
|
||||||
muted,
|
|
||||||
id,
|
|
||||||
intersectionObserverWrapper,
|
|
||||||
prepend,
|
|
||||||
} = this.props;
|
|
||||||
const autoCollapseSettings = settings.getIn(['collapsed', 'auto']);
|
|
||||||
|
|
||||||
if (
|
|
||||||
collapse ||
|
|
||||||
autoCollapseSettings.get('all') || (
|
|
||||||
autoCollapseSettings.get('notifications') && muted
|
|
||||||
) || (
|
|
||||||
autoCollapseSettings.get('lengthy') &&
|
|
||||||
node.clientHeight > (
|
|
||||||
status.get('media_attachments').size && !muted ? 650 : 400
|
|
||||||
)
|
|
||||||
) || (
|
|
||||||
autoCollapseSettings.get('reblogs') &&
|
|
||||||
prepend === 'reblogged_by'
|
|
||||||
) || (
|
|
||||||
autoCollapseSettings.get('replies') &&
|
|
||||||
status.get('in_reply_to_id', null) !== null
|
|
||||||
) || (
|
|
||||||
autoCollapseSettings.get('media') &&
|
|
||||||
!(status.get('spoiler_text').length) &&
|
|
||||||
status.get('media_attachments').size
|
|
||||||
)
|
|
||||||
) this.setExpansion(false);
|
|
||||||
|
|
||||||
if (!intersectionObserverWrapper) return;
|
|
||||||
else intersectionObserverWrapper.observe(
|
|
||||||
id,
|
|
||||||
node,
|
|
||||||
handleIntersection
|
|
||||||
);
|
|
||||||
|
|
||||||
this.componentMounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `shouldComponentUpdate()`.
|
|
||||||
|
|
||||||
If the status is about to be both offscreen (not intersecting) and
|
|
||||||
hidden, then we only need to update it if it's not that way currently.
|
|
||||||
If the status is moving from offscreen to onscreen, then we *have* to
|
|
||||||
re-render, so that we can unhide the element if necessary.
|
|
||||||
|
|
||||||
If neither of these cases are true, we can leave it up to our
|
|
||||||
`updateOnProps` and `updateOnStates` arrays.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
|
||||||
switch (true) {
|
|
||||||
case !nextState.isIntersecting && nextState.isHidden:
|
|
||||||
return this.state.isIntersecting || !this.state.isHidden;
|
|
||||||
case nextState.isIntersecting && !this.state.isIntersecting:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.shouldComponentUpdate(nextProps, nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `componentDidUpdate()`.
|
|
||||||
|
|
||||||
If our component is being rendered for any reason and an update has
|
|
||||||
triggered, this will save its height.
|
|
||||||
|
|
||||||
This is, frankly, a bit overkill, as the only instance when we
|
|
||||||
actually *need* to update the height right now should be when the
|
|
||||||
value of `isExpanded` has changed. But it makes for more readable
|
|
||||||
code and prevents bugs in the future where the height isn't set
|
|
||||||
properly after some change.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
if (
|
|
||||||
this.state.isIntersecting || !this.state.isHidden
|
|
||||||
) this.saveHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `componentWillUnmount()`.
|
|
||||||
|
|
||||||
If our component is about to unmount, then we'd better unset
|
|
||||||
`this.componentMounted`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.componentMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `handleIntersection()`.
|
|
||||||
|
|
||||||
`handleIntersection()` either hides the status (if it is offscreen) or
|
|
||||||
unhides it (if it is onscreen). It's called by
|
|
||||||
`intersectionObserverWrapper.observe()`.
|
|
||||||
|
|
||||||
If our status isn't intersecting, we schedule an idle task (using the
|
|
||||||
aptly-named `scheduleIdleTask()`) to hide the status at the next
|
|
||||||
available opportunity.
|
|
||||||
|
|
||||||
tootsuite/mastodon left us with the following enlightening comment
|
|
||||||
regarding this function:
|
|
||||||
|
|
||||||
> Edge 15 doesn't support isIntersecting, but we can infer it
|
|
||||||
|
|
||||||
It then implements a polyfill (intersectionRect.height > 0) which isn't
|
|
||||||
actually sufficient. The short answer is, this behaviour isn't really
|
|
||||||
supported on Edge but we can get kinda close.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
handleIntersection = (entry) => {
|
|
||||||
const isIntersecting = (
|
|
||||||
typeof entry.isIntersecting === 'boolean' ?
|
|
||||||
entry.isIntersecting :
|
|
||||||
entry.intersectionRect.height > 0
|
|
||||||
);
|
|
||||||
this.setState(
|
|
||||||
(prevState) => {
|
|
||||||
if (prevState.isIntersecting && !isIntersecting) {
|
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isIntersecting : isIntersecting,
|
|
||||||
isHidden : false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `hideIfNotIntersecting()`.
|
|
||||||
|
|
||||||
This function will hide the status if we're still not intersecting.
|
|
||||||
Hiding the status means that it will just render an empty div instead
|
|
||||||
of actual content, which saves RAMS and CPUs or some such.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
hideIfNotIntersecting = () => {
|
|
||||||
if (!this.componentMounted) return;
|
|
||||||
this.setState(
|
|
||||||
(prevState) => ({ isHidden: !prevState.isIntersecting })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `saveHeight()`.
|
|
||||||
|
|
||||||
`saveHeight()` saves the height of our status so that when whe hide it
|
|
||||||
we preserve its dimensions. We only want to store our height, though,
|
|
||||||
if our status has content (otherwise, it would imply that it is
|
|
||||||
already hidden).
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
saveHeight = () => {
|
|
||||||
if (this.node && this.node.children.length) {
|
|
||||||
this.height = this.node.getBoundingClientRect().height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `setExpansion()`.
|
|
||||||
|
|
||||||
`setExpansion()` sets the value of `isExpanded` in our state. It takes
|
|
||||||
one argument, `value`, which gives the desired value for `isExpanded`.
|
|
||||||
The default for this argument is `null`.
|
|
||||||
|
|
||||||
`setExpansion()` automatically checks for us whether toot collapsing
|
|
||||||
is enabled, so we don't have to.
|
|
||||||
|
|
||||||
We use a `switch` statement to simplify our code.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
setExpansion = (value) => {
|
|
||||||
switch (true) {
|
|
||||||
case value === undefined || value === null:
|
|
||||||
this.setState({ isExpanded: null });
|
|
||||||
break;
|
|
||||||
case !value && this.props.settings.getIn(['collapsed', 'enabled']):
|
|
||||||
this.setState({ isExpanded: false });
|
|
||||||
break;
|
|
||||||
case !!value:
|
|
||||||
this.setState({ isExpanded: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `handleRef()`.
|
|
||||||
|
|
||||||
`handleRef()` just saves a reference to our status node to `this.node`.
|
|
||||||
It also saves our height, in case the height of our node has changed.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
handleRef = (node) => {
|
|
||||||
this.node = node;
|
|
||||||
this.saveHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `parseClick()`.
|
|
||||||
|
|
||||||
`parseClick()` takes a click event and responds appropriately.
|
|
||||||
If our status is collapsed, then clicking on it should uncollapse it.
|
|
||||||
If `Shift` is held, then clicking on it should collapse it.
|
|
||||||
Otherwise, we open the url handed to us in `destination`, if
|
|
||||||
applicable.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
parseClick = (e, destination) => {
|
|
||||||
const { router } = this.context;
|
|
||||||
const { status } = this.props;
|
|
||||||
const { isExpanded } = this.state;
|
|
||||||
if (!router) return;
|
|
||||||
if (destination === undefined) {
|
|
||||||
destination = `/statuses/${
|
|
||||||
status.getIn(['reblog', 'id'], status.get('id'))
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
if (e.button === 0) {
|
|
||||||
if (isExpanded === false) this.setExpansion(null);
|
|
||||||
else if (e.shiftKey) {
|
|
||||||
this.setExpansion(false);
|
|
||||||
document.getSelection().removeAllRanges();
|
|
||||||
} else router.history.push(destination);
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
#### `render()`.
|
|
||||||
|
|
||||||
`render()` actually puts our element on the screen. The particulars of
|
|
||||||
this operation are further explained in the code below.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const {
|
|
||||||
parseClick,
|
|
||||||
setExpansion,
|
|
||||||
saveHeight,
|
|
||||||
handleRef,
|
|
||||||
} = this;
|
|
||||||
const { router } = this.context;
|
|
||||||
const {
|
|
||||||
status,
|
|
||||||
account,
|
|
||||||
settings,
|
|
||||||
collapsed,
|
|
||||||
muted,
|
|
||||||
prepend,
|
|
||||||
intersectionObserverWrapper,
|
|
||||||
onOpenVideo,
|
|
||||||
onOpenMedia,
|
|
||||||
autoPlayGif,
|
|
||||||
notification,
|
|
||||||
...other
|
|
||||||
} = this.props;
|
|
||||||
const { isExpanded, isIntersecting, isHidden } = this.state;
|
|
||||||
let background = null;
|
|
||||||
let attachments = null;
|
|
||||||
let media = null;
|
|
||||||
let mediaIcon = null;
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
If we don't have a status, then we don't render anything.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (status === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
If our status is offscreen and hidden, then we render an empty <div> in
|
|
||||||
its place. We fill it with "content" but note that opacity is set to 0.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (!isIntersecting && isHidden) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={this.handleRef}
|
|
||||||
data-id={status.get('id')}
|
|
||||||
style={{
|
|
||||||
height : `${this.height}px`,
|
|
||||||
opacity : 0,
|
|
||||||
overflow : 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
status.getIn(['account', 'display_name']) ||
|
|
||||||
status.getIn(['account', 'username'])
|
|
||||||
}
|
|
||||||
{status.get('content')}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
If user backgrounds for collapsed statuses are enabled, then we
|
|
||||||
initialize our background accordingly. This will only be rendered if
|
|
||||||
the status is collapsed.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (
|
|
||||||
settings.getIn(['collapsed', 'backgrounds', 'user_backgrounds'])
|
|
||||||
) background = status.getIn(['account', 'header']);
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
This handles our media attachments. Note that we don't show media on
|
|
||||||
muted (notification) statuses. If the media type is unknown, then we
|
|
||||||
simply ignore it.
|
|
||||||
|
|
||||||
After we have generated our appropriate media element and stored it in
|
|
||||||
`media`, we snatch the thumbnail to use as our `background` if media
|
|
||||||
backgrounds for collapsed statuses are enabled.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
attachments = status.get('media_attachments');
|
|
||||||
if (attachments.size && !muted) {
|
|
||||||
if (attachments.some((item) => item.get('type') === 'unknown')) {
|
|
||||||
|
|
||||||
} else if (
|
|
||||||
attachments.getIn([0, 'type']) === 'video'
|
|
||||||
) {
|
|
||||||
media = ( // Media type is 'video'
|
|
||||||
<StatusPlayer
|
|
||||||
media={attachments.get(0)}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
|
||||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
|
||||||
height={250}
|
|
||||||
onOpenVideo={onOpenVideo}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
mediaIcon = 'video-camera';
|
|
||||||
} else { // Media type is 'image' or 'gifv'
|
|
||||||
media = (
|
|
||||||
<StatusGallery
|
|
||||||
media={attachments}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
letterbox={settings.getIn(['media', 'letterbox'])}
|
|
||||||
fullwidth={settings.getIn(['media', 'fullwidth'])}
|
|
||||||
height={250}
|
|
||||||
onOpenMedia={onOpenMedia}
|
|
||||||
autoPlayGif={autoPlayGif}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
mediaIcon = 'picture-o';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!status.get('sensitive') &&
|
|
||||||
!(status.get('spoiler_text').length > 0) &&
|
|
||||||
settings.getIn(['collapsed', 'backgrounds', 'preview_images'])
|
|
||||||
) background = attachments.getIn([0, 'preview_url']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Here we prepare extra data-* attributes for CSS selectors.
|
|
||||||
Users can use those for theming, hiding avatars etc via UserStyle
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const selectorAttribs = {
|
|
||||||
'data-status-by': `@${status.getIn(['account', 'acct'])}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (prepend && account) {
|
|
||||||
const notifKind = {
|
|
||||||
favourite: 'favourited',
|
|
||||||
reblog: 'boosted',
|
|
||||||
reblogged_by: 'boosted',
|
|
||||||
}[prepend];
|
|
||||||
|
|
||||||
selectorAttribs[`data-${notifKind}-by`] = `@${account.get('acct')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Finally, we can render our status. We just put the pieces together
|
|
||||||
from above. We only render the action bar if the status isn't
|
|
||||||
collapsed.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className={
|
|
||||||
`status${
|
|
||||||
muted ? ' muted' : ''
|
|
||||||
} status-${status.get('visibility')}${
|
|
||||||
isExpanded === false ? ' collapsed' : ''
|
|
||||||
}${
|
|
||||||
isExpanded === false && background ? ' has-background' : ''
|
|
||||||
}${
|
|
||||||
this.state.markedForDelete ? ' marked-for-delete' : ''
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
backgroundImage: (
|
|
||||||
isExpanded === false && background ?
|
|
||||||
`url(${background})` :
|
|
||||||
'none'
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
ref={handleRef}
|
|
||||||
{...selectorAttribs}
|
|
||||||
>
|
|
||||||
{prepend && account ? (
|
|
||||||
<StatusPrepend
|
|
||||||
type={prepend}
|
|
||||||
account={account}
|
|
||||||
parseClick={parseClick}
|
|
||||||
notificationId={this.props.notificationId}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<StatusHeader
|
|
||||||
status={status}
|
|
||||||
friend={account}
|
|
||||||
mediaIcon={mediaIcon}
|
|
||||||
collapsible={settings.getIn(['collapsed', 'enabled'])}
|
|
||||||
collapsed={isExpanded === false}
|
|
||||||
parseClick={parseClick}
|
|
||||||
setExpansion={setExpansion}
|
|
||||||
/>
|
|
||||||
<StatusContent
|
|
||||||
status={status}
|
|
||||||
media={media}
|
|
||||||
mediaIcon={mediaIcon}
|
|
||||||
expanded={isExpanded}
|
|
||||||
setExpansion={setExpansion}
|
|
||||||
onHeightUpdate={saveHeight}
|
|
||||||
parseClick={parseClick}
|
|
||||||
disabled={!router}
|
|
||||||
/>
|
|
||||||
{isExpanded !== false ? (
|
|
||||||
<StatusActionBar
|
|
||||||
{...other}
|
|
||||||
status={status}
|
|
||||||
account={status.get('account')}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{notification ? (
|
|
||||||
<NotificationOverlayContainer
|
|
||||||
notification={notification}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
// Package imports //
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import IconButton from '../../../mastodon/components/icon_button';
|
|
||||||
import { isIOS } from '../../../mastodon/is_mobile';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
toggle_sound: { id: 'video_player.toggle_sound', defaultMessage: 'Toggle sound' },
|
|
||||||
toggle_visible: { id: 'video_player.toggle_visible', defaultMessage: 'Toggle visibility' },
|
|
||||||
expand_video: { id: 'video_player.expand', defaultMessage: 'Expand video' },
|
|
||||||
});
|
|
||||||
|
|
||||||
@injectIntl
|
|
||||||
export default class StatusPlayer extends React.PureComponent {
|
|
||||||
|
|
||||||
static contextTypes = {
|
|
||||||
router: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
media: ImmutablePropTypes.map.isRequired,
|
|
||||||
letterbox: PropTypes.bool,
|
|
||||||
fullwidth: PropTypes.bool,
|
|
||||||
height: PropTypes.number,
|
|
||||||
sensitive: PropTypes.bool,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
autoplay: PropTypes.bool,
|
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
height: 110,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
visible: !this.props.sensitive,
|
|
||||||
preview: true,
|
|
||||||
muted: true,
|
|
||||||
hasAudio: true,
|
|
||||||
videoError: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleClick = () => {
|
|
||||||
this.setState({ muted: !this.state.muted });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVideoClick = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const node = this.video;
|
|
||||||
|
|
||||||
if (node.paused) {
|
|
||||||
node.play();
|
|
||||||
} else {
|
|
||||||
node.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOpen = () => {
|
|
||||||
this.setState({ preview: !this.state.preview });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVisibility = () => {
|
|
||||||
this.setState({
|
|
||||||
visible: !this.state.visible,
|
|
||||||
preview: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleExpand = () => {
|
|
||||||
this.video.pause();
|
|
||||||
this.props.onOpenVideo(this.props.media, this.video.currentTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.video = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadedData = () => {
|
|
||||||
if (('WebkitAppearance' in document.documentElement.style && this.video.audioTracks.length === 0) || this.video.mozHasAudio === false) {
|
|
||||||
this.setState({ hasAudio: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleVideoError = () => {
|
|
||||||
this.setState({ videoError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
if (!this.video) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
|
||||||
this.video.addEventListener('error', this.handleVideoError);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
if (!this.video) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video.addEventListener('loadeddata', this.handleLoadedData);
|
|
||||||
this.video.addEventListener('error', this.handleVideoError);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
if (!this.video) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.video.removeEventListener('loadeddata', this.handleLoadedData);
|
|
||||||
this.video.removeEventListener('error', this.handleVideoError);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { media, intl, letterbox, fullwidth, height, sensitive, autoplay } = this.props;
|
|
||||||
|
|
||||||
let spoilerButton = (
|
|
||||||
<div className={`status__video-player-spoiler ${this.state.visible ? 'status__video-player-spoiler--visible' : ''}`}>
|
|
||||||
<IconButton overlay title={intl.formatMessage(messages.toggle_visible)} icon={this.state.visible ? 'eye' : 'eye-slash'} onClick={this.handleVisibility} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let expandButton = !this.context.router ? '' : (
|
|
||||||
<div className='status__video-player-expand'>
|
|
||||||
<IconButton overlay title={intl.formatMessage(messages.expand_video)} icon='expand' onClick={this.handleExpand} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let muteButton = '';
|
|
||||||
|
|
||||||
if (this.state.hasAudio) {
|
|
||||||
muteButton = (
|
|
||||||
<div className='status__video-player-mute'>
|
|
||||||
<IconButton overlay title={intl.formatMessage(messages.toggle_sound)} icon={this.state.muted ? 'volume-off' : 'volume-up'} onClick={this.handleClick} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.visible) {
|
|
||||||
if (sensitive) {
|
|
||||||
return (
|
|
||||||
<div role='button' tabIndex='0' style={{ height: `${height}px` }} className={`media-spoiler ${fullwidth ? 'full-width' : ''}`} onClick={this.handleVisibility}>
|
|
||||||
{spoilerButton}
|
|
||||||
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
|
|
||||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div role='button' tabIndex='0' style={{ height: `${height}px` }} className={`media-spoiler ${fullwidth ? 'full-width' : ''}`} onClick={this.handleVisibility}>
|
|
||||||
{spoilerButton}
|
|
||||||
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
|
|
||||||
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.preview && !autoplay) {
|
|
||||||
return (
|
|
||||||
<div role='button' tabIndex='0' className={`media-spoiler-video ${fullwidth ? 'full-width' : ''}`} style={{ height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
|
|
||||||
{spoilerButton}
|
|
||||||
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.state.videoError) {
|
|
||||||
return (
|
|
||||||
<div style={{ height: `${height}px` }} className='video-error-cover' >
|
|
||||||
<span className='media-spoiler__warning'><FormattedMessage id='video_player.video_error' defaultMessage='Video could not be played' /></span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`status__video-player ${fullwidth ? 'full-width' : ''}`} style={{ height: `${height}px` }}>
|
|
||||||
{spoilerButton}
|
|
||||||
{muteButton}
|
|
||||||
{expandButton}
|
|
||||||
|
|
||||||
<video
|
|
||||||
className={`status__video-player-video${letterbox ? ' letterbox' : ''}`}
|
|
||||||
role='button'
|
|
||||||
tabIndex='0'
|
|
||||||
ref={this.setRef}
|
|
||||||
src={media.get('url')}
|
|
||||||
autoPlay={!isIOS()}
|
|
||||||
loop
|
|
||||||
muted={this.state.muted}
|
|
||||||
onClick={this.handleVideoClick}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
44
app/javascript/glitch/locales/pl.json
Normal file
44
app/javascript/glitch/locales/pl.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"getting_started.open_source_notice": "Glitchsoc jest wolnym i otwartoźródłowym forkiem oprogramowania {Mastodon}. Możesz współtworzyć projekt lub zgłaszać błędy na GitHubie pod adresem {github}.",
|
||||||
|
"layout.auto": "Automatyczny",
|
||||||
|
"layout.current_is": "Twój obecny układ to:",
|
||||||
|
"layout.desktop": "Desktopowy",
|
||||||
|
"layout.mobile": "Mobilny",
|
||||||
|
"navigation_bar.app_settings": "Ustawienia aplikacji",
|
||||||
|
"getting_started.onboarding": "Rozejrzyj się",
|
||||||
|
"onboarding.page_one.federation": "{domain} jest 'instancją' Mastodona. Mastodon to sieć działających niezależnie serwerów tworzących jedną sieć społecznościową. Te serwery nazywane są instancjami.",
|
||||||
|
"onboarding.page_one.welcome": "Witamy na {domain}!",
|
||||||
|
"onboarding.page_six.github": "{domain} jest oparty na Glitchsoc. Glitchsoc jest {forkiem} {Mastodon}a kompatybilnym z każdym klientem i aplikacją Mastodona. Glitchsoc jest całkowicie wolnym i otwartoźródłowym oprogramowaniem. Możesz zgłaszać błędy i sugestie funkcji oraz współtworzyć projekt na {github}.",
|
||||||
|
"settings.auto_collapse": "Automatyczne zwijanie",
|
||||||
|
"settings.auto_collapse_all": "Wszystko",
|
||||||
|
"settings.auto_collapse_lengthy": "Długie wpisy",
|
||||||
|
"settings.auto_collapse_media": "Wpisy z zawartością multimedialną",
|
||||||
|
"settings.auto_collapse_notifications": "Powiadomienia",
|
||||||
|
"settings.auto_collapse_reblogs": "Podbicia",
|
||||||
|
"settings.auto_collapse_replies": "Odpowiedzi",
|
||||||
|
"settings.close": "Zamknij",
|
||||||
|
"settings.collapsed_statuses": "Zwijanie wpisów",
|
||||||
|
"settings.enable_collapsed": "Włącz zwijanie wpisów",
|
||||||
|
"settings.general": "Ogólne",
|
||||||
|
"settings.image_backgrounds": "Obrazy w tle",
|
||||||
|
"settings.image_backgrounds_media": "Wyświetlaj zawartość multimedialną zwiniętych wpisów",
|
||||||
|
"settings.image_backgrounds_users": "Nadaj tło zwiniętym wpisom",
|
||||||
|
"settings.media": "Zawartość multimedialna",
|
||||||
|
"settings.media_letterbox": "Letterbox media",
|
||||||
|
"settings.media_fullwidth": "Podgląd zawartości multimedialnej o pełnej szerokości",
|
||||||
|
"settings.preferences": "Preferencje użyytkownika",
|
||||||
|
"settings.wide_view": "Szeroki widok (tylko w trybie desktopowym)",
|
||||||
|
"settings.navbar_under": "Pasek nawigacji na dole (tylko w trybie mobilnym)",
|
||||||
|
"status.collapse": "Zwiń",
|
||||||
|
"status.uncollapse": "Rozwiń",
|
||||||
|
|
||||||
|
"notification.markForDeletion": "Oznacz do usunięcia",
|
||||||
|
"notifications.clear": "Wyczyść wszystkie powiadomienia",
|
||||||
|
"notifications.marked_clear_confirmation": "Czy na pewno chcesz bezpowrtonie usunąć wszystkie powiadomienia?",
|
||||||
|
"notifications.marked_clear": "Usuń zaznaczone powiadomienia",
|
||||||
|
|
||||||
|
"notification_purge.btn_all": "Zaznacz\nwszystkie",
|
||||||
|
"notification_purge.btn_none": "Odznacz\nwszystkie",
|
||||||
|
"notification_purge.btn_invert": "Odwróć\nzaznaczenie",
|
||||||
|
"notification_purge.btn_apply": "Usuń\nzaznaczone"
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
/*
|
|
||||||
|
|
||||||
`reducers/local_settings`
|
|
||||||
========================
|
|
||||||
|
|
||||||
> For more information on the contents of this file, please contact:
|
|
||||||
>
|
|
||||||
> - kibigo! [@kibi@glitch.social]
|
|
||||||
|
|
||||||
This file provides our Redux reducers related to local settings. The
|
|
||||||
associated actions are:
|
|
||||||
|
|
||||||
- __`STORE_HYDRATE` :__
|
|
||||||
Used to hydrate the store with its initial values.
|
|
||||||
|
|
||||||
- __`LOCAL_SETTING_CHANGE` :__
|
|
||||||
Used to change the value of a local setting in the store.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Imports:
|
|
||||||
--------
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Package imports //
|
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
|
|
||||||
// Mastodon imports //
|
|
||||||
import { STORE_HYDRATE } from '../../mastodon/actions/store';
|
|
||||||
|
|
||||||
// Our imports //
|
|
||||||
import { LOCAL_SETTING_CHANGE } from '../actions/local_settings';
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
initialState:
|
|
||||||
-------------
|
|
||||||
|
|
||||||
You can see the default values for all of our local settings here.
|
|
||||||
These are only used if no previously-saved values exist.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
|
||||||
layout : 'auto',
|
|
||||||
stretch : true,
|
|
||||||
navbar_under : false,
|
|
||||||
side_arm : 'none',
|
|
||||||
collapsed : ImmutableMap({
|
|
||||||
enabled : true,
|
|
||||||
auto : ImmutableMap({
|
|
||||||
all : false,
|
|
||||||
notifications : true,
|
|
||||||
lengthy : true,
|
|
||||||
reblogs : false,
|
|
||||||
replies : false,
|
|
||||||
media : false,
|
|
||||||
}),
|
|
||||||
backgrounds : ImmutableMap({
|
|
||||||
user_backgrounds : false,
|
|
||||||
preview_images : false,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
media : ImmutableMap({
|
|
||||||
letterbox : true,
|
|
||||||
fullwidth : true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Helper functions:
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
### `hydrate(state, localSettings)`
|
|
||||||
|
|
||||||
`hydrate()` is used to hydrate the `local_settings` part of our store
|
|
||||||
with its initial values. The `state` will probably just be the
|
|
||||||
`initialState`, and the `localSettings` should be whatever we pulled
|
|
||||||
from `localStorage`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const hydrate = (state, localSettings) => state.mergeDeep(localSettings);
|
|
||||||
|
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
`localSettings(state = initialState, action)`:
|
|
||||||
----------------------------------------------
|
|
||||||
|
|
||||||
This function holds our actual reducer.
|
|
||||||
|
|
||||||
If our action is `STORE_HYDRATE`, then we call `hydrate()` with the
|
|
||||||
`local_settings` property of the provided `action.state`.
|
|
||||||
|
|
||||||
If our action is `LOCAL_SETTING_CHANGE`, then we set `action.key` in
|
|
||||||
our state to the provided `action.value`. Note that `action.key` MUST
|
|
||||||
be an array, since we use `setIn()`.
|
|
||||||
|
|
||||||
> __Note :__
|
|
||||||
> We call this function `localSettings`, but its associated object
|
|
||||||
> in the store is `local_settings`.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default function localSettings(state = initialState, action) {
|
|
||||||
switch(action.type) {
|
|
||||||
case STORE_HYDRATE:
|
|
||||||
return hydrate(state, action.state.get('local_settings'));
|
|
||||||
case LOCAL_SETTING_CHANGE:
|
|
||||||
return state.setIn(action.key, action.value);
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
refreshHomeTimeline,
|
refreshHomeTimeline,
|
||||||
refreshCommunityTimeline,
|
refreshCommunityTimeline,
|
||||||
refreshPublicTimeline,
|
refreshPublicTimeline,
|
||||||
refreshDirectTimeline,
|
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
|
|
||||||
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
|
||||||
@@ -32,7 +31,6 @@ export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT';
|
|||||||
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
|
export const COMPOSE_MOUNT = 'COMPOSE_MOUNT';
|
||||||
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
||||||
|
|
||||||
export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE';
|
|
||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
@@ -46,8 +44,6 @@ export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'
|
|||||||
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS';
|
||||||
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL';
|
||||||
|
|
||||||
export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET';
|
|
||||||
|
|
||||||
export function changeCompose(text) {
|
export function changeCompose(text) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_CHANGE,
|
type: COMPOSE_CHANGE,
|
||||||
@@ -95,16 +91,14 @@ export function mentionCompose(account, router) {
|
|||||||
|
|
||||||
export function submitCompose() {
|
export function submitCompose() {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
let status = getState().getIn(['compose', 'text'], '');
|
const status = getState().getIn(['compose', 'text'], '');
|
||||||
|
|
||||||
if (!status || !status.length) {
|
if (!status || !status.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(submitComposeRequest());
|
dispatch(submitComposeRequest());
|
||||||
if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) {
|
|
||||||
status = status + ' 👁️';
|
|
||||||
}
|
|
||||||
api(getState).post('/api/v1/statuses', {
|
api(getState).post('/api/v1/statuses', {
|
||||||
status,
|
status,
|
||||||
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null),
|
||||||
@@ -134,8 +128,6 @@ export function submitCompose() {
|
|||||||
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
|
||||||
insertOrRefresh('community', refreshCommunityTimeline);
|
insertOrRefresh('community', refreshCommunityTimeline);
|
||||||
insertOrRefresh('public', refreshPublicTimeline);
|
insertOrRefresh('public', refreshPublicTimeline);
|
||||||
} else if (response.data.visibility === 'direct') {
|
|
||||||
insertOrRefresh('direct', refreshDirectTimeline);
|
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
dispatch(submitComposeFail(error));
|
dispatch(submitComposeFail(error));
|
||||||
@@ -163,13 +155,6 @@ export function submitComposeFail(error) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function doodleSet(options) {
|
|
||||||
return {
|
|
||||||
type: COMPOSE_DOODLE_SET,
|
|
||||||
options: options,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function uploadCompose(files) {
|
export function uploadCompose(files) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
|
if (getState().getIn(['compose', 'media_attachments']).size > 3) {
|
||||||
@@ -349,13 +334,6 @@ export function unmountCompose() {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toggleComposeAdvancedOption(option) {
|
|
||||||
return {
|
|
||||||
type: COMPOSE_ADVANCED_OPTIONS_CHANGE,
|
|
||||||
option: option,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function changeComposeSensitivity() {
|
export function changeComposeSensitivity() {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_SENSITIVITY_CHANGE,
|
type: COMPOSE_SENSITIVITY_CHANGE,
|
||||||
|
|||||||
28
app/javascript/mastodon/actions/lists.js
Normal file
28
app/javascript/mastodon/actions/lists.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
|
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const fetchList = id => (dispatch, getState) => {
|
||||||
|
dispatch(fetchListRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/lists/${id}`)
|
||||||
|
.then(({ data }) => dispatch(fetchListSuccess(data)))
|
||||||
|
.catch(err => dispatch(fetchListFail(err)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchListRequest = id => ({
|
||||||
|
type: LIST_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchListSuccess = list => ({
|
||||||
|
type: LIST_FETCH_SUCCESS,
|
||||||
|
list,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchListFail = error => ({
|
||||||
|
type: LIST_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
@@ -6,17 +6,6 @@ import { defineMessages } from 'react-intl';
|
|||||||
|
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
|
|
||||||
// tracking the notif cleaning request
|
|
||||||
export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST';
|
|
||||||
export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS';
|
|
||||||
export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL';
|
|
||||||
export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE';
|
|
||||||
export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes
|
|
||||||
// Unmark notifications (when the cleaning mode is left)
|
|
||||||
export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE';
|
|
||||||
// Mark one for delete
|
|
||||||
export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE';
|
|
||||||
|
|
||||||
export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST';
|
export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST';
|
||||||
export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS';
|
export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS';
|
||||||
export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL';
|
export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL';
|
||||||
@@ -199,67 +188,3 @@ export function scrollTopNotifications(top) {
|
|||||||
top,
|
top,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export function deleteMarkedNotifications() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch(deleteMarkedNotificationsRequest());
|
|
||||||
|
|
||||||
let ids = [];
|
|
||||||
getState().getIn(['notifications', 'items']).forEach((n) => {
|
|
||||||
if (n.get('markedForDelete')) {
|
|
||||||
ids.push(n.get('id'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ids.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => {
|
|
||||||
dispatch(deleteMarkedNotificationsSuccess());
|
|
||||||
}).catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
dispatch(deleteMarkedNotificationsFail(error));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function enterNotificationClearingMode(yes) {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATIONS_ENTER_CLEARING_MODE,
|
|
||||||
yes: yes,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function markAllNotifications(yes) {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATIONS_MARK_ALL_FOR_DELETE,
|
|
||||||
yes: yes, // true, false or null. null = invert
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function deleteMarkedNotificationsRequest() {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATIONS_DELETE_MARKED_REQUEST,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function deleteMarkedNotificationsFail() {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATIONS_DELETE_MARKED_FAIL,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function markNotificationForDelete(id, yes) {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATION_MARK_FOR_DELETE,
|
|
||||||
id: id,
|
|
||||||
yes: yes,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function deleteMarkedNotificationsSuccess() {
|
|
||||||
return {
|
|
||||||
type: NOTIFICATIONS_DELETE_MARKED_SUCCESS,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -92,4 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
|
|||||||
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
|
export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
|
||||||
export const connectPublicStream = () => connectTimelineStream('public', 'public');
|
export const connectPublicStream = () => connectTimelineStream('public', 'public');
|
||||||
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
|
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
|
||||||
export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
|
export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||||
|
|||||||
@@ -115,10 +115,10 @@ export function refreshTimeline(timelineId, path, params = {}) {
|
|||||||
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
|
export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
|
||||||
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
|
||||||
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
|
export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||||
export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct');
|
|
||||||
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
||||||
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
||||||
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
||||||
|
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
|
||||||
|
|
||||||
export function refreshTimelineFail(timeline, error, skipLoading) {
|
export function refreshTimelineFail(timeline, error, skipLoading) {
|
||||||
return {
|
return {
|
||||||
@@ -156,10 +156,10 @@ export function expandTimeline(timelineId, path, params = {}) {
|
|||||||
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
|
export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
|
||||||
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
|
||||||
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
|
||||||
export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct');
|
|
||||||
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
|
||||||
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
|
||||||
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
|
||||||
|
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
|
||||||
|
|
||||||
export function expandTimelineRequest(timeline) {
|
export function expandTimelineRequest(timeline) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar"
|
className="account__avatar"
|
||||||
data-avatar-of="@alice"
|
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
style={
|
style={
|
||||||
@@ -20,7 +19,6 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
|||||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar"
|
className="account__avatar"
|
||||||
data-avatar-of="@alice"
|
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="account__avatar-overlay-base"
|
className="account__avatar-overlay-base"
|
||||||
data-avatar-of="@alice"
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"backgroundImage": "url(/static/alice.jpg)",
|
"backgroundImage": "url(/static/alice.jpg)",
|
||||||
@@ -15,7 +14,6 @@ exports[`<AvatarOverlay renders a overlay avatar 1`] = `
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="account__avatar-overlay-overlay"
|
className="account__avatar-overlay-overlay"
|
||||||
data-avatar-of="@eve@blackhat.lair"
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"backgroundImage": "url(/static/eve.jpg)",
|
"backgroundImage": "url(/static/eve.jpg)",
|
||||||
|
|||||||
@@ -112,19 +112,3 @@ exports[`<Button /> renders the props.text instead of children 1`] = `
|
|||||||
foo
|
foo
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<Button /> renders title if props.title is given 1`] = `
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
disabled={undefined}
|
|
||||||
onClick={[Function]}
|
|
||||||
style={
|
|
||||||
Object {
|
|
||||||
"height": "36px",
|
|
||||||
"lineHeight": "36px",
|
|
||||||
"padding": "0 16px",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
title="foo"
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -72,11 +72,4 @@ describe('<Button />', () => {
|
|||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders title if props.title is given', () => {
|
|
||||||
const component = renderer.create(<Button title='foo' />);
|
|
||||||
const tree = component.toJSON();
|
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Permalink from './permalink';
|
|||||||
import IconButton from './icon_button';
|
import IconButton from './icon_button';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||||
@@ -14,8 +15,8 @@ const messages = defineMessages({
|
|||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' },
|
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
||||||
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' },
|
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import classNames from 'classnames';
|
|||||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||||
let word;
|
let word;
|
||||||
|
|
||||||
let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/);
|
let left = str.slice(0, caretPosition).search(/\S+$/);
|
||||||
let right = str.slice(caretPosition).search(/[\s\u200B]/);
|
let right = str.slice(caretPosition).search(/\s/);
|
||||||
|
|
||||||
if (right < 0) {
|
if (right < 0) {
|
||||||
word = str.slice(left);
|
word = str.slice(left);
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ export default class Avatar extends React.PureComponent {
|
|||||||
onMouseEnter={this.handleMouseEnter}
|
onMouseEnter={this.handleMouseEnter}
|
||||||
onMouseLeave={this.handleMouseLeave}
|
onMouseLeave={this.handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
data-avatar-of={`@${account.get('acct')}`}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export default class AvatarOverlay extends React.PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__avatar-overlay'>
|
<div className='account__avatar-overlay'>
|
||||||
<div className='account__avatar-overlay-base' style={baseStyle} data-avatar-of={`@${account.get('acct')}`} />
|
<div className='account__avatar-overlay-base' style={baseStyle} />
|
||||||
<div className='account__avatar-overlay-overlay' style={overlayStyle} data-avatar-of={`@${friend.get('acct')}`} />
|
<div className='account__avatar-overlay-overlay' style={overlayStyle} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ export default class Button extends React.PureComponent {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
style: PropTypes.object,
|
style: PropTypes.object,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
title: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -36,26 +35,26 @@ export default class Button extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let attrs = {
|
const style = {
|
||||||
className: classNames('button', this.props.className, {
|
|
||||||
'button-secondary': this.props.secondary,
|
|
||||||
'button--block': this.props.block,
|
|
||||||
}),
|
|
||||||
disabled: this.props.disabled,
|
|
||||||
onClick: this.handleClick,
|
|
||||||
ref: this.setRef,
|
|
||||||
style: {
|
|
||||||
padding: `0 ${this.props.size / 2.25}px`,
|
padding: `0 ${this.props.size / 2.25}px`,
|
||||||
height: `${this.props.size}px`,
|
height: `${this.props.size}px`,
|
||||||
lineHeight: `${this.props.size}px`,
|
lineHeight: `${this.props.size}px`,
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.props.title) attrs.title = this.props.title;
|
const className = classNames('button', this.props.className, {
|
||||||
|
'button-secondary': this.props.secondary,
|
||||||
|
'button--block': this.props.block,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button {...attrs}>
|
<button
|
||||||
|
className={className}
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
ref={this.setRef}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
{this.props.text || this.props.children}
|
{this.props.text || this.props.children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ export default class Column extends React.PureComponent {
|
|||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
extraClasses: PropTypes.string,
|
|
||||||
name: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scrollTop () {
|
scrollTop () {
|
||||||
@@ -42,10 +40,10 @@ export default class Column extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, extraClasses, name } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div role='region' data-column={name} className={`column ${extraClasses || ''}`} ref={this.setRef}>
|
<div role='region' className='column' ref={this.setRef}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ export default class ColumnBackButton extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
// if history is exhausted, or we would leave mastodon, just go to root.
|
if (window.history && window.history.length === 1) {
|
||||||
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
|
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
} else {
|
} else {
|
||||||
this.context.router.history.goBack();
|
this.context.router.history.goBack();
|
||||||
|
|||||||
@@ -9,12 +9,8 @@ export default class ColumnBackButtonSlim extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
// if history is exhausted, or we would leave mastodon, just go to root.
|
if (window.history && window.history.length === 1) this.context.router.history.push('/');
|
||||||
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
|
else this.context.router.history.goBack();
|
||||||
this.context.router.history.push('/');
|
|
||||||
} else {
|
|
||||||
this.context.router.history.goBack();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
// Glitch imports
|
|
||||||
import NotificationPurgeButtonsContainer from '../../glitch/components/column/notif_cleaning_widget/container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||||
enterNotifCleaning : { id: 'notification_purge.start', defaultMessage: 'Enter notification cleaning mode' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
@@ -27,19 +22,14 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
title: PropTypes.node.isRequired,
|
title: PropTypes.node.isRequired,
|
||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
active: PropTypes.bool,
|
active: PropTypes.bool,
|
||||||
localSettings : ImmutablePropTypes.map,
|
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
focusable: PropTypes.bool,
|
focusable: PropTypes.bool,
|
||||||
showBackButton: PropTypes.bool,
|
showBackButton: PropTypes.bool,
|
||||||
notifCleaning: PropTypes.bool, // true only for the notification column
|
|
||||||
notifCleaningActive: PropTypes.bool,
|
|
||||||
onEnterCleaningMode: PropTypes.func,
|
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
pinned: PropTypes.bool,
|
pinned: PropTypes.bool,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
onMove: PropTypes.func,
|
onMove: PropTypes.func,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -49,7 +39,6 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
state = {
|
state = {
|
||||||
collapsed: true,
|
collapsed: true,
|
||||||
animating: false,
|
animating: false,
|
||||||
animatingNCD: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggleClick = (e) => {
|
handleToggleClick = (e) => {
|
||||||
@@ -70,32 +59,17 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleBackClick = () => {
|
handleBackClick = () => {
|
||||||
// if history is exhausted, or we would leave mastodon, just go to root.
|
if (window.history && window.history.length === 1) this.context.router.history.push('/');
|
||||||
if (window.history && (window.history.length === 1 || window.history.length === window._mastoInitialHistoryLen)) {
|
else this.context.router.history.goBack();
|
||||||
this.context.router.history.push('/');
|
|
||||||
} else {
|
|
||||||
this.context.router.history.goBack();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTransitionEnd = () => {
|
handleTransitionEnd = () => {
|
||||||
this.setState({ animating: false });
|
this.setState({ animating: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTransitionEndNCD = () => {
|
|
||||||
this.setState({ animatingNCD: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnterCleaningMode = () => {
|
|
||||||
this.setState({ animatingNCD: true });
|
|
||||||
this.props.onEnterCleaningMode(!this.props.notifCleaningActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage }, notifCleaning, notifCleaningActive } = this.props;
|
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
|
||||||
const { collapsed, animating, animatingNCD } = this.state;
|
const { collapsed, animating } = this.state;
|
||||||
|
|
||||||
let title = this.props.title;
|
|
||||||
|
|
||||||
const wrapperClassName = classNames('column-header__wrapper', {
|
const wrapperClassName = classNames('column-header__wrapper', {
|
||||||
'active': active,
|
'active': active,
|
||||||
@@ -114,20 +88,8 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
'active': !collapsed,
|
'active': !collapsed,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notifCleaningButtonClassName = classNames('column-header__button', {
|
|
||||||
'active': notifCleaningActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
const notifCleaningDrawerClassName = classNames('ncd column-header__collapsible', {
|
|
||||||
'collapsed': !notifCleaningActive,
|
|
||||||
'animating': animatingNCD,
|
|
||||||
});
|
|
||||||
|
|
||||||
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
let extraContent, pinButton, moveButtons, backButton, collapseButton;
|
||||||
|
|
||||||
//*glitch
|
|
||||||
const msgEnterNotifCleaning = intl.formatMessage(messages.enterNotifCleaning);
|
|
||||||
|
|
||||||
if (children) {
|
if (children) {
|
||||||
extraContent = (
|
extraContent = (
|
||||||
<div key='extra-content' className='column-header__collapsible__extra'>
|
<div key='extra-content' className='column-header__collapsible__extra'>
|
||||||
@@ -178,30 +140,13 @@ export default class ColumnHeader extends React.PureComponent {
|
|||||||
<span className='column-header__title'>
|
<span className='column-header__title'>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{backButton}
|
{backButton}
|
||||||
{ notifCleaning ? (
|
|
||||||
<button
|
|
||||||
aria-label={msgEnterNotifCleaning}
|
|
||||||
title={msgEnterNotifCleaning}
|
|
||||||
onClick={this.onEnterCleaningMode}
|
|
||||||
className={notifCleaningButtonClassName}
|
|
||||||
>
|
|
||||||
<i className='fa fa-eraser' />
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
{collapseButton}
|
{collapseButton}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{ notifCleaning ? (
|
|
||||||
<div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
|
|
||||||
<div className='column-header__collapsible-inner nopad-drawer'>
|
|
||||||
{(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
|
<div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
|
||||||
<div className='column-header__collapsible-inner'>
|
<div className='column-header__collapsible-inner'>
|
||||||
{(!collapsed || animating) && collapsedContent}
|
{(!collapsed || animating) && collapsedContent}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
icon: PropTypes.string.isRequired,
|
icon: PropTypes.string.isRequired,
|
||||||
items: PropTypes.array.isRequired,
|
items: PropTypes.array.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
ariaLabel: PropTypes.string,
|
title: PropTypes.string,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
isUserTouching: PropTypes.func,
|
isUserTouching: PropTypes.func,
|
||||||
@@ -120,7 +120,7 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
ariaLabel: 'Menu',
|
title: 'Menu',
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -186,14 +186,14 @@ export default class Dropdown extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { icon, items, size, ariaLabel, disabled } = this.props;
|
const { icon, items, size, title, disabled } = this.props;
|
||||||
const { expanded } = this.state;
|
const { expanded } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onKeyDown={this.handleKeyDown}>
|
<div onKeyDown={this.handleKeyDown}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={ariaLabel}
|
title={title}
|
||||||
active={expanded}
|
active={expanded}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size={size}
|
size={size}
|
||||||
|
|||||||
@@ -20,10 +20,8 @@ export default class IconButton extends React.PureComponent {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
inverted: PropTypes.bool,
|
inverted: PropTypes.bool,
|
||||||
animate: PropTypes.bool,
|
animate: PropTypes.bool,
|
||||||
flip: PropTypes.bool,
|
|
||||||
overlay: PropTypes.bool,
|
overlay: PropTypes.bool,
|
||||||
tabIndex: PropTypes.string,
|
tabIndex: PropTypes.string,
|
||||||
label: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
@@ -44,18 +42,14 @@ export default class IconButton extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let style = {
|
const style = {
|
||||||
fontSize: `${this.props.size}px`,
|
fontSize: `${this.props.size}px`,
|
||||||
|
width: `${this.props.size * 1.28571429}px`,
|
||||||
height: `${this.props.size * 1.28571429}px`,
|
height: `${this.props.size * 1.28571429}px`,
|
||||||
lineHeight: `${this.props.size}px`,
|
lineHeight: `${this.props.size}px`,
|
||||||
...this.props.style,
|
...this.props.style,
|
||||||
...(this.props.active ? this.props.activeStyle : {}),
|
...(this.props.active ? this.props.activeStyle : {}),
|
||||||
};
|
};
|
||||||
if (!this.props.label) {
|
|
||||||
style.width = `${this.props.size * 1.28571429}px`;
|
|
||||||
} else {
|
|
||||||
style.textAlign = 'left';
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
active,
|
active,
|
||||||
@@ -78,23 +72,27 @@ export default class IconButton extends React.PureComponent {
|
|||||||
overlayed: overlay,
|
overlayed: overlay,
|
||||||
});
|
});
|
||||||
|
|
||||||
const flipDeg = this.props.flip ? -180 : -360;
|
if (!animate) {
|
||||||
const rotateDeg = this.props.active ? flipDeg : 0;
|
// Perf optimization: avoid unnecessary <Motion> components unless
|
||||||
|
// we actually need to animate.
|
||||||
const motionDefaultStyle = {
|
return (
|
||||||
rotate: rotateDeg,
|
<button
|
||||||
};
|
aria-label={title}
|
||||||
|
aria-pressed={pressed}
|
||||||
const springOpts = {
|
aria-expanded={expanded}
|
||||||
stiffness: this.props.flip ? 60 : 120,
|
title={title}
|
||||||
damping: 7,
|
className={classes}
|
||||||
};
|
onClick={this.handleClick}
|
||||||
const motionStyle = {
|
style={style}
|
||||||
rotate: animate ? spring(rotateDeg, springOpts) : 0,
|
tabIndex={tabIndex}
|
||||||
};
|
>
|
||||||
|
<i className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Motion defaultStyle={motionDefaultStyle} style={motionStyle}>
|
<Motion defaultStyle={{ rotate: active ? -360 : 0 }} style={{ rotate: animate ? spring(active ? -360 : 0, { stiffness: 120, damping: 7 }) : 0 }}>
|
||||||
{({ rotate }) =>
|
{({ rotate }) =>
|
||||||
<button
|
<button
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
@@ -107,7 +105,6 @@ export default class IconButton extends React.PureComponent {
|
|||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
<i style={{ transform: `rotate(${rotate}deg)` }} className={`fa fa-fw fa-${icon}`} aria-hidden='true' />
|
||||||
{this.props.label}
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</Motion>
|
</Motion>
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/status/gallery
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -9,6 +6,7 @@ import IconButton from './icon_button';
|
|||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { isIOS } from '../is_mobile';
|
import { isIOS } from '../is_mobile';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { autoPlayGif } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
|
||||||
@@ -26,11 +24,9 @@ class Item extends React.PureComponent {
|
|||||||
index: PropTypes.number.isRequired,
|
index: PropTypes.number.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
autoPlayGif: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoPlayGif: false,
|
|
||||||
standalone: false,
|
standalone: false,
|
||||||
index: 0,
|
index: 0,
|
||||||
size: 1,
|
size: 1,
|
||||||
@@ -50,7 +46,7 @@ class Item extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hoverToPlay () {
|
hoverToPlay () {
|
||||||
const { attachment, autoPlayGif } = this.props;
|
const { attachment } = this.props;
|
||||||
return !autoPlayGif && attachment.get('type') === 'gifv';
|
return !autoPlayGif && attachment.get('type') === 'gifv';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +138,7 @@ class Item extends React.PureComponent {
|
|||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (attachment.get('type') === 'gifv') {
|
} else if (attachment.get('type') === 'gifv') {
|
||||||
const autoPlay = !isIOS() && this.props.autoPlayGif;
|
const autoPlay = !isIOS() && autoPlayGif;
|
||||||
|
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
|
||||||
@@ -184,11 +180,9 @@ export default class MediaGallery extends React.PureComponent {
|
|||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
autoPlayGif: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
autoPlayGif: false,
|
|
||||||
standalone: false,
|
standalone: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,9 +258,9 @@ export default class MediaGallery extends React.PureComponent {
|
|||||||
const size = media.take(4).size;
|
const size = media.take(4).size;
|
||||||
|
|
||||||
if (this.isStandaloneEligible()) {
|
if (this.isStandaloneEligible()) {
|
||||||
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} autoPlayGif={this.props.autoPlayGif} />;
|
children = <Item standalone onClick={this.handleClick} attachment={media.get(0)} />;
|
||||||
} else {
|
} else {
|
||||||
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} autoPlayGif={this.props.autoPlayGif} index={i} size={size} />);
|
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} onClick={this.handleClick} attachment={attachment} index={i} size={size} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/status
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -39,9 +36,6 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
onBlock: PropTypes.func,
|
onBlock: PropTypes.func,
|
||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
onHeightChange: PropTypes.func,
|
onHeightChange: PropTypes.func,
|
||||||
me: PropTypes.string,
|
|
||||||
boostModal: PropTypes.bool,
|
|
||||||
autoPlayGif: PropTypes.bool,
|
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
onMoveUp: PropTypes.func,
|
onMoveUp: PropTypes.func,
|
||||||
@@ -57,9 +51,6 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
'status',
|
'status',
|
||||||
'account',
|
'account',
|
||||||
'me',
|
|
||||||
'boostModal',
|
|
||||||
'autoPlayGif',
|
|
||||||
'muted',
|
'muted',
|
||||||
'hidden',
|
'hidden',
|
||||||
]
|
]
|
||||||
@@ -200,7 +191,7 @@ export default class Status extends ImmutablePureComponent {
|
|||||||
} else {
|
} else {
|
||||||
media = (
|
media = (
|
||||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} autoPlayGif={this.props.autoPlayGif} />}
|
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
|
||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/status/action_bar
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -8,6 +5,7 @@ import IconButton from './icon_button';
|
|||||||
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
import DropdownMenuContainer from '../containers/dropdown_menu_container';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { me } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
delete: { id: 'status.delete', defaultMessage: 'Delete' },
|
||||||
@@ -50,7 +48,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
onEmbed: PropTypes.func,
|
onEmbed: PropTypes.func,
|
||||||
onMuteConversation: PropTypes.func,
|
onMuteConversation: PropTypes.func,
|
||||||
onPin: PropTypes.func,
|
onPin: PropTypes.func,
|
||||||
me: PropTypes.string,
|
|
||||||
withDismiss: PropTypes.bool,
|
withDismiss: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
@@ -59,7 +56,6 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
// evaluate to false. See react-immutable-pure-component for usage.
|
// evaluate to false. See react-immutable-pure-component for usage.
|
||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
'status',
|
'status',
|
||||||
'me',
|
|
||||||
'withDismiss',
|
'withDismiss',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -119,7 +115,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, me, intl, withDismiss } = this.props;
|
const { status, intl, withDismiss } = this.props;
|
||||||
|
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const anonymousAccess = !me;
|
const anonymousAccess = !me;
|
||||||
@@ -183,7 +179,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
|
|||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' ariaLabel={intl.formatMessage(messages.more)} />
|
<DropdownMenuContainer disabled={anonymousAccess} status={status} items={menu} icon='ellipsis-h' size={18} direction='right' title={intl.formatMessage(messages.more)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/status/content
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusContainer from '../../glitch/components/status/container';
|
import StatusContainer from '../containers/status_container';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/status/container
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
@@ -17,20 +14,18 @@ import {
|
|||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import {
|
import { blockAccount } from '../actions/accounts';
|
||||||
blockAccount,
|
|
||||||
muteAccount,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
|
||||||
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { boostModal, deleteModal } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
@@ -38,10 +33,6 @@ const makeMapStateToProps = () => {
|
|||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
status: getStatus(state, props.id),
|
status: getStatus(state, props.id),
|
||||||
me: state.getIn(['meta', 'me']),
|
|
||||||
boostModal: state.getIn(['meta', 'boost_modal']),
|
|
||||||
deleteModal: state.getIn(['meta', 'delete_modal']),
|
|
||||||
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
@@ -61,7 +52,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
if (status.get('reblogged')) {
|
if (status.get('reblogged')) {
|
||||||
dispatch(unreblog(status));
|
dispatch(unreblog(status));
|
||||||
} else {
|
} else {
|
||||||
if (e.shiftKey || !this.boostModal) {
|
if (e.shiftKey || !boostModal) {
|
||||||
this.onModalReblog(status);
|
this.onModalReblog(status);
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
|
||||||
@@ -90,7 +81,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onDelete (status) {
|
onDelete (status) {
|
||||||
if (!this.deleteModal) {
|
if (!deleteModal) {
|
||||||
dispatch(deleteStatus(status.get('id')));
|
dispatch(deleteStatus(status.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
@@ -126,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onMute (account) {
|
onMute (account) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initMuteModal(account));
|
||||||
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.muteConfirm),
|
|
||||||
onConfirm: () => dispatch(muteAccount(account.get('id'))),
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMuteConversation (status) {
|
onMuteConversation (status) {
|
||||||
|
|||||||
@@ -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}' },
|
||||||
@@ -26,7 +27,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,
|
||||||
@@ -44,7 +44,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 = '';
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
// THIS FILE EXISTS FOR UPSTREAM COMPATIBILITY & SHOULDN'T BE USED !!
|
|
||||||
// SEE INSTEAD : glitch/components/account/header
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -8,8 +5,9 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Motion from '../../ui/util/optional_motion';
|
import Motion from '../../ui/util/optional_motion';
|
||||||
import spring from 'react-motion/lib/spring';
|
import spring from 'react-motion/lib/spring';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import { autoPlayGif, me } from '../../../initial_state';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||||
@@ -17,19 +15,10 @@ const messages = defineMessages({
|
|||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
autoPlayGif: state.getIn(['meta', 'auto_play_gif']),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Avatar extends ImmutablePureComponent {
|
class Avatar extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map.isRequired,
|
account: ImmutablePropTypes.map.isRequired,
|
||||||
autoPlayGif: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -47,7 +36,7 @@ class Avatar extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, autoPlayGif } = this.props;
|
const { account } = this.props;
|
||||||
const { isHovered } = this.state;
|
const { isHovered } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -74,20 +63,17 @@ class Avatar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@connect(makeMapStateToProps)
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
export default class Header extends ImmutablePureComponent {
|
export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
me: PropTypes.string.isRequired,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
autoPlayGif: PropTypes.bool.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, intl } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
@@ -117,6 +103,10 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (account.get('moved')) {
|
||||||
|
actionBtn = '';
|
||||||
|
}
|
||||||
|
|
||||||
if (account.get('locked')) {
|
if (account.get('locked')) {
|
||||||
lockedIcon = <i className='fa fa-lock' />;
|
lockedIcon = <i className='fa fa-lock' />;
|
||||||
}
|
}
|
||||||
@@ -125,9 +115,9 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
<div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
|
<Avatar account={account} />
|
||||||
|
|
||||||
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
|
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
|
||||||
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import InnerHeader from '../../../../glitch/components/account/header';
|
import InnerHeader from '../../account/components/header';
|
||||||
import ActionBar from '../../account/components/action_bar';
|
import ActionBar from '../../account/components/action_bar';
|
||||||
import MissingIndicator from '../../../components/missing_indicator';
|
import MissingIndicator from '../../../components/missing_indicator';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import MovedNote from './moved_note';
|
||||||
|
|
||||||
export default class Header extends ImmutablePureComponent {
|
export default class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
me: PropTypes.string.isRequired,
|
|
||||||
onFollow: PropTypes.func.isRequired,
|
onFollow: PropTypes.func.isRequired,
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMention: PropTypes.func.isRequired,
|
onMention: PropTypes.func.isRequired,
|
||||||
@@ -61,7 +61,7 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me } = this.props;
|
const { account } = this.props;
|
||||||
|
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
return <MissingIndicator />;
|
return <MissingIndicator />;
|
||||||
@@ -69,15 +69,15 @@ export default class Header extends ImmutablePureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-timeline__header'>
|
<div className='account-timeline__header'>
|
||||||
|
{account.get('moved') && <MovedNote from={account} to={account.get('moved')} />}
|
||||||
|
|
||||||
<InnerHeader
|
<InnerHeader
|
||||||
account={account}
|
account={account}
|
||||||
me={me}
|
|
||||||
onFollow={this.handleFollow}
|
onFollow={this.handleFollow}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ActionBar
|
<ActionBar
|
||||||
account={account}
|
account={account}
|
||||||
me={me}
|
|
||||||
onBlock={this.handleBlock}
|
onBlock={this.handleBlock}
|
||||||
onMention={this.handleMention}
|
onMention={this.handleMention}
|
||||||
onReport={this.handleReport}
|
onReport={this.handleReport}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import AvatarOverlay from '../../../components/avatar_overlay';
|
||||||
|
import DisplayName from '../../../components/display_name';
|
||||||
|
|
||||||
|
export default class MovedNote extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
from: ImmutablePropTypes.map.isRequired,
|
||||||
|
to: ImmutablePropTypes.map.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAccountClick = e => {
|
||||||
|
if (e.button === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.context.router.history.push(`/accounts/${this.props.to.get('id')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { from, to } = this.props;
|
||||||
|
const displayNameHtml = { __html: from.get('display_name_html') };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='account__moved-note'>
|
||||||
|
<div className='account__moved-note__message'>
|
||||||
|
<div className='account__moved-note__icon-wrapper'><i className='fa fa-fw fa-suitcase account__moved-note__icon' /></div>
|
||||||
|
<FormattedMessage id='account.moved_to' defaultMessage='{name} has moved to:' values={{ name: <strong dangerouslySetInnerHTML={displayNameHtml} /> }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href={to.get('url')} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
||||||
|
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div>
|
||||||
|
<DisplayName account={to} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user