mirror of
https://iceshrimp.dev/blueb/Chuckya-fe-standalone.git
synced 2026-01-11 05:23:14 -08:00
Merge branch 'main' into standalone
This commit is contained in:
commit
911833f860
631 changed files with 17275 additions and 5147 deletions
|
|
@ -39,7 +39,7 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
"onCreateCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
|
||||||
"postCreateCommand": "bin/setup",
|
"postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup",
|
||||||
"waitFor": "postCreateCommand",
|
"waitFor": "postCreateCommand",
|
||||||
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
|
|
|
||||||
1
.github/workflows/crowdin-upload.yml
vendored
1
.github/workflows/crowdin-upload.yml
vendored
|
|
@ -19,6 +19,7 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
upload-translations:
|
upload-translations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'mastodon/mastodon'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|
|
||||||
2
.nvmrc
2
.nvmrc
|
|
@ -1 +1 @@
|
||||||
20.15
|
20.16
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,11 @@ You can contribute in the following ways:
|
||||||
|
|
||||||
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
|
If your contributions are accepted into Mastodon, you can request to be paid through [our OpenCollective](https://opencollective.com/mastodon).
|
||||||
|
|
||||||
|
Please review the org-level [contribution guidelines] for high-level acceptance
|
||||||
|
criteria guidance.
|
||||||
|
|
||||||
|
[contribution guidelines]: https://github.com/mastodon/.github/blob/main/CONTRIBUTING.md
|
||||||
|
|
||||||
## API Changes and Additions
|
## API Changes and Additions
|
||||||
|
|
||||||
Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation).
|
Please note that any changes or additions made to the API should have an accompanying pull request on [our documentation repository](https://github.com/mastodon/documentation).
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# syntax=docker/dockerfile:1.8
|
# syntax=docker/dockerfile:1.9
|
||||||
|
|
||||||
# This file is designed for production server deployment, not local development work
|
# This file is designed for production server deployment, not local development work
|
||||||
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
|
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
|
||||||
|
|
|
||||||
4
Gemfile
4
Gemfile
|
|
@ -88,7 +88,7 @@ gem 'sidekiq-unique-jobs', '~> 7.1'
|
||||||
gem 'simple_form', '~> 5.2'
|
gem 'simple_form', '~> 5.2'
|
||||||
gem 'simple-navigation', '~> 4.4'
|
gem 'simple-navigation', '~> 4.4'
|
||||||
gem 'stoplight', '~> 4.1'
|
gem 'stoplight', '~> 4.1'
|
||||||
gem 'strong_migrations', '1.8.0'
|
gem 'strong_migrations'
|
||||||
gem 'tty-prompt', '~> 0.23', require: false
|
gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2023'
|
gem 'tzinfo-data', '~> 1.2023'
|
||||||
|
|
@ -100,7 +100,7 @@ gem 'json-ld'
|
||||||
gem 'json-ld-preloaded', '~> 3.2'
|
gem 'json-ld-preloaded', '~> 3.2'
|
||||||
gem 'rdf-normalize', '~> 0.5'
|
gem 'rdf-normalize', '~> 0.5'
|
||||||
|
|
||||||
gem 'opentelemetry-api', '~> 1.2.5'
|
gem 'opentelemetry-api', '~> 1.3.0'
|
||||||
|
|
||||||
group :opentelemetry do
|
group :opentelemetry do
|
||||||
gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false
|
gem 'opentelemetry-exporter-otlp', '~> 0.28.0', require: false
|
||||||
|
|
|
||||||
83
Gemfile.lock
83
Gemfile.lock
|
|
@ -222,16 +222,16 @@ GEM
|
||||||
elasticsearch-transport (7.17.10)
|
elasticsearch-transport (7.17.10)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
multi_json
|
multi_json
|
||||||
email_spec (2.2.2)
|
email_spec (2.3.0)
|
||||||
htmlentities (~> 4.3.3)
|
htmlentities (~> 4.3.3)
|
||||||
launchy (~> 2.1)
|
launchy (>= 2.1, < 4.0)
|
||||||
mail (~> 2.7)
|
mail (~> 2.7)
|
||||||
erubi (1.13.0)
|
erubi (1.13.0)
|
||||||
et-orbi (1.2.11)
|
et-orbi (1.2.11)
|
||||||
tzinfo
|
tzinfo
|
||||||
excon (0.110.0)
|
excon (0.110.0)
|
||||||
fabrication (2.31.0)
|
fabrication (2.31.0)
|
||||||
faker (3.4.1)
|
faker (3.4.2)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
|
|
@ -289,7 +289,7 @@ GEM
|
||||||
ruby-progressbar (~> 1.4)
|
ruby-progressbar (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-protobuf (3.25.3)
|
google-protobuf (3.25.4)
|
||||||
googleapis-common-protos-types (1.14.0)
|
googleapis-common-protos-types (1.14.0)
|
||||||
google-protobuf (~> 3.18)
|
google-protobuf (~> 3.18)
|
||||||
haml (6.3.0)
|
haml (6.3.0)
|
||||||
|
|
@ -357,17 +357,18 @@ GEM
|
||||||
aes_key_wrap
|
aes_key_wrap
|
||||||
bindata
|
bindata
|
||||||
httpclient
|
httpclient
|
||||||
json-ld (3.3.1)
|
json-ld (3.3.2)
|
||||||
htmlentities (~> 4.3)
|
htmlentities (~> 4.3)
|
||||||
json-canonicalization (~> 1.0)
|
json-canonicalization (~> 1.0)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
multi_json (~> 1.15)
|
multi_json (~> 1.15)
|
||||||
rack (>= 2.2, < 4)
|
rack (>= 2.2, < 4)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
|
rexml (~> 3.2)
|
||||||
json-ld-preloaded (3.3.0)
|
json-ld-preloaded (3.3.0)
|
||||||
json-ld (~> 3.3)
|
json-ld (~> 3.3)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
json-schema (4.3.0)
|
json-schema (4.3.1)
|
||||||
addressable (>= 2.8)
|
addressable (>= 2.8)
|
||||||
jsonapi-renderer (0.2.2)
|
jsonapi-renderer (0.2.2)
|
||||||
jwt (2.7.1)
|
jwt (2.7.1)
|
||||||
|
|
@ -440,7 +441,7 @@ GEM
|
||||||
uri
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.12)
|
net-imap (0.4.14)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.19.0)
|
net-ldap (0.19.0)
|
||||||
|
|
@ -451,7 +452,7 @@ GEM
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.16.6)
|
nokogiri (1.16.7)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nsa (0.3.0)
|
nsa (0.3.0)
|
||||||
|
|
@ -492,10 +493,10 @@ GEM
|
||||||
openssl (3.2.0)
|
openssl (3.2.0)
|
||||||
openssl-signature_algorithm (1.3.0)
|
openssl-signature_algorithm (1.3.0)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
opentelemetry-api (1.2.5)
|
opentelemetry-api (1.3.0)
|
||||||
opentelemetry-common (0.20.1)
|
opentelemetry-common (0.20.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-exporter-otlp (0.28.0)
|
opentelemetry-exporter-otlp (0.28.1)
|
||||||
google-protobuf (>= 3.18)
|
google-protobuf (>= 3.18)
|
||||||
googleapis-common-protos-types (~> 1.3)
|
googleapis-common-protos-types (~> 1.3)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
|
|
@ -512,14 +513,14 @@ GEM
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rack (~> 0.21)
|
opentelemetry-instrumentation-rack (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_view (0.7.0)
|
opentelemetry-instrumentation-action_view (0.7.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_job (0.7.2)
|
opentelemetry-instrumentation-active_job (0.7.4)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_model_serializers (0.20.1)
|
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-active_record (0.7.2)
|
opentelemetry-instrumentation-active_record (0.7.2)
|
||||||
|
|
@ -531,32 +532,32 @@ GEM
|
||||||
opentelemetry-instrumentation-base (0.22.3)
|
opentelemetry-instrumentation-base (0.22.3)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-registry (~> 0.1)
|
opentelemetry-registry (~> 0.1)
|
||||||
opentelemetry-instrumentation-concurrent_ruby (0.21.3)
|
opentelemetry-instrumentation-concurrent_ruby (0.21.4)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-excon (0.22.3)
|
opentelemetry-instrumentation-excon (0.22.4)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-faraday (0.24.5)
|
opentelemetry-instrumentation-faraday (0.24.6)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-http (0.23.3)
|
opentelemetry-instrumentation-http (0.23.4)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-http_client (0.22.6)
|
opentelemetry-instrumentation-http_client (0.22.7)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-net_http (0.22.6)
|
opentelemetry-instrumentation-net_http (0.22.7)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-pg (0.27.3)
|
opentelemetry-instrumentation-pg (0.27.4)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rack (0.24.5)
|
opentelemetry-instrumentation-rack (0.24.6)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-rails (0.31.0)
|
opentelemetry-instrumentation-rails (0.31.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
||||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||||
|
|
@ -565,20 +566,20 @@ GEM
|
||||||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-redis (0.25.6)
|
opentelemetry-instrumentation-redis (0.25.7)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-instrumentation-sidekiq (0.25.6)
|
opentelemetry-instrumentation-sidekiq (0.25.7)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||||
opentelemetry-registry (0.3.1)
|
opentelemetry-registry (0.3.1)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-sdk (1.4.1)
|
opentelemetry-sdk (1.5.0)
|
||||||
opentelemetry-api (~> 1.1)
|
opentelemetry-api (~> 1.1)
|
||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-registry (~> 0.2)
|
opentelemetry-registry (~> 0.2)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-semantic_conventions (1.10.0)
|
opentelemetry-semantic_conventions (1.10.1)
|
||||||
opentelemetry-api (~> 1.0)
|
opentelemetry-api (~> 1.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
ox (2.14.18)
|
ox (2.14.18)
|
||||||
|
|
@ -589,7 +590,7 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.6)
|
pg (1.5.7)
|
||||||
pghero (3.6.0)
|
pghero (3.6.0)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
premailer (1.23.0)
|
premailer (1.23.0)
|
||||||
|
|
@ -607,13 +608,13 @@ GEM
|
||||||
railties (>= 7.0.0)
|
railties (>= 7.0.0)
|
||||||
psych (5.1.2)
|
psych (5.1.2)
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.0)
|
public_suffix (6.0.1)
|
||||||
puma (6.4.2)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.2)
|
pundit (2.3.2)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.0)
|
racc (1.8.1)
|
||||||
rack (2.2.9)
|
rack (2.2.9)
|
||||||
rack-attack (6.7.0)
|
rack-attack (6.7.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
|
|
@ -675,8 +676,9 @@ GEM
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
rdf (3.3.1)
|
rdf (3.3.2)
|
||||||
bcp47_spec (~> 0.2)
|
bcp47_spec (~> 0.2)
|
||||||
|
bigdecimal (~> 3.1, >= 3.1.5)
|
||||||
link_header (~> 0.0, >= 0.0.8)
|
link_header (~> 0.0, >= 0.0.8)
|
||||||
rdf-normalize (0.7.0)
|
rdf-normalize (0.7.0)
|
||||||
rdf (~> 3.3)
|
rdf (~> 3.3)
|
||||||
|
|
@ -696,7 +698,7 @@ GEM
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.1)
|
rexml (3.3.4)
|
||||||
strscan
|
strscan
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.2.1)
|
rouge (4.2.1)
|
||||||
|
|
@ -733,7 +735,7 @@ GEM
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.13.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.65.0)
|
rubocop (1.65.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
|
|
@ -766,21 +768,22 @@ GEM
|
||||||
ruby-saml (1.16.0)
|
ruby-saml (1.16.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
ruby-vips (2.2.1)
|
ruby-vips (2.2.2)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
rufus-scheduler (3.9.1)
|
rufus-scheduler (3.9.1)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
safety_net_attestation (0.4.0)
|
safety_net_attestation (0.4.0)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
sanitize (6.1.1)
|
sanitize (6.1.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
scenic (1.8.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
selenium-webdriver (4.22.0)
|
selenium-webdriver (4.23.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
|
|
@ -820,8 +823,8 @@ GEM
|
||||||
stoplight (4.1.0)
|
stoplight (4.1.0)
|
||||||
redlock (~> 1.0)
|
redlock (~> 1.0)
|
||||||
stringio (3.1.1)
|
stringio (3.1.1)
|
||||||
strong_migrations (1.8.0)
|
strong_migrations (2.0.0)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 6.1)
|
||||||
strscan (3.1.0)
|
strscan (3.1.0)
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
|
|
@ -893,7 +896,7 @@ GEM
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
websocket (1.2.10)
|
websocket (1.2.11)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
|
|
@ -982,7 +985,7 @@ DEPENDENCIES
|
||||||
omniauth-rails_csrf_protection (~> 1.0)
|
omniauth-rails_csrf_protection (~> 1.0)
|
||||||
omniauth-saml (~> 2.0)
|
omniauth-saml (~> 2.0)
|
||||||
omniauth_openid_connect (~> 0.6.1)
|
omniauth_openid_connect (~> 0.6.1)
|
||||||
opentelemetry-api (~> 1.2.5)
|
opentelemetry-api (~> 1.3.0)
|
||||||
opentelemetry-exporter-otlp (~> 0.28.0)
|
opentelemetry-exporter-otlp (~> 0.28.0)
|
||||||
opentelemetry-instrumentation-active_job (~> 0.7.1)
|
opentelemetry-instrumentation-active_job (~> 0.7.1)
|
||||||
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
|
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
|
||||||
|
|
@ -1045,7 +1048,7 @@ DEPENDENCIES
|
||||||
simplecov-lcov (~> 0.8)
|
simplecov-lcov (~> 0.8)
|
||||||
stackprof
|
stackprof
|
||||||
stoplight (~> 4.1)
|
stoplight (~> 4.1)
|
||||||
strong_migrations (= 1.8.0)
|
strong_migrations
|
||||||
test-prof
|
test-prof
|
||||||
thor (~> 1.2)
|
thor (~> 1.2)
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Should anything break, open `https://masto.example.com/logout.html` or clear loc
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# <img src="https://github.com/TheEssem/mastodon/raw/main/app/javascript/icons/android-chrome-256x256.png" width="128"> Chuckya
|
# <img src="https://github.com/TheEssem/mastodon/raw/main/public/chuckya.svg" width="128"> Chuckya
|
||||||
|
|
||||||
Chuckya is a close-to-upstream soft fork of Mastodon Glitch Edition (more commonly known as glitch-soc) that aims to introduce more experimental features/fixes with the goal of making the overall experience more enjoyable. Although it's mainly developed for and used on the [wetdry.world](https://wetdry.world) instance, it can be deployed by any server admin as a drop-in, backwards-compatible replacement for Mastodon.
|
Chuckya is a close-to-upstream soft fork of Mastodon Glitch Edition (more commonly known as glitch-soc) that aims to introduce more experimental features/fixes with the goal of making the overall experience more enjoyable. Although it's mainly developed for and used on the [wetdry.world](https://wetdry.world) instance, it can be deployed by any server admin as a drop-in, backwards-compatible replacement for Mastodon.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ module Admin
|
||||||
def show
|
def show
|
||||||
authorize :instance, :show?
|
authorize :instance, :show?
|
||||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||||
|
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,15 @@
|
||||||
|
|
||||||
module Admin
|
module Admin
|
||||||
class TagsController < BaseController
|
class TagsController < BaseController
|
||||||
before_action :set_tag
|
before_action :set_tag, except: [:index]
|
||||||
|
|
||||||
|
PER_PAGE = 20
|
||||||
|
|
||||||
|
def index
|
||||||
|
authorize :tag, :index?
|
||||||
|
|
||||||
|
@tags = filtered_tags.page(params[:page]).per(PER_PAGE)
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize @tag, :show?
|
authorize @tag, :show?
|
||||||
|
|
@ -31,5 +39,13 @@ module Admin
|
||||||
def tag_params
|
def tag_params
|
||||||
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
|
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filtered_tags
|
||||||
|
TagFilter.new(filter_params.with_defaults(order: 'newest')).results
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_params
|
||||||
|
params.slice(:page, *TagFilter::KEYS).permit(:page, *TagFilter::KEYS)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,10 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def limit_param(default_limit)
|
def limit_param(default_limit, max_limit = nil)
|
||||||
return default_limit unless params[:limit]
|
return default_limit unless params[:limit]
|
||||||
|
|
||||||
[params[:limit].to_i.abs, default_limit * 2].min
|
[params[:limit].to_i.abs, max_limit || (default_limit * 2)].min
|
||||||
end
|
end
|
||||||
|
|
||||||
def params_slice(*keys)
|
def params_slice(*keys)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
MAX_LIMIT = 500
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_allows' }, only: [:index, :show]
|
||||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_allows' }, except: [:index, :show]
|
||||||
|
|
@ -47,18 +48,13 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_domain_allows
|
def set_domain_allows
|
||||||
@domain_allows = filtered_domain_allows.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@domain_allows = DomainAllow.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_domain_allow
|
def set_domain_allow
|
||||||
@domain_allow = DomainAllow.find(params[:id])
|
@domain_allow = DomainAllow.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_domain_allows
|
|
||||||
# TODO: no filtering yet
|
|
||||||
DomainAllow.all
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||||
end
|
end
|
||||||
|
|
@ -72,7 +68,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
@domain_allows.size == limit_param(LIMIT)
|
@domain_allows.size == limit_param(LIMIT, MAX_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
include AccountableConcern
|
include AccountableConcern
|
||||||
|
|
||||||
LIMIT = 100
|
LIMIT = 100
|
||||||
|
MAX_LIMIT = 500
|
||||||
|
|
||||||
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:domain_blocks' }, only: [:index, :show]
|
||||||
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show]
|
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:domain_blocks' }, except: [:index, :show]
|
||||||
|
|
@ -59,18 +60,13 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
@domain_blocks = filtered_domain_blocks.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
|
@domain_blocks = DomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT, MAX_LIMIT), params_slice(:max_id, :since_id, :min_id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_domain_block
|
def set_domain_block
|
||||||
@domain_block = DomainBlock.find(params[:id])
|
@domain_block = DomainBlock.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def filtered_domain_blocks
|
|
||||||
# TODO: no filtering yet
|
|
||||||
DomainBlock.all
|
|
||||||
end
|
|
||||||
|
|
||||||
def domain_block_params
|
def domain_block_params
|
||||||
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
|
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
|
||||||
end
|
end
|
||||||
|
|
@ -88,7 +84,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
@domain_blocks.size == limit_param(LIMIT)
|
@domain_blocks.size == limit_param(LIMIT, MAX_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
|
||||||
|
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
before_action :set_request, except: :index
|
before_action :set_request, only: [:show, :accept, :dismiss]
|
||||||
|
before_action :set_requests, only: [:accept_bulk, :dismiss_bulk]
|
||||||
|
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
|
@ -32,6 +33,16 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def accept_bulk
|
||||||
|
@requests.each { |request| AcceptNotificationRequestService.new.call(request) }
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss_bulk
|
||||||
|
@requests.each(&:destroy!)
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_requests
|
def load_requests
|
||||||
|
|
@ -53,6 +64,10 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
||||||
@request = NotificationRequest.where(account: current_account).find(params[:id])
|
@request = NotificationRequest.where(account: current_account).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_requests
|
||||||
|
@requests = NotificationRequest.where(account: current_account, id: Array(params[:id]).uniq.map(&:to_i))
|
||||||
|
end
|
||||||
|
|
||||||
def next_path
|
def next_path
|
||||||
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
|
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
||||||
|
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
|
||||||
|
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
|
||||||
|
|
||||||
def index
|
def index
|
||||||
with_read_replica do
|
with_read_replica do
|
||||||
|
|
@ -17,6 +19,14 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
|
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def unread_count
|
||||||
|
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
|
||||||
|
|
||||||
|
with_read_replica do
|
||||||
|
render json: { count: browserable_account_notifications.paginate_by_min_id(limit, notification_marker&.last_read_id).count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.without_suspended.find(params[:id])
|
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||||
render json: @notification, serializer: REST::NotificationSerializer
|
render json: @notification, serializer: REST::NotificationSerializer
|
||||||
|
|
@ -63,6 +73,10 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notification_marker
|
||||||
|
current_user.markers.find_by(timeline: 'notifications')
|
||||||
|
end
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
|
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
|
||||||
@report = ReportService.new.call(
|
@report = ReportService.new.call(
|
||||||
current_account,
|
current_account,
|
||||||
reported_account,
|
reported_account,
|
||||||
report_params
|
report_params.merge(application: doorkeeper_token.application)
|
||||||
)
|
)
|
||||||
|
|
||||||
render json: @report, serializer: REST::ReportSerializer
|
render json: @report, serializer: REST::ReportSerializer
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,49 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
||||||
|
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
|
||||||
|
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
|
||||||
|
|
||||||
def index
|
def index
|
||||||
with_read_replica do
|
with_read_replica do
|
||||||
@notifications = load_notifications
|
@notifications = load_notifications
|
||||||
@group_metadata = load_group_metadata
|
@group_metadata = load_group_metadata
|
||||||
|
@grouped_notifications = load_grouped_notifications
|
||||||
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
@relationships = StatusRelationshipsPresenter.new(target_statuses_from_notifications, current_user&.account_id)
|
||||||
|
@sample_accounts = @grouped_notifications.flat_map(&:sample_accounts)
|
||||||
|
|
||||||
|
# Preload associations to avoid N+1s
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @sample_accounts, associations: [:account_stat, { user: :role }]).call
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }, each_serializer: REST::NotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
|
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#index rendering') do |span|
|
||||||
|
statuses = @grouped_notifications.filter_map { |group| group.target_status&.id }
|
||||||
|
|
||||||
|
span.add_attributes(
|
||||||
|
'app.notification_grouping.count' => @grouped_notifications.size,
|
||||||
|
'app.notification_grouping.sample_account.count' => @sample_accounts.size,
|
||||||
|
'app.notification_grouping.sample_account.unique_count' => @sample_accounts.pluck(:id).uniq.size,
|
||||||
|
'app.notification_grouping.status.count' => statuses.size,
|
||||||
|
'app.notification_grouping.status.unique_count' => statuses.uniq.size
|
||||||
|
)
|
||||||
|
|
||||||
|
presenter = GroupedNotificationsPresenter.new(@grouped_notifications)
|
||||||
|
render json: presenter, serializer: REST::DedupNotificationGroupSerializer, relationships: @relationships, group_metadata: @group_metadata
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unread_count
|
||||||
|
limit = limit_param(DEFAULT_NOTIFICATIONS_COUNT_LIMIT, MAX_NOTIFICATIONS_COUNT_LIMIT)
|
||||||
|
|
||||||
|
with_read_replica do
|
||||||
|
render json: { count: browserable_account_notifications.paginate_groups_by_min_id(limit, min_id: notification_marker&.last_read_id).count }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
|
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:id])
|
||||||
render json: NotificationGroup.from_notification(@notification), serializer: REST::NotificationGroupSerializer
|
presenter = GroupedNotificationsPresenter.new([NotificationGroup.from_notification(@notification)])
|
||||||
|
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear
|
def clear
|
||||||
|
|
@ -36,25 +65,35 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
|
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
|
||||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
|
||||||
params_slice(:max_id, :since_id, :min_id)
|
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||||
)
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
|
)
|
||||||
|
|
||||||
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
|
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
|
||||||
preload_collection(target_statuses, Status)
|
preload_collection(target_statuses, Status)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_group_metadata
|
def load_group_metadata
|
||||||
return {} if @notifications.empty?
|
return {} if @notifications.empty?
|
||||||
|
|
||||||
browserable_account_notifications
|
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
|
||||||
.where(group_key: @notifications.filter_map(&:group_key))
|
browserable_account_notifications
|
||||||
.where(id: (@notifications.last.id)..(@notifications.first.id))
|
.where(group_key: @notifications.filter_map(&:group_key))
|
||||||
.group(:group_key)
|
.where(id: (@notifications.last.id)..(@notifications.first.id))
|
||||||
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
|
.group(:group_key)
|
||||||
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
|
.pluck(:group_key, 'min(notifications.id) as min_id', 'max(notifications.id) as max_id', 'max(notifications.created_at) as latest_notification_at')
|
||||||
|
.to_h { |group_key, min_id, max_id, latest_notification_at| [group_key, { min_id: min_id, max_id: max_id, latest_notification_at: latest_notification_at }] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_grouped_notifications
|
||||||
|
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
|
||||||
|
@notifications.map { |notification| NotificationGroup.from_notification(notification, max_id: @group_metadata.dig(notification.group_key, :max_id)) }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
|
|
@ -65,6 +104,10 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def notification_marker
|
||||||
|
current_user.markers.find_by(timeline: 'notifications')
|
||||||
|
end
|
||||||
|
|
||||||
def target_statuses_from_notifications
|
def target_statuses_from_notifications
|
||||||
@notifications.filter_map(&:target_status)
|
@notifications.filter_map(&:target_status)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base
|
||||||
helper_method :current_theme
|
helper_method :current_theme
|
||||||
helper_method :single_user_mode?
|
helper_method :single_user_mode?
|
||||||
helper_method :use_seamless_external_login?
|
helper_method :use_seamless_external_login?
|
||||||
helper_method :omniauth_only?
|
|
||||||
helper_method :sso_account_settings
|
helper_method :sso_account_settings
|
||||||
helper_method :limited_federation_mode?
|
helper_method :limited_federation_mode?
|
||||||
helper_method :body_class_string
|
helper_method :body_class_string
|
||||||
|
|
@ -140,10 +139,6 @@ class ApplicationController < ActionController::Base
|
||||||
Devise.pam_authentication || Devise.ldap_authentication
|
Devise.pam_authentication || Devise.ldap_authentication
|
||||||
end
|
end
|
||||||
|
|
||||||
def omniauth_only?
|
|
||||||
ENV['OMNIAUTH_ONLY'] == 'true'
|
|
||||||
end
|
|
||||||
|
|
||||||
def sso_account_settings
|
def sso_account_settings
|
||||||
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
|
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||||
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
|
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
|
||||||
|
|
||||||
|
|
@ -73,10 +72,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
||||||
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
end
|
|
||||||
|
|
||||||
def after_resending_confirmation_instructions_path_for(_resource_name)
|
def after_resending_confirmation_instructions_path_for(_resource_name)
|
||||||
if user_signed_in?
|
if user_signed_in?
|
||||||
if current_user.confirmed? && current_user.approved?
|
if current_user.confirmed? && current_user.approved?
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
class Auth::PasswordsController < Devise::PasswordsController
|
class Auth::PasswordsController < Devise::PasswordsController
|
||||||
skip_before_action :check_self_destruct!
|
skip_before_action :check_self_destruct!
|
||||||
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
||||||
before_action :set_body_classes
|
|
||||||
|
|
||||||
layout 'auth'
|
layout 'auth'
|
||||||
|
|
||||||
|
|
@ -24,10 +23,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
||||||
redirect_to new_password_path(resource_name)
|
redirect_to new_password_path(resource_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset_password_token_is_valid?
|
def reset_password_token_is_valid?
|
||||||
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
def set_body_classes
|
||||||
@body_classes = %w(edit update).include?(action_name) ? 'admin' : 'lighter'
|
@body_classes = 'admin' if %w(edit update).include?(action_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_invite
|
def set_invite
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
include Auth::TwoFactorAuthenticationConcern
|
include Auth::TwoFactorAuthenticationConcern
|
||||||
|
|
||||||
before_action :set_body_classes
|
|
||||||
|
|
||||||
content_security_policy only: :new do |p|
|
content_security_policy only: :new do |p|
|
||||||
p.form_action(false)
|
p.form_action(false)
|
||||||
end
|
end
|
||||||
|
|
@ -103,10 +101,6 @@ class Auth::SessionsController < Devise::SessionsController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
end
|
|
||||||
|
|
||||||
def home_paths(resource)
|
def home_paths(resource)
|
||||||
paths = [about_path, '/explore']
|
paths = [about_path, '/explore']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ class Auth::SetupController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :require_unconfirmed_or_pending!
|
before_action :require_unconfirmed_or_pending!
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
@ -35,10 +34,6 @@ class Auth::SetupController < ApplicationController
|
||||||
@user = current_user
|
@user = current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email)
|
params.require(:user).permit(:email)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ module Auth::TwoFactorAuthenticationConcern
|
||||||
def prompt_for_two_factor(user)
|
def prompt_for_two_factor(user)
|
||||||
register_attempt_in_session(user)
|
register_attempt_in_session(user)
|
||||||
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
@webauthn_enabled = user.webauthn_enabled?
|
@webauthn_enabled = user.webauthn_enabled?
|
||||||
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
||||||
'webauthn'
|
'webauthn'
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ module ChallengableConcern
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_challenge
|
def render_challenge
|
||||||
@body_classes = 'lighter'
|
|
||||||
render 'auth/challenges/new', layout: 'auth'
|
render 'auth/challenges/new', layout: 'auth'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ class MailSubscriptionsController < ApplicationController
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_user
|
before_action :set_user
|
||||||
before_action :set_type
|
before_action :set_type
|
||||||
|
|
||||||
|
|
@ -25,10 +24,6 @@ class MailSubscriptionsController < ApplicationController
|
||||||
not_found unless @user
|
not_found unless @user
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'lighter'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_type
|
def set_type
|
||||||
@type = email_type_from_param
|
@type = email_type_from_param
|
||||||
end
|
end
|
||||||
|
|
|
||||||
15
app/helpers/admin/tags_helper.rb
Normal file
15
app/helpers/admin/tags_helper.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Admin::TagsHelper
|
||||||
|
def admin_tags_moderation_options
|
||||||
|
[
|
||||||
|
[t('admin.tags.moderation.reviewed'), 'reviewed'],
|
||||||
|
[t('admin.tags.moderation.review_requested'), 'review_requested'],
|
||||||
|
[t('admin.tags.moderation.unreviewed'), 'unreviewed'],
|
||||||
|
[t('admin.tags.moderation.trendable'), 'trendable'],
|
||||||
|
[t('admin.tags.moderation.not_trendable'), 'not_trendable'],
|
||||||
|
[t('admin.tags.moderation.usable'), 'usable'],
|
||||||
|
[t('admin.tags.moderation.not_usable'), 'not_usable'],
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -5,8 +5,10 @@ module ThemeHelper
|
||||||
flavour, theme = flavour_and_skin
|
flavour, theme = flavour_and_skin
|
||||||
|
|
||||||
if theme == 'system'
|
if theme == 'system'
|
||||||
stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
|
''.html_safe.tap do |tags|
|
||||||
stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
tags << stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
|
tags << stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous'
|
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous'
|
||||||
end
|
end
|
||||||
|
|
@ -16,8 +18,10 @@ module ThemeHelper
|
||||||
_, theme = flavour_and_skin
|
_, theme = flavour_and_skin
|
||||||
|
|
||||||
if theme == 'system'
|
if theme == 'system'
|
||||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
|
''.html_safe.tap do |tags|
|
||||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
|
tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)')
|
||||||
|
tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
|
||||||
|
end
|
||||||
else
|
else
|
||||||
tag.meta name: 'theme-color', content: theme_color_for(theme)
|
tag.meta name: 'theme-color', content: theme_color_for(theme)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -316,8 +316,8 @@ function loaded() {
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
statusEl.dataset.spoiler === 'expanded'
|
statusEl.dataset.spoiler === 'expanded'
|
||||||
? localeData['status.show_less'] ?? 'Show less'
|
? (localeData['status.show_less'] ?? 'Show less')
|
||||||
: localeData['status.show_more'] ?? 'Show more';
|
: (localeData['status.show_more'] ?? 'Show more');
|
||||||
spoilerLink.textContent = new IntlMessageFormat(
|
spoilerLink.textContent = new IntlMessageFormat(
|
||||||
message,
|
message,
|
||||||
locale,
|
locale,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { browserHistory } from 'flavours/glitch/components/router';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -722,6 +724,16 @@ export const updateAccount = ({ displayName, note, avatar, header, discoverable,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const navigateToProfile = (accountId) => {
|
||||||
|
return (_dispatch, getState) => {
|
||||||
|
const acct = getState().accounts.getIn([accountId, 'acct']);
|
||||||
|
|
||||||
|
if (acct) {
|
||||||
|
browserHistory.push(`/@${acct}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function fetchPinnedAccountsSuggestions(q) {
|
export function fetchPinnedAccountsSuggestions(q) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import type { LayoutType } from '../is_mobile';
|
import type { LayoutType } from '../is_mobile';
|
||||||
|
|
||||||
|
export const focusApp = createAction('APP_FOCUS');
|
||||||
|
export const unfocusApp = createAction('APP_UNFOCUS');
|
||||||
|
|
||||||
interface ChangeLayoutPayload {
|
interface ChangeLayoutPayload {
|
||||||
layout: LayoutType;
|
layout: LayoutType;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
import api from 'flavours/glitch/api';
|
import api from 'flavours/glitch/api';
|
||||||
|
import { browserHistory } from 'flavours/glitch/components/router';
|
||||||
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
import { search as emojiSearch } from 'flavours/glitch/features/emoji/emoji_mart_search_light';
|
||||||
import { tagHistory } from 'flavours/glitch/settings';
|
import { tagHistory } from 'flavours/glitch/settings';
|
||||||
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
||||||
|
|
@ -94,9 +95,9 @@ const messages = defineMessages({
|
||||||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
export const ensureComposeIsVisible = (getState) => {
|
||||||
if (!getState().getIn(['compose', 'mounted'])) {
|
if (!getState().getIn(['compose', 'mounted'])) {
|
||||||
routerHistory.push('/publish');
|
browserHistory.push('/publish');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -117,7 +118,7 @@ export function changeCompose(text) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replyCompose(status, routerHistory) {
|
export function replyCompose(status) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
|
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
|
||||||
dispatch({
|
dispatch({
|
||||||
|
|
@ -126,7 +127,19 @@ export function replyCompose(status, routerHistory) {
|
||||||
prependCWRe: prependCWRe,
|
prependCWRe: prependCWRe,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replyComposeById(statusId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const status = state.statuses.get(statusId);
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
const account = state.accounts.get(status.get('account'));
|
||||||
|
dispatch(replyCompose(status.set('account', account)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,38 +155,44 @@ export function resetCompose() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => {
|
export const focusCompose = (defaultText) => (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_FOCUS,
|
type: COMPOSE_FOCUS,
|
||||||
defaultText,
|
defaultText,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function mentionCompose(account, routerHistory) {
|
export function mentionCompose(account) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_MENTION,
|
type: COMPOSE_MENTION,
|
||||||
account: account,
|
account: account,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function directCompose(account, routerHistory) {
|
export function mentionComposeById(accountId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch(mentionCompose(getState().accounts.get(accountId)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function directCompose(account) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: COMPOSE_DIRECT,
|
type: COMPOSE_DIRECT,
|
||||||
account: account,
|
account: account,
|
||||||
});
|
});
|
||||||
|
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitCompose(routerHistory, overridePrivacy = null) {
|
export function submitCompose(overridePrivacy = null) {
|
||||||
return function (dispatch, getState) {
|
return function (dispatch, getState) {
|
||||||
let status = getState().getIn(['compose', 'text'], '');
|
let status = getState().getIn(['compose', 'text'], '');
|
||||||
const media = getState().getIn(['compose', 'media_attachments']);
|
const media = getState().getIn(['compose', 'media_attachments']);
|
||||||
|
|
@ -230,11 +249,10 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
||||||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||||
},
|
},
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
if (routerHistory
|
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new')
|
||||||
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
|
|
||||||
&& window.history.state
|
&& window.history.state
|
||||||
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
|
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
|
||||||
routerHistory.goBack();
|
browserHistory.goBack();
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||||
|
|
@ -272,7 +290,7 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
||||||
message: statusId === null ? messages.published : messages.saved,
|
message: statusId === null ? messages.published : messages.saved,
|
||||||
action: messages.open,
|
action: messages.open,
|
||||||
dismissAfter: 10000,
|
dismissAfter: 10000,
|
||||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}).catch(function (error) {
|
}).catch(function (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
import { boostModal, favouriteModal } from 'flavours/glitch/initial_state';
|
||||||
|
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||||
|
import { unreblog, reblog } from './interactions_typed';
|
||||||
|
import { openModal } from './modal';
|
||||||
|
|
||||||
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
export const REBLOGS_EXPAND_REQUEST = 'REBLOGS_EXPAND_REQUEST';
|
||||||
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
export const REBLOGS_EXPAND_SUCCESS = 'REBLOGS_EXPAND_SUCCESS';
|
||||||
|
|
@ -443,6 +447,64 @@ export function unpinFail(status, error) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleReblogWithoutConfirmation(status, privacy) {
|
||||||
|
return (dispatch) => {
|
||||||
|
if (status.get('reblogged')) {
|
||||||
|
dispatch(unreblog({ statusId: status.get('id') }));
|
||||||
|
} else {
|
||||||
|
dispatch(reblog({ statusId: status.get('id'), privacy }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleReblog(statusId, skipModal = false) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
let status = state.statuses.get(statusId);
|
||||||
|
|
||||||
|
if (!status)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The reblog modal expects a pre-filled account in status
|
||||||
|
// TODO: fix this by having the reblog modal get a statusId and do the work itself
|
||||||
|
status = status.set('account', state.accounts.get(status.get('account')));
|
||||||
|
|
||||||
|
const missing_description_setting = state.getIn(['local_settings', 'confirm_boost_missing_media_description']);
|
||||||
|
const missing_description = status.get('media_attachments').some(item => !item.get('description'));
|
||||||
|
if (missing_description_setting && missing_description && !status.get('reblogged')) {
|
||||||
|
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)), missingMediaDescription: true } }));
|
||||||
|
} else if (boostModal && !skipModal) {
|
||||||
|
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: (status, privacy) => dispatch(toggleReblogWithoutConfirmation(status, privacy)) } }));
|
||||||
|
} else {
|
||||||
|
dispatch(toggleReblogWithoutConfirmation(status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleFavourite(statusId, skipModal = false) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
let status = state.statuses.get(statusId);
|
||||||
|
|
||||||
|
if (!status)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The favourite modal expects a pre-filled account in status
|
||||||
|
// TODO: fix this by having the reblog modal get a statusId and do the work itself
|
||||||
|
status = status.set('account', state.accounts.get(status.get('account')));
|
||||||
|
|
||||||
|
if (status.get('favourited')) {
|
||||||
|
dispatch(unfavourite(status));
|
||||||
|
} else {
|
||||||
|
if (favouriteModal && !skipModal) {
|
||||||
|
dispatch(openModal({ modalType: 'FAVOURITE', modalProps: { status, onFavourite: (status) => dispatch(favourite(status)) } }));
|
||||||
|
} else {
|
||||||
|
dispatch(favourite(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const addReaction = (statusId, name, url) => (dispatch, getState) => {
|
export const addReaction = (statusId, name, url) => (dispatch, getState) => {
|
||||||
const status = getState().get('statuses').get(statusId);
|
const status = getState().get('statuses').get(statusId);
|
||||||
let alreadyAdded = false;
|
let alreadyAdded = false;
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,17 @@ interface MarkerParam {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLastNotificationId(state: RootState): string | undefined {
|
function getLastNotificationId(state: RootState): string | undefined {
|
||||||
// @ts-expect-error state.notifications is not yet typed
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
const enableBeta = state.settings.getIn(
|
||||||
return state.getIn(['notifications', 'lastReadId']);
|
['notifications', 'groupingBeta'],
|
||||||
|
false,
|
||||||
|
) as boolean;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return enableBeta
|
||||||
|
? state.notificationGroups.lastReadId
|
||||||
|
: // @ts-expect-error state.notifications is not yet typed
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||||
|
state.getIn(['notifications', 'lastReadId']);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPostMarkersParams = (state: RootState) => {
|
const buildPostMarkersParams = (state: RootState) => {
|
||||||
|
|
|
||||||
144
app/javascript/flavours/glitch/actions/notification_groups.ts
Normal file
144
app/javascript/flavours/glitch/actions/notification_groups.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import {
|
||||||
|
apiClearNotifications,
|
||||||
|
apiFetchNotifications,
|
||||||
|
} from 'flavours/glitch/api/notifications';
|
||||||
|
import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
||||||
|
import type {
|
||||||
|
ApiNotificationGroupJSON,
|
||||||
|
ApiNotificationJSON,
|
||||||
|
} from 'flavours/glitch/api_types/notifications';
|
||||||
|
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
|
||||||
|
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
||||||
|
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
|
||||||
|
import {
|
||||||
|
selectSettingsNotificationsExcludedTypes,
|
||||||
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
|
} from 'flavours/glitch/selectors/settings';
|
||||||
|
import type { AppDispatch } from 'flavours/glitch/store';
|
||||||
|
import {
|
||||||
|
createAppAsyncThunk,
|
||||||
|
createDataLoadingThunk,
|
||||||
|
} from 'flavours/glitch/store/typed_functions';
|
||||||
|
|
||||||
|
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||||
|
import { NOTIFICATIONS_FILTER_SET } from './notifications';
|
||||||
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
|
function excludeAllTypesExcept(filter: string) {
|
||||||
|
return allNotificationTypes.filter((item) => item !== filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchAssociatedRecords(
|
||||||
|
dispatch: AppDispatch,
|
||||||
|
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
||||||
|
) {
|
||||||
|
const fetchedAccounts: ApiAccountJSON[] = [];
|
||||||
|
const fetchedStatuses: ApiStatusJSON[] = [];
|
||||||
|
|
||||||
|
notifications.forEach((notification) => {
|
||||||
|
if (notification.type === 'admin.report') {
|
||||||
|
fetchedAccounts.push(notification.report.target_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.type === 'moderation_warning') {
|
||||||
|
fetchedAccounts.push(notification.moderation_warning.target_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('status' in notification) {
|
||||||
|
fetchedStatuses.push(notification.status);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fetchedAccounts.length > 0)
|
||||||
|
dispatch(importFetchedAccounts(fetchedAccounts));
|
||||||
|
|
||||||
|
if (fetchedStatuses.length > 0)
|
||||||
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
|
'notificationGroups/fetch',
|
||||||
|
async (_params, { getState }) => {
|
||||||
|
const activeFilter =
|
||||||
|
selectSettingsNotificationsQuickFilterActive(getState());
|
||||||
|
|
||||||
|
return apiFetchNotifications({
|
||||||
|
exclude_types:
|
||||||
|
activeFilter === 'all'
|
||||||
|
? selectSettingsNotificationsExcludedTypes(getState())
|
||||||
|
: excludeAllTypesExcept(activeFilter),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
const payload: (ApiNotificationGroupJSON | NotificationGap)[] =
|
||||||
|
notifications;
|
||||||
|
|
||||||
|
// TODO: might be worth not using gaps for that…
|
||||||
|
// if (nextLink) payload.push({ type: 'gap', loadUrl: nextLink.uri });
|
||||||
|
if (notifications.length > 1)
|
||||||
|
payload.push({ type: 'gap', maxId: notifications.at(-1)?.page_min_id });
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
// dispatch(submitMarkers());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||||
|
'notificationGroups/fetchGap',
|
||||||
|
async (params: { gap: NotificationGap }) =>
|
||||||
|
apiFetchNotifications({ max_id: params.gap.maxId }),
|
||||||
|
|
||||||
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
dispatchAssociatedRecords(dispatch, notifications);
|
||||||
|
|
||||||
|
return { notifications };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||||
|
'notificationGroups/processNew',
|
||||||
|
(notification: ApiNotificationJSON, { dispatch }) => {
|
||||||
|
dispatchAssociatedRecords(dispatch, [notification]);
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadPending = createAction('notificationGroups/loadPending');
|
||||||
|
|
||||||
|
export const updateScrollPosition = createAction<{ top: boolean }>(
|
||||||
|
'notificationGroups/updateScrollPosition',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
|
'notifications/filter/set',
|
||||||
|
({ filterType }: { filterType: string }, { dispatch }) => {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_FILTER_SET,
|
||||||
|
path: ['notifications', 'quickFilter', 'active'],
|
||||||
|
value: filterType,
|
||||||
|
});
|
||||||
|
// dispatch(expandNotifications({ forceLoad: true }));
|
||||||
|
void dispatch(fetchNotifications());
|
||||||
|
dispatch(saveSettings());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clearNotifications = createDataLoadingThunk(
|
||||||
|
'notifications/clear',
|
||||||
|
() => apiClearNotifications(),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const markNotificationsAsRead = createAction(
|
||||||
|
'notificationGroups/markAsRead',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const mountNotifications = createAction('notificationGroups/mount');
|
||||||
|
export const unmountNotifications = createAction('notificationGroups/unmount');
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
apiGetNotificationPolicy,
|
apiGetNotificationPolicy,
|
||||||
apiUpdateNotificationsPolicy,
|
apiUpdateNotificationsPolicy,
|
||||||
|
|
@ -14,3 +16,7 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
|
||||||
'notificationPolicy/update',
|
'notificationPolicy/update',
|
||||||
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
|
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const decreasePendingNotificationsCount = createAction<number>(
|
||||||
|
'notificationPolicy/decreasePendingNotificationCount',
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
importFetchedStatuses,
|
importFetchedStatuses,
|
||||||
} from './importer';
|
} from './importer';
|
||||||
import { submitMarkers } from './markers';
|
import { submitMarkers } from './markers';
|
||||||
|
import { decreasePendingNotificationsCount } from './notification_policies';
|
||||||
import { notificationsUpdate } from "./notifications_typed";
|
import { notificationsUpdate } from "./notifications_typed";
|
||||||
import { register as registerPushNotifications } from './push_notifications';
|
import { register as registerPushNotifications } from './push_notifications';
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
@ -43,7 +44,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||||
|
|
||||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
|
||||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING';
|
||||||
|
|
||||||
|
|
@ -97,6 +97,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectNotificationCountForRequest = (state, id) => {
|
||||||
|
const requests = state.getIn(['notificationRequests', 'items']);
|
||||||
|
const thisRequest = requests.find(request => request.get('id') === id);
|
||||||
|
return thisRequest ? thisRequest.get('notifications_count') : 0;
|
||||||
|
};
|
||||||
|
|
||||||
export const loadPending = () => ({
|
export const loadPending = () => ({
|
||||||
type: NOTIFICATIONS_LOAD_PENDING,
|
type: NOTIFICATIONS_LOAD_PENDING,
|
||||||
});
|
});
|
||||||
|
|
@ -187,7 +193,7 @@ const noOp = () => {};
|
||||||
|
|
||||||
let expandNotificationsController = new AbortController();
|
let expandNotificationsController = new AbortController();
|
||||||
|
|
||||||
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||||
const notifications = getState().get('notifications');
|
const notifications = getState().get('notifications');
|
||||||
|
|
@ -270,16 +276,6 @@ export function expandNotificationsFail(error, isLoadingMore) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearNotifications() {
|
|
||||||
return (dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: NOTIFICATIONS_CLEAR,
|
|
||||||
});
|
|
||||||
|
|
||||||
api().post('/api/v1/notifications/clear');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollTopNotifications(top) {
|
export function scrollTopNotifications(top) {
|
||||||
return {
|
return {
|
||||||
type: NOTIFICATIONS_SCROLL_TOP,
|
type: NOTIFICATIONS_SCROLL_TOP,
|
||||||
|
|
@ -533,11 +529,13 @@ export const fetchNotificationRequestFail = (id, error) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const acceptNotificationRequest = id => (dispatch) => {
|
export const acceptNotificationRequest = (id) => (dispatch, getState) => {
|
||||||
|
const count = selectNotificationCountForRequest(getState(), id);
|
||||||
dispatch(acceptNotificationRequestRequest(id));
|
dispatch(acceptNotificationRequestRequest(id));
|
||||||
|
|
||||||
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||||
dispatch(acceptNotificationRequestSuccess(id));
|
dispatch(acceptNotificationRequestSuccess(id));
|
||||||
|
dispatch(decreasePendingNotificationsCount(count));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(acceptNotificationRequestFail(id, err));
|
dispatch(acceptNotificationRequestFail(id, err));
|
||||||
});
|
});
|
||||||
|
|
@ -559,11 +557,13 @@ export const acceptNotificationRequestFail = (id, error) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dismissNotificationRequest = id => (dispatch) => {
|
export const dismissNotificationRequest = (id) => (dispatch, getState) => {
|
||||||
|
const count = selectNotificationCountForRequest(getState(), id);
|
||||||
dispatch(dismissNotificationRequestRequest(id));
|
dispatch(dismissNotificationRequestRequest(id));
|
||||||
|
|
||||||
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||||
dispatch(dismissNotificationRequestSuccess(id));
|
dispatch(dismissNotificationRequestSuccess(id));
|
||||||
|
dispatch(decreasePendingNotificationsCount(count));
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
dispatch(dismissNotificationRequestFail(id, err));
|
dispatch(dismissNotificationRequestFail(id, err));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { createAppAsyncThunk } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { fetchNotifications } from './notification_groups';
|
||||||
|
import { expandNotifications } from './notifications';
|
||||||
|
|
||||||
|
export const initializeNotifications = createAppAsyncThunk(
|
||||||
|
'notifications/initialize',
|
||||||
|
(_, { dispatch, getState }) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const enableBeta = getState().settings.getIn(
|
||||||
|
['notifications', 'groupingBeta'],
|
||||||
|
false,
|
||||||
|
) as boolean;
|
||||||
|
|
||||||
|
if (enableBeta) void dispatch(fetchNotifications());
|
||||||
|
else dispatch(expandNotifications());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { createAction } from '@reduxjs/toolkit';
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
import type { ApiAccountJSON } from '../api_types/accounts';
|
import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications';
|
||||||
// To be replaced once ApiNotificationJSON type exists
|
|
||||||
interface FakeApiNotificationJSON {
|
|
||||||
type: string;
|
|
||||||
account: ApiAccountJSON;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const notificationsUpdate = createAction(
|
export const notificationsUpdate = createAction(
|
||||||
'notifications/update',
|
'notifications/update',
|
||||||
|
|
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
|
||||||
playSound,
|
playSound,
|
||||||
...args
|
...args
|
||||||
}: {
|
}: {
|
||||||
notification: FakeApiNotificationJSON;
|
notification: ApiNotificationJSON;
|
||||||
usePendingItems: boolean;
|
usePendingItems: boolean;
|
||||||
playSound: boolean;
|
playSound: boolean;
|
||||||
}) => ({
|
}) => ({
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { browserHistory } from 'flavours/glitch/components/router';
|
||||||
|
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||||
|
|
@ -94,7 +96,7 @@ export function redraft(status, raw_text, content_type) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
export const editStatus = (id) => (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
if (status.get('poll')) {
|
if (status.get('poll')) {
|
||||||
|
|
@ -105,7 +107,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
||||||
|
|
||||||
api().get(`/api/v1/statuses/${id}/source`).then(response => {
|
api().get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||||
dispatch(fetchStatusSourceSuccess());
|
dispatch(fetchStatusSourceSuccess());
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
|
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusSourceFail(error));
|
dispatch(fetchStatusSourceFail(error));
|
||||||
|
|
@ -125,7 +127,7 @@ export const fetchStatusSourceFail = error => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
export function deleteStatus(id, withRedraft = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
let status = getState().getIn(['statuses', id]);
|
let status = getState().getIn(['statuses', id]);
|
||||||
|
|
||||||
|
|
@ -142,7 +144,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
|
|
||||||
if (withRedraft) {
|
if (withRedraft) {
|
||||||
dispatch(redraft(status, response.data.text, response.data.content_type));
|
dispatch(redraft(status, response.data.text, response.data.content_type));
|
||||||
ensureComposeIsVisible(getState, routerHistory);
|
ensureComposeIsVisible(getState);
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(deleteStatusFail(id, error));
|
dispatch(deleteStatusFail(id, error));
|
||||||
|
|
@ -309,6 +311,21 @@ export function revealStatus(ids) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggleStatusSpoilers(statusId) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const status = getState().statuses.get(statusId);
|
||||||
|
|
||||||
|
if (!status)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (status.get('hidden')) {
|
||||||
|
dispatch(revealStatus(statusId));
|
||||||
|
} else {
|
||||||
|
dispatch(hideStatus(statusId));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function toggleStatusCollapse(id, isCollapsed) {
|
export function toggleStatusCollapse(id, isCollapsed) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_COLLAPSE,
|
type: STATUS_COLLAPSE,
|
||||||
|
|
@ -349,3 +366,15 @@ export const undoStatusTranslation = (id, pollId) => ({
|
||||||
id,
|
id,
|
||||||
pollId,
|
pollId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const navigateToStatus = (statusId) => {
|
||||||
|
return (_dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
const accountId = state.statuses.getIn([statusId, 'account']);
|
||||||
|
const acct = state.accounts.getIn([accountId, 'acct']);
|
||||||
|
|
||||||
|
if (acct) {
|
||||||
|
browserHistory.push(`/@${acct}/${statusId}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
deleteAnnouncement,
|
deleteAnnouncement,
|
||||||
} from './announcements';
|
} from './announcements';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
|
import { processNewNotificationForGroups } from './notification_groups';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateStatus } from './statuses';
|
import { updateStatus } from './statuses';
|
||||||
import {
|
import {
|
||||||
|
|
@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
||||||
case 'delete':
|
case 'delete':
|
||||||
dispatch(deleteFromTimelines(data.payload));
|
dispatch(deleteFromTimelines(data.payload));
|
||||||
break;
|
break;
|
||||||
case 'notification':
|
case 'notification': {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
|
const notificationJSON = JSON.parse(data.payload);
|
||||||
|
dispatch(updateNotifications(notificationJSON, messages, locale));
|
||||||
|
// TODO: remove this once the groups feature replaces the previous one
|
||||||
|
if(getState().notificationGroups.groups.length > 0) {
|
||||||
|
dispatch(processNewNotificationForGroups(notificationJSON));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
dispatch(updateConversations(JSON.parse(data.payload)));
|
dispatch(updateConversations(JSON.parse(data.payload)));
|
||||||
|
|
|
||||||
25
app/javascript/flavours/glitch/api/notifications.ts
Normal file
25
app/javascript/flavours/glitch/api/notifications.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import api, { apiRequest, getLinks } from 'flavours/glitch/api';
|
||||||
|
import type { ApiNotificationGroupsResultJSON } from 'flavours/glitch/api_types/notifications';
|
||||||
|
|
||||||
|
export const apiFetchNotifications = async (params?: {
|
||||||
|
exclude_types?: string[];
|
||||||
|
max_id?: string;
|
||||||
|
}) => {
|
||||||
|
const response = await api().request<ApiNotificationGroupsResultJSON>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/v2_alpha/notifications',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { statuses, accounts, notification_groups } = response.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
statuses,
|
||||||
|
accounts,
|
||||||
|
notifications: notification_groups,
|
||||||
|
links: getLinks(response),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const apiClearNotifications = () =>
|
||||||
|
apiRequest<undefined>('POST', 'v1/notifications/clear');
|
||||||
153
app/javascript/flavours/glitch/api_types/notifications.ts
Normal file
153
app/javascript/flavours/glitch/api_types/notifications.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
// See app/serializers/rest/notification_group_serializer.rb
|
||||||
|
|
||||||
|
import type { AccountWarningAction } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
import type { ApiAccountJSON } from './accounts';
|
||||||
|
import type { ApiReportJSON } from './reports';
|
||||||
|
import type { ApiStatusJSON } from './statuses';
|
||||||
|
|
||||||
|
// See app/model/notification.rb
|
||||||
|
export const allNotificationTypes = [
|
||||||
|
'follow',
|
||||||
|
'follow_request',
|
||||||
|
'favourite',
|
||||||
|
'reaction',
|
||||||
|
'reblog',
|
||||||
|
'mention',
|
||||||
|
'poll',
|
||||||
|
'status',
|
||||||
|
'update',
|
||||||
|
'admin.sign_up',
|
||||||
|
'admin.report',
|
||||||
|
'moderation_warning',
|
||||||
|
'severed_relationships',
|
||||||
|
];
|
||||||
|
|
||||||
|
export type NotificationWithStatusType =
|
||||||
|
| 'favourite'
|
||||||
|
| 'reaction'
|
||||||
|
| 'reblog'
|
||||||
|
| 'status'
|
||||||
|
| 'mention'
|
||||||
|
| 'poll'
|
||||||
|
| 'update';
|
||||||
|
|
||||||
|
export type NotificationType =
|
||||||
|
| NotificationWithStatusType
|
||||||
|
| 'follow'
|
||||||
|
| 'follow_request'
|
||||||
|
| 'moderation_warning'
|
||||||
|
| 'severed_relationships'
|
||||||
|
| 'admin.sign_up'
|
||||||
|
| 'admin.report';
|
||||||
|
|
||||||
|
export interface BaseNotificationJSON {
|
||||||
|
id: string;
|
||||||
|
type: NotificationType;
|
||||||
|
created_at: string;
|
||||||
|
group_key: string;
|
||||||
|
account: ApiAccountJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseNotificationGroupJSON {
|
||||||
|
group_key: string;
|
||||||
|
notifications_count: number;
|
||||||
|
type: NotificationType;
|
||||||
|
sample_account_ids: string[];
|
||||||
|
latest_page_notification_at: string; // FIXME: This will only be present if the notification group is returned in a paginated list, not requested directly
|
||||||
|
most_recent_notification_id: string;
|
||||||
|
page_min_id?: string;
|
||||||
|
page_max_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||||
|
type: NotificationWithStatusType;
|
||||||
|
status_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationWithStatusJSON extends BaseNotificationJSON {
|
||||||
|
type: NotificationWithStatusType;
|
||||||
|
status: ApiStatusJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||||
|
type: 'admin.report';
|
||||||
|
report: ApiReportJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: 'admin.report';
|
||||||
|
report: ApiReportJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimpleNotificationTypes = 'follow' | 'follow_request' | 'admin.sign_up';
|
||||||
|
interface SimpleNotificationGroupJSON extends BaseNotificationGroupJSON {
|
||||||
|
type: SimpleNotificationTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SimpleNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: SimpleNotificationTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAccountWarningJSON {
|
||||||
|
id: string;
|
||||||
|
action: AccountWarningAction;
|
||||||
|
text: string;
|
||||||
|
status_ids: string[];
|
||||||
|
created_at: string;
|
||||||
|
target_account: ApiAccountJSON;
|
||||||
|
appeal: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModerationWarningNotificationGroupJSON
|
||||||
|
extends BaseNotificationGroupJSON {
|
||||||
|
type: 'moderation_warning';
|
||||||
|
moderation_warning: ApiAccountWarningJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModerationWarningNotificationJSON extends BaseNotificationJSON {
|
||||||
|
type: 'moderation_warning';
|
||||||
|
moderation_warning: ApiAccountWarningJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiAccountRelationshipSeveranceEventJSON {
|
||||||
|
id: string;
|
||||||
|
type: 'account_suspension' | 'domain_block' | 'user_domain_block';
|
||||||
|
purged: boolean;
|
||||||
|
target_name: string;
|
||||||
|
followers_count: number;
|
||||||
|
following_count: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountRelationshipSeveranceNotificationGroupJSON
|
||||||
|
extends BaseNotificationGroupJSON {
|
||||||
|
type: 'severed_relationships';
|
||||||
|
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountRelationshipSeveranceNotificationJSON
|
||||||
|
extends BaseNotificationJSON {
|
||||||
|
type: 'severed_relationships';
|
||||||
|
event: ApiAccountRelationshipSeveranceEventJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiNotificationJSON =
|
||||||
|
| SimpleNotificationJSON
|
||||||
|
| ReportNotificationJSON
|
||||||
|
| AccountRelationshipSeveranceNotificationJSON
|
||||||
|
| NotificationWithStatusJSON
|
||||||
|
| ModerationWarningNotificationJSON;
|
||||||
|
|
||||||
|
export type ApiNotificationGroupJSON =
|
||||||
|
| SimpleNotificationGroupJSON
|
||||||
|
| ReportNotificationGroupJSON
|
||||||
|
| AccountRelationshipSeveranceNotificationGroupJSON
|
||||||
|
| NotificationGroupWithStatusJSON
|
||||||
|
| ModerationWarningNotificationGroupJSON;
|
||||||
|
|
||||||
|
export interface ApiNotificationGroupsResultJSON {
|
||||||
|
accounts: ApiAccountJSON[];
|
||||||
|
statuses: ApiStatusJSON[];
|
||||||
|
notification_groups: ApiNotificationGroupJSON[];
|
||||||
|
}
|
||||||
16
app/javascript/flavours/glitch/api_types/reports.ts
Normal file
16
app/javascript/flavours/glitch/api_types/reports.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import type { ApiAccountJSON } from './accounts';
|
||||||
|
|
||||||
|
export type ReportCategory = 'other' | 'spam' | 'legal' | 'violation';
|
||||||
|
|
||||||
|
export interface ApiReportJSON {
|
||||||
|
id: string;
|
||||||
|
action_taken: unknown;
|
||||||
|
action_taken_at: unknown;
|
||||||
|
category: ReportCategory;
|
||||||
|
comment: string;
|
||||||
|
forwarded: boolean;
|
||||||
|
created_at: string;
|
||||||
|
status_ids: string[];
|
||||||
|
rule_ids: string[];
|
||||||
|
target_account: ApiAccountJSON;
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,8 @@ interface Props {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
inline?: boolean;
|
inline?: boolean;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
|
counter?: number | string;
|
||||||
|
counterBorderColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar: React.FC<Props> = ({
|
export const Avatar: React.FC<Props> = ({
|
||||||
|
|
@ -19,6 +21,8 @@ export const Avatar: React.FC<Props> = ({
|
||||||
size = 20,
|
size = 20,
|
||||||
inline = false,
|
inline = false,
|
||||||
style: styleFromParent,
|
style: styleFromParent,
|
||||||
|
counter,
|
||||||
|
counterBorderColor,
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
||||||
|
|
||||||
|
|
@ -43,7 +47,15 @@ export const Avatar: React.FC<Props> = ({
|
||||||
style={style}
|
style={style}
|
||||||
data-avatar-of={account && `@${account.get('acct')}`}
|
data-avatar-of={account && `@${account.get('acct')}`}
|
||||||
>
|
>
|
||||||
{src && <img src={src} alt={account?.get('acct')} />}
|
{src && <img src={src} alt='' />}
|
||||||
|
{counter && (
|
||||||
|
<div
|
||||||
|
className='account__avatar__counter'
|
||||||
|
style={{ borderColor: counterBorderColor }}
|
||||||
|
>
|
||||||
|
{counter}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { useCallback } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
||||||
|
import { unblockDomain } from 'flavours/glitch/actions/domain_blocks';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
import { IconButton } from './icon_button';
|
||||||
|
|
||||||
|
|
@ -13,17 +15,15 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
export const Domain: React.FC<{
|
||||||
domain: string;
|
domain: string;
|
||||||
onUnblockDomain: (domain: string) => void;
|
}> = ({ domain }) => {
|
||||||
}
|
|
||||||
|
|
||||||
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleDomainUnblock = useCallback(() => {
|
const handleDomainUnblock = useCallback(() => {
|
||||||
onUnblockDomain(domain);
|
dispatch(unblockDomain(domain));
|
||||||
}, [domain, onUnblockDomain]);
|
}, [dispatch, domain]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='domain'>
|
<div className='domain'>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ let id = 0;
|
||||||
class DropdownMenu extends PureComponent {
|
class DropdownMenu extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
items: PropTypes.array.isRequired,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
scrollable: PropTypes.bool,
|
scrollable: PropTypes.bool,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
|
|
@ -39,6 +39,7 @@ class DropdownMenu extends PureComponent {
|
||||||
if (this.node && !this.node.contains(e.target)) {
|
if (this.node && !this.node.contains(e.target)) {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -164,7 +165,7 @@ class Dropdown extends PureComponent {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
iconComponent: PropTypes.func,
|
iconComponent: PropTypes.func,
|
||||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
|
items: PropTypes.array.isRequired,
|
||||||
loading: PropTypes.bool,
|
loading: PropTypes.bool,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { useIdentity } from '@/flavours/glitch/identity_context';
|
import { useIdentity } from '@/flavours/glitch/identity_context';
|
||||||
import {
|
import {
|
||||||
fetchRelationships,
|
fetchRelationships,
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
|
||||||
} from 'flavours/glitch/actions/accounts';
|
} from 'flavours/glitch/actions/accounts';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { Button } from 'flavours/glitch/components/button';
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
|
@ -59,29 +58,14 @@ export const FollowButton: React.FC<{
|
||||||
|
|
||||||
if (accountId === me) {
|
if (accountId === me) {
|
||||||
return;
|
return;
|
||||||
} else if (relationship.following || relationship.requested) {
|
} else if (account && (relationship.following || relationship.requested)) {
|
||||||
dispatch(
|
dispatch(
|
||||||
openModal({
|
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: (
|
|
||||||
<FormattedMessage
|
|
||||||
id='confirmations.unfollow.message'
|
|
||||||
defaultMessage='Are you sure you want to unfollow {name}?'
|
|
||||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
confirm: intl.formatMessage(messages.unfollow),
|
|
||||||
onConfirm: () => {
|
|
||||||
dispatch(unfollowAccount(accountId));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(accountId));
|
dispatch(followAccount(accountId));
|
||||||
}
|
}
|
||||||
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
}, [dispatch, accountId, relationship, account, signedIn]);
|
||||||
|
|
||||||
let label;
|
let label;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@ const messages = defineMessages({
|
||||||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props<T> {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
maxId: string;
|
param: T;
|
||||||
onClick: (maxId: string) => void;
|
onClick: (params: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadGap: React.FC<Props> = ({ disabled, maxId, onClick }) => {
|
export const LoadGap = <T,>({ disabled, param, onClick }: Props<T>) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
onClick(maxId);
|
onClick(param);
|
||||||
}, [maxId, onClick]);
|
}, [param, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ type LocationState = MastodonLocationState | null | undefined;
|
||||||
|
|
||||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||||
|
|
||||||
const browserHistory = createBrowserHistory<LocationState>();
|
export const browserHistory = createBrowserHistory<LocationState>();
|
||||||
const originalReplace = browserHistory.replace.bind(browserHistory);
|
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||||
|
|
||||||
export function useAppHistory() {
|
export function useAppHistory() {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,8 @@ class Status extends ImmutablePureComponent {
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
cachedMediaWidth: PropTypes.number,
|
cachedMediaWidth: PropTypes.number,
|
||||||
scrollKey: PropTypes.string,
|
scrollKey: PropTypes.string,
|
||||||
|
skipPrepend: PropTypes.bool,
|
||||||
|
avatarSize: PropTypes.number,
|
||||||
deployPictureInPicture: PropTypes.func,
|
deployPictureInPicture: PropTypes.func,
|
||||||
settings: ImmutablePropTypes.map.isRequired,
|
settings: ImmutablePropTypes.map.isRequired,
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
pictureInPicture: ImmutablePropTypes.contains({
|
||||||
|
|
@ -445,7 +447,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleHotkeyReply = e => {
|
handleHotkeyReply = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onReply(this.props.status, this.props.history);
|
this.props.onReply(this.props.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyFavourite = (e) => {
|
handleHotkeyFavourite = (e) => {
|
||||||
|
|
@ -462,7 +464,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
handleHotkeyMention = e => {
|
handleHotkeyMention = e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.onMention(this.props.status.get('account'), this.props.history);
|
this.props.onMention(this.props.status.get('account'));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyOpen = () => {
|
handleHotkeyOpen = () => {
|
||||||
|
|
@ -523,12 +525,14 @@ class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
parseClick,
|
parseClick,
|
||||||
setCollapsed,
|
setCollapsed,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
intl,
|
|
||||||
status,
|
status,
|
||||||
account,
|
account,
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -538,13 +542,6 @@ class Status extends ImmutablePureComponent {
|
||||||
onOpenVideo,
|
onOpenVideo,
|
||||||
onOpenMedia,
|
onOpenMedia,
|
||||||
notification,
|
notification,
|
||||||
hidden,
|
|
||||||
unread,
|
|
||||||
featured,
|
|
||||||
pictureInPicture,
|
|
||||||
previousId,
|
|
||||||
nextInReplyToId,
|
|
||||||
rootId,
|
|
||||||
history,
|
history,
|
||||||
identity,
|
identity,
|
||||||
...other
|
...other
|
||||||
|
|
@ -594,8 +591,8 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||||
<div ref={this.handleRef} className='status focusable' tabIndex={0}>
|
<div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
|
||||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||||
<span>{status.get('content')}</span>
|
<span>{status.get('content')}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -615,8 +612,8 @@ class Status extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={minHandlers}>
|
<HotKeys handlers={minHandlers} tabIndex={unfocusable ? null : -1}>
|
||||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
|
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={unfocusable ? null : 0} ref={this.handleRef}>
|
||||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
||||||
{' '}
|
{' '}
|
||||||
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||||
|
|
@ -796,17 +793,17 @@ class Status extends ImmutablePureComponent {
|
||||||
contentMedia.push(hashtagBar);
|
contentMedia.push(hashtagBar);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||||
<div
|
<div
|
||||||
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
|
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
|
||||||
{...selectorAttribs}
|
{...selectorAttribs}
|
||||||
tabIndex={0}
|
tabIndex={unfocusable ? null : 0}
|
||||||
data-featured={featured ? 'true' : null}
|
data-featured={featured ? 'true' : null}
|
||||||
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
|
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
|
||||||
ref={this.handleRef}
|
ref={this.handleRef}
|
||||||
data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
|
data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
|
||||||
>
|
>
|
||||||
{prepend}
|
{!skipPrepend && prepend}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'has-background': isCollapsed && background })}
|
className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted, 'has-background': isCollapsed && background })}
|
||||||
|
|
@ -822,6 +819,7 @@ class Status extends ImmutablePureComponent {
|
||||||
friend={account}
|
friend={account}
|
||||||
collapsed={isCollapsed}
|
collapsed={isCollapsed}
|
||||||
parseClick={parseClick}
|
parseClick={parseClick}
|
||||||
|
avatarSize={avatarSize}
|
||||||
/>
|
/>
|
||||||
<StatusIcons
|
<StatusIcons
|
||||||
status={status}
|
status={status}
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
const { signedIn } = this.props.identity;
|
const { signedIn } = this.props.identity;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
this.props.onReply(this.props.status, this.props.history);
|
this.props.onReply(this.props.status);
|
||||||
} else {
|
} else {
|
||||||
this.props.onInteractionModal('reply', this.props.status);
|
this.props.onInteractionModal('reply', this.props.status);
|
||||||
}
|
}
|
||||||
|
|
@ -153,15 +153,15 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.props.history);
|
this.props.onDelete(this.props.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRedraftClick = () => {
|
handleRedraftClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.props.history, true);
|
this.props.onDelete(this.props.status, true);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEditClick = () => {
|
handleEditClick = () => {
|
||||||
this.props.onEdit(this.props.status, this.props.history);
|
this.props.onEdit(this.props.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePinClick = () => {
|
handlePinClick = () => {
|
||||||
|
|
@ -169,11 +169,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMentionClick = () => {
|
handleMentionClick = () => {
|
||||||
this.props.onMention(this.props.status.get('account'), this.props.history);
|
this.props.onMention(this.props.status.get('account'));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDirectClick = () => {
|
handleDirectClick = () => {
|
||||||
this.props.onDirect(this.props.status.get('account'), this.props.history);
|
this.props.onDirect(this.props.status.get('account'));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMuteClick = () => {
|
handleMuteClick = () => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default class StatusHeader extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
status: ImmutablePropTypes.map.isRequired,
|
||||||
friend: ImmutablePropTypes.map,
|
friend: ImmutablePropTypes.map,
|
||||||
|
avatarSize: PropTypes.number,
|
||||||
parseClick: PropTypes.func.isRequired,
|
parseClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -33,13 +34,14 @@ export default class StatusHeader extends PureComponent {
|
||||||
const {
|
const {
|
||||||
status,
|
status,
|
||||||
friend,
|
friend,
|
||||||
|
avatarSize,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
let statusAvatar;
|
let statusAvatar;
|
||||||
if (friend === undefined || friend === null) {
|
if (friend === undefined || friend === null) {
|
||||||
statusAvatar = <Avatar account={account} size={46} />;
|
statusAvatar = <Avatar account={account} size={avatarSize} />;
|
||||||
} else {
|
} else {
|
||||||
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
<LoadGap
|
<LoadGap
|
||||||
key={'gap:' + statusIds.get(index + 1)}
|
key={'gap:' + statusIds.get(index + 1)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
param={index > 0 ? statusIds.get(index - 1) : null}
|
||||||
onClick={onLoadMore}
|
onClick={onLoadMore}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export default class StatusPrepend extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.poll'
|
id='notification.poll'
|
||||||
defaultMessage='A poll you have voted in has ended'
|
defaultMessage='A poll you voted in has ended'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,20 @@
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
|
||||||
blockAccount,
|
blockAccount,
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
muteAccount,
|
muteAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { openModal } from '../actions/modal';
|
|
||||||
import { initMuteModal } from '../actions/mutes';
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import Account from '../components/account';
|
import Account from '../components/account';
|
||||||
import { makeGetAccount } from '../selectors';
|
import { makeGetAccount } from '../selectors';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
|
@ -29,18 +25,11 @@ const makeMapStateToProps = () => {
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
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),
|
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { blockDomain, unblockDomain } from '../actions/domain_blocks';
|
|
||||||
import { openModal } from '../actions/modal';
|
|
||||||
import { Domain } from '../components/domain';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const mapStateToProps = () => ({});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
|
||||||
onBlockDomain (domain) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.blockDomainConfirm),
|
|
||||||
onConfirm: () => dispatch(blockDomain(domain)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnblockDomain (domain) {
|
|
||||||
dispatch(unblockDomain(domain));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain));
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
import { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||||
|
|
@ -12,18 +10,15 @@ import {
|
||||||
initAddFilter,
|
initAddFilter,
|
||||||
} from 'flavours/glitch/actions/filters';
|
} from 'flavours/glitch/actions/filters';
|
||||||
import {
|
import {
|
||||||
reblog,
|
toggleReblog,
|
||||||
favourite,
|
toggleFavourite,
|
||||||
bookmark,
|
bookmark,
|
||||||
unreblog,
|
|
||||||
unfavourite,
|
|
||||||
unbookmark,
|
unbookmark,
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
addReaction,
|
addReaction,
|
||||||
removeReaction,
|
removeReaction,
|
||||||
} from 'flavours/glitch/actions/interactions';
|
} from 'flavours/glitch/actions/interactions';
|
||||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||||
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||||
|
|
@ -32,33 +27,17 @@ import {
|
||||||
muteStatus,
|
muteStatus,
|
||||||
unmuteStatus,
|
unmuteStatus,
|
||||||
deleteStatus,
|
deleteStatus,
|
||||||
hideStatus,
|
toggleStatusSpoilers,
|
||||||
revealStatus,
|
|
||||||
editStatus,
|
editStatus,
|
||||||
translateStatus,
|
translateStatus,
|
||||||
undoStatusTranslation,
|
undoStatusTranslation,
|
||||||
} from 'flavours/glitch/actions/statuses';
|
} from 'flavours/glitch/actions/statuses';
|
||||||
import Status from 'flavours/glitch/components/status';
|
import Status from 'flavours/glitch/components/status';
|
||||||
import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/initial_state';
|
import { deleteModal } from 'flavours/glitch/initial_state';
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
|
||||||
|
|
||||||
import { showAlertForError } from '../actions/alerts';
|
import { showAlertForError } from '../actions/alerts';
|
||||||
|
|
||||||
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?' },
|
|
||||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
|
||||||
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favorites and boosts will be lost, and replies to the original post will be orphaned.' },
|
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
editConfirm: { id: 'confirmations.edit.confirm', defaultMessage: 'Edit' },
|
|
||||||
editMessage: { id: 'confirmations.edit.message', defaultMessage: 'Editing now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' },
|
|
||||||
author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' },
|
|
||||||
matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' },
|
|
||||||
editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
const getPictureInPicture = makeGetPictureInPicture();
|
const getPictureInPicture = makeGetPictureInPicture();
|
||||||
|
|
@ -93,47 +72,22 @@ const makeMapStateToProps = () => {
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
const mapDispatchToProps = (dispatch, { contextType }) => ({
|
||||||
|
|
||||||
onReply (status, router) {
|
onReply (status) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
||||||
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
|
||||||
confirm: intl.formatMessage(messages.replyConfirm),
|
|
||||||
onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_before_clearing_draft'], false)),
|
|
||||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(replyCompose(status, router));
|
dispatch(replyCompose(status));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalReblog (status, privacy) {
|
|
||||||
if (status.get('reblogged')) {
|
|
||||||
dispatch(unreblog({ statusId: status.get('id') }));
|
|
||||||
} else {
|
|
||||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
dispatch((_, getState) => {
|
dispatch(toggleReblog(status.get('id'), e.shiftKey));
|
||||||
let state = getState();
|
|
||||||
if (state.getIn(['local_settings', 'confirm_boost_missing_media_description']) && status.get('media_attachments').some(item => !item.get('description')) && !status.get('reblogged')) {
|
|
||||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog, missingMediaDescription: true } }));
|
|
||||||
} else if (e.shiftKey || !boostModal) {
|
|
||||||
this.onModalReblog(status);
|
|
||||||
} else {
|
|
||||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onBookmark (status) {
|
onBookmark (status) {
|
||||||
|
|
@ -144,26 +98,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onModalFavourite (status) {
|
|
||||||
dispatch(favourite(status));
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status, e) {
|
onFavourite (status, e) {
|
||||||
if (status.get('favourited')) {
|
dispatch(toggleFavourite(status.get('id'), e.shiftKey));
|
||||||
dispatch(unfavourite(status));
|
|
||||||
} else {
|
|
||||||
if (e.shiftKey || !favouriteModal) {
|
|
||||||
this.onModalFavourite(status);
|
|
||||||
} else {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'FAVOURITE',
|
|
||||||
modalProps: {
|
|
||||||
status,
|
|
||||||
onFavourite: this.onModalFavourite,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onPin (status) {
|
onPin (status) {
|
||||||
|
|
@ -192,35 +128,21 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
onDelete (status, history, withRedraft = false) {
|
onDelete (status, withRedraft = false) {
|
||||||
if (!deleteModal) {
|
if (!deleteModal) {
|
||||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
dispatch(deleteStatus(status.get('id'), withRedraft));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
|
|
||||||
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
|
|
||||||
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onEdit (status, history) {
|
onEdit (status) {
|
||||||
dispatch((_, getState) => {
|
dispatch((_, getState) => {
|
||||||
let state = getState();
|
let state = getState();
|
||||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.editMessage),
|
|
||||||
confirm: intl.formatMessage(messages.editConfirm),
|
|
||||||
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(editStatus(status.get('id'), history));
|
dispatch(editStatus(status.get('id')));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -233,12 +155,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account));
|
||||||
},
|
},
|
||||||
|
|
||||||
onMention (account, router) {
|
onMention (account) {
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account));
|
||||||
},
|
},
|
||||||
|
|
||||||
onOpenMedia (statusId, media, index, lang) {
|
onOpenMedia (statusId, media, index, lang) {
|
||||||
|
|
@ -281,11 +203,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
onToggleHidden (status) {
|
onToggleHidden (status) {
|
||||||
if (status.get('hidden')) {
|
dispatch(toggleStatusSpoilers(status.get('id')));
|
||||||
dispatch(revealStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideStatus(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deployPictureInPicture (status, type, mediaProps) {
|
deployPictureInPicture (status, type, mediaProps) {
|
||||||
|
|
@ -309,4 +227,4 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default connect(makeMapStateToProps, mapDispatchToProps)(Status);
|
||||||
|
|
|
||||||
|
|
@ -316,8 +316,8 @@ function loaded() {
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
statusEl.dataset.spoiler === 'expanded'
|
statusEl.dataset.spoiler === 'expanded'
|
||||||
? localeData['status.show_less'] ?? 'Show less'
|
? (localeData['status.show_less'] ?? 'Show less')
|
||||||
: localeData['status.show_more'] ?? 'Show more';
|
: (localeData['status.show_more'] ?? 'Show more');
|
||||||
spoilerLink.textContent = new IntlMessageFormat(
|
spoilerLink.textContent = new IntlMessageFormat(
|
||||||
message,
|
message,
|
||||||
locale,
|
locale,
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
|
||||||
|
|
||||||
import ActionBar from '../../account/components/action_bar';
|
import ActionBar from '../../account/components/action_bar';
|
||||||
import InnerHeader from '../../account/components/header';
|
import InnerHeader from '../../account/components/header';
|
||||||
|
|
||||||
|
|
@ -36,7 +34,6 @@ class Header extends ImmutablePureComponent {
|
||||||
hideTabs: PropTypes.bool,
|
hideTabs: PropTypes.bool,
|
||||||
domain: PropTypes.string.isRequired,
|
domain: PropTypes.string.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
|
|
@ -48,11 +45,11 @@ class Header extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMention = () => {
|
handleMention = () => {
|
||||||
this.props.onMention(this.props.account, this.props.history);
|
this.props.onMention(this.props.account);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDirect = () => {
|
handleDirect = () => {
|
||||||
this.props.onDirect(this.props.account, this.props.history);
|
this.props.onDirect(this.props.account);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleReport = () => {
|
handleReport = () => {
|
||||||
|
|
@ -158,4 +155,4 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Header);
|
export default Header;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
pinAccount,
|
pinAccount,
|
||||||
|
|
@ -22,11 +21,6 @@ import { initReport } from '../../../actions/reports';
|
||||||
import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Block entire domain' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getAccount = makeGetAccount();
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
|
@ -39,18 +33,11 @@ const makeMapStateToProps = () => {
|
||||||
return mapStateToProps;
|
return mapStateToProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
onFollow (account) {
|
onFollow (account) {
|
||||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
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),
|
|
||||||
onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
|
|
@ -75,12 +62,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onMention (account, router) {
|
onMention (account) {
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account));
|
||||||
},
|
},
|
||||||
|
|
||||||
onDirect (account, router) {
|
onDirect (account) {
|
||||||
dispatch(directCompose(account, router));
|
dispatch(directCompose(account));
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblogToggle (account) {
|
onReblogToggle (account) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
|
|
@ -23,8 +22,6 @@ const messages = defineMessages({
|
||||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
|
||||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ActionBar = () => {
|
export const ActionBar = () => {
|
||||||
|
|
@ -32,16 +29,8 @@ export const ActionBar = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleLogoutClick = useCallback(() => {
|
const handleLogoutClick = useCallback(() => {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
||||||
modalType: 'CONFIRM',
|
}, [dispatch]);
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
|
||||||
closeWhenConfirm: false,
|
|
||||||
onConfirm: () => logOut(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}, [dispatch, intl]);
|
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import { length } from 'stringz';
|
import { length } from 'stringz';
|
||||||
|
|
||||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
|
||||||
|
|
||||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||||
import { Button } from '../../../components/button';
|
import { Button } from '../../../components/button';
|
||||||
|
|
@ -81,7 +79,6 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
singleColumn: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
lang: PropTypes.string,
|
lang: PropTypes.string,
|
||||||
maxChars: PropTypes.number,
|
maxChars: PropTypes.number,
|
||||||
...WithOptionalRouterPropTypes
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
@ -141,9 +138,9 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
// Submit unless there are media with missing descriptions
|
// Submit unless there are media with missing descriptions
|
||||||
if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) {
|
if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) {
|
||||||
const firstWithoutDescription = this.props.media.find(item => !item.get('description'));
|
const firstWithoutDescription = this.props.media.find(item => !item.get('description'));
|
||||||
this.props.onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overridePrivacy);
|
this.props.onMediaDescriptionConfirm(firstWithoutDescription.get('id'), overridePrivacy);
|
||||||
} else {
|
} else {
|
||||||
this.props.onSubmit(this.props.history || null, overridePrivacy);
|
this.props.onSubmit(overridePrivacy);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -351,4 +348,4 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withOptionalRouter(injectIntl(ComposeForm));
|
export default injectIntl(ComposeForm);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||||
|
import { EmbeddedStatusContent } from 'flavours/glitch/features/notifications_v2/components/embedded_status_content';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||||
|
|
@ -32,8 +33,6 @@ export const EditIndicator = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='edit-indicator'>
|
<div className='edit-indicator'>
|
||||||
<div className='edit-indicator__header'>
|
<div className='edit-indicator__header'>
|
||||||
|
|
@ -48,7 +47,12 @@ export const EditIndicator = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
|
<EmbeddedStatusContent
|
||||||
|
className='edit-indicator__content translate'
|
||||||
|
content={status.get('contentHtml')}
|
||||||
|
language={status.get('language')}
|
||||||
|
mentions={status.get('mentions')}
|
||||||
|
/>
|
||||||
|
|
||||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||||
<div className='edit-indicator__attachments'>
|
<div className='edit-indicator__attachments'>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||||
|
import { EmbeddedStatusContent } from 'flavours/glitch/features/notifications_v2/components/embedded_status_content';
|
||||||
|
|
||||||
export const ReplyIndicator = () => {
|
export const ReplyIndicator = () => {
|
||||||
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||||
|
|
@ -18,8 +19,6 @@ export const ReplyIndicator = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: status.get('contentHtml') };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='reply-indicator'>
|
<div className='reply-indicator'>
|
||||||
<div className='reply-indicator__line' />
|
<div className='reply-indicator__line' />
|
||||||
|
|
@ -33,7 +32,12 @@ export const ReplyIndicator = () => {
|
||||||
<DisplayName account={account} />
|
<DisplayName account={account} />
|
||||||
</Permalink>
|
</Permalink>
|
||||||
|
|
||||||
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
<EmbeddedStatusContent
|
||||||
|
className='reply-indicator__content translate'
|
||||||
|
content={status.get('contentHtml')}
|
||||||
|
language={status.get('language')}
|
||||||
|
mentions={status.get('mentions')}
|
||||||
|
/>
|
||||||
|
|
||||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||||
<div className='reply-indicator__attachments'>
|
<div className='reply-indicator__attachments'>
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(changeCompose(text));
|
dispatch(changeCompose(text));
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit (router, overridePrivacy = null) {
|
onSubmit (overridePrivacy = null) {
|
||||||
dispatch(submitCompose(router, overridePrivacy));
|
dispatch(submitCompose(overridePrivacy));
|
||||||
},
|
},
|
||||||
|
|
||||||
onClearSuggestions () {
|
onClearSuggestions () {
|
||||||
|
|
@ -110,14 +110,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||||
},
|
},
|
||||||
|
|
||||||
onMediaDescriptionConfirm (routerHistory, mediaId, overridePrivacy = null) {
|
onMediaDescriptionConfirm (mediaId, overridePrivacy = null) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'CONFIRM',
|
modalType: 'CONFIRM',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
message: intl.formatMessage(messages.missingDescriptionMessage),
|
message: intl.formatMessage(messages.missingDescriptionMessage),
|
||||||
confirm: intl.formatMessage(messages.missingDescriptionConfirm),
|
confirm: intl.formatMessage(messages.missingDescriptionConfirm),
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
dispatch(submitCompose(routerHistory, overridePrivacy));
|
dispatch(submitCompose(overridePrivacy));
|
||||||
},
|
},
|
||||||
secondary: intl.formatMessage(messages.missingDescriptionEdit),
|
secondary: intl.formatMessage(messages.missingDescriptionEdit),
|
||||||
onSecondary: () => dispatch(openModal({
|
onSecondary: () => dispatch(openModal({
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import glitchedElephant1 from 'flavours/glitch/images/mbstobon-ui-0.png';
|
import glitchedElephant1 from 'flavours/glitch/images/mbstobon-ui-0.png';
|
||||||
import glitchedElephant2 from 'flavours/glitch/images/mbstobon-ui-1.png';
|
import glitchedElephant2 from 'flavours/glitch/images/mbstobon-ui-1.png';
|
||||||
import glitchedElephant3 from 'flavours/glitch/images/mbstobon-ui-2.png';
|
import glitchedElephant3 from 'flavours/glitch/images/mbstobon-ui-2.png';
|
||||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
|
||||||
|
|
||||||
import elephantUIPlane from '../../../../images/elephant_ui_plane.svg';
|
import elephantUIPlane from '../../../../images/elephant_ui_plane.svg';
|
||||||
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
|
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
|
||||||
|
|
@ -45,8 +44,6 @@ const messages = defineMessages({
|
||||||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' },
|
||||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
|
||||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => ({
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
|
|
@ -88,20 +85,12 @@ class Compose extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLogoutClick = e => {
|
handleLogoutClick = e => {
|
||||||
const { dispatch, intl } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.logoutMessage),
|
|
||||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
|
||||||
closeWhenConfirm: false,
|
|
||||||
onConfirm: () => logOut(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||||
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
|
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'flavours/glitch/actions/statuses';
|
import { muteStatus, unmuteStatus, toggleStatusSpoilers } from 'flavours/glitch/actions/statuses';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
|
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
|
@ -37,8 +37,6 @@ const messages = defineMessages({
|
||||||
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
||||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
|
||||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getAccounts = createSelector(
|
const getAccounts = createSelector(
|
||||||
|
|
@ -121,19 +119,12 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||||
let state = getState();
|
let state = getState();
|
||||||
|
|
||||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status: lastStatus } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.replyMessage),
|
|
||||||
confirm: intl.formatMessage(messages.replyConfirm),
|
|
||||||
onConfirm: () => dispatch(replyCompose(lastStatus, history)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
} else {
|
} else {
|
||||||
dispatch(replyCompose(lastStatus, history));
|
dispatch(replyCompose(lastStatus));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [dispatch, lastStatus, history, intl]);
|
}, [dispatch, lastStatus]);
|
||||||
|
|
||||||
const handleDelete = useCallback(() => {
|
const handleDelete = useCallback(() => {
|
||||||
dispatch(deleteConversation(id));
|
dispatch(deleteConversation(id));
|
||||||
|
|
@ -156,11 +147,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||||
}, [dispatch, lastStatus]);
|
}, [dispatch, lastStatus]);
|
||||||
|
|
||||||
const handleShowMore = useCallback(() => {
|
const handleShowMore = useCallback(() => {
|
||||||
if (lastStatus.get('hidden')) {
|
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
|
||||||
dispatch(revealStatus(lastStatus.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(hideStatus(lastStatus.get('id')));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastStatus.get('spoiler_text')) {
|
if (lastStatus.get('spoiler_text')) {
|
||||||
setExpanded(!expanded);
|
setExpanded(!expanded);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import classNames from 'classnames';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
followAccount,
|
followAccount,
|
||||||
unfollowAccount,
|
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from 'flavours/glitch/actions/accounts';
|
} from 'flavours/glitch/actions/accounts';
|
||||||
|
|
@ -29,20 +28,12 @@ const messages = defineMessages({
|
||||||
id: 'account.cancel_follow_request',
|
id: 'account.cancel_follow_request',
|
||||||
defaultMessage: 'Withdraw follow request',
|
defaultMessage: 'Withdraw follow request',
|
||||||
},
|
},
|
||||||
cancelFollowRequestConfirm: {
|
|
||||||
id: 'confirmations.cancel_follow_request.confirm',
|
|
||||||
defaultMessage: 'Withdraw request',
|
|
||||||
},
|
|
||||||
requested: {
|
requested: {
|
||||||
id: 'account.requested',
|
id: 'account.requested',
|
||||||
defaultMessage: 'Awaiting approval. Click to cancel follow request',
|
defaultMessage: 'Awaiting approval. Click to cancel follow request',
|
||||||
},
|
},
|
||||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||||
unfollowConfirm: {
|
|
||||||
id: 'confirmations.unfollow.confirm',
|
|
||||||
defaultMessage: 'Unfollow',
|
|
||||||
},
|
|
||||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -89,48 +80,17 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
|
||||||
const handleFollow = useCallback(() => {
|
const handleFollow = useCallback(() => {
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
|
|
||||||
if (account.getIn(['relationship', 'following'])) {
|
if (
|
||||||
|
account.getIn(['relationship', 'following']) ||
|
||||||
|
account.getIn(['relationship', 'requested'])
|
||||||
|
) {
|
||||||
dispatch(
|
dispatch(
|
||||||
openModal({
|
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
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),
|
|
||||||
onConfirm: () => {
|
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
|
||||||
dispatch(
|
|
||||||
openModal({
|
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: (
|
|
||||||
<FormattedMessage
|
|
||||||
id='confirmations.cancel_follow_request.message'
|
|
||||||
defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
|
|
||||||
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
|
|
||||||
onConfirm: () => {
|
|
||||||
dispatch(unfollowAccount(account.get('id')));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
dispatch(followAccount(account.get('id')));
|
dispatch(followAccount(account.get('id')));
|
||||||
}
|
}
|
||||||
}, [account, dispatch, intl]);
|
}, [account, dispatch]);
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
const handleBlock = useCallback(() => {
|
||||||
if (account?.relationship?.blocking) {
|
if (account?.relationship?.blocking) {
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,15 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
|
import BlockIcon from '@/material-icons/400-24px/block-fill.svg?react';
|
||||||
|
import { Domain } from 'flavours/glitch/components/domain';
|
||||||
|
|
||||||
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
|
import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import DomainContainer from '../../containers/domain_container';
|
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
|
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|
@ -70,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
|
||||||
bindToDocument={!multiColumn}
|
bindToDocument={!multiColumn}
|
||||||
>
|
>
|
||||||
{domains.map(domain =>
|
{domains.map(domain =>
|
||||||
<DomainContainer key={domain} domain={domain} />,
|
<Domain key={domain} domain={domain} />,
|
||||||
)}
|
)}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns';
|
||||||
import { fetchList, deleteList, updateList } from 'flavours/glitch/actions/lists';
|
import { fetchList, updateList } from 'flavours/glitch/actions/lists';
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { connectListStream } from 'flavours/glitch/actions/streaming';
|
import { connectListStream } from 'flavours/glitch/actions/streaming';
|
||||||
import { expandListTimeline } from 'flavours/glitch/actions/timelines';
|
import { expandListTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
|
@ -29,8 +29,6 @@ import StatusListContainer from 'flavours/glitch/features/ui/containers/status_l
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
|
||||||
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
|
||||||
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
||||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
||||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
||||||
|
|
@ -125,25 +123,10 @@ class ListTimeline extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
const { dispatch, columnId, intl } = this.props;
|
const { dispatch, columnId } = this.props;
|
||||||
const { id } = this.props.params;
|
const { id } = this.props.params;
|
||||||
|
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.deleteMessage),
|
|
||||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
|
||||||
onConfirm: () => {
|
|
||||||
dispatch(deleteList(id));
|
|
||||||
|
|
||||||
if (columnId) {
|
|
||||||
dispatch(removeColumn(columnId));
|
|
||||||
} else {
|
|
||||||
this.props.history.push('/lists');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRepliesPolicyChange = ({ target }) => {
|
handleRepliesPolicyChange = ({ target }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
|
|
||||||
export const CheckboxWithLabel = ({ checked, disabled, children, onChange }) => {
|
|
||||||
const handleChange = useCallback(({ target }) => {
|
|
||||||
onChange(target.checked);
|
|
||||||
}, [onChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className='app-form__toggle'>
|
|
||||||
<div className='app-form__toggle__label'>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='app-form__toggle__toggle'>
|
|
||||||
<div>
|
|
||||||
<Toggle checked={checked} onChange={handleChange} disabled={disabled} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
CheckboxWithLabel.propTypes = {
|
|
||||||
checked: PropTypes.bool,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
children: PropTypes.children,
|
|
||||||
onChange: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CheckboxWithLabel: React.FC<PropsWithChildren<Props>> = ({
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ target }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(target.checked);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className='app-form__toggle'>
|
||||||
|
<div className='app-form__toggle__label'>{children}</div>
|
||||||
|
|
||||||
|
<div className='app-form__toggle__toggle'>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
checked={checked}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -8,10 +8,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
||||||
|
|
||||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
|
||||||
import ClearColumnButton from './clear_column_button';
|
import ClearColumnButton from './clear_column_button';
|
||||||
import GrantPermissionButton from './grant_permission_button';
|
import GrantPermissionButton from './grant_permission_button';
|
||||||
import PillBarButton from './pill_bar_button';
|
import PillBarButton from './pill_bar_button';
|
||||||
|
import { PolicyControls } from './policy_controls';
|
||||||
import SettingToggle from './setting_toggle';
|
import SettingToggle from './setting_toggle';
|
||||||
|
|
||||||
class ColumnSettings extends PureComponent {
|
class ColumnSettings extends PureComponent {
|
||||||
|
|
@ -25,34 +25,17 @@ class ColumnSettings extends PureComponent {
|
||||||
alertsEnabled: PropTypes.bool,
|
alertsEnabled: PropTypes.bool,
|
||||||
browserSupport: PropTypes.bool,
|
browserSupport: PropTypes.bool,
|
||||||
browserPermission: PropTypes.string,
|
browserPermission: PropTypes.string,
|
||||||
notificationPolicy: PropTypes.object.isRequired,
|
|
||||||
onChangePolicy: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onPushChange = (path, checked) => {
|
onPushChange = (path, checked) => {
|
||||||
this.props.onChange(['push', ...path], checked);
|
this.props.onChange(['push', ...path], checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFilterNotFollowing = checked => {
|
|
||||||
this.props.onChangePolicy('filter_not_following', checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFilterNotFollowers = checked => {
|
|
||||||
this.props.onChangePolicy('filter_not_followers', checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFilterNewAccounts = checked => {
|
|
||||||
this.props.onChangePolicy('filter_new_accounts', checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFilterPrivateMentions = checked => {
|
|
||||||
this.props.onChangePolicy('filter_private_mentions', checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
|
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
||||||
|
|
||||||
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||||
|
const groupingShowStr = <FormattedMessage id='notifications.column_settings.beta.grouping' defaultMessage='Group notifications' />;
|
||||||
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||||
|
|
@ -80,31 +63,7 @@ class ColumnSettings extends PureComponent {
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section>
|
<PolicyControls />
|
||||||
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
<CheckboxWithLabel checked={notificationPolicy.filter_not_following} onChange={this.handleFilterNotFollowing}>
|
|
||||||
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
|
|
||||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
|
|
||||||
</CheckboxWithLabel>
|
|
||||||
|
|
||||||
<CheckboxWithLabel checked={notificationPolicy.filter_not_followers} onChange={this.handleFilterNotFollowers}>
|
|
||||||
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
|
|
||||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
|
|
||||||
</CheckboxWithLabel>
|
|
||||||
|
|
||||||
<CheckboxWithLabel checked={notificationPolicy.filter_new_accounts} onChange={this.handleFilterNewAccounts}>
|
|
||||||
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
|
|
||||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
|
|
||||||
</CheckboxWithLabel>
|
|
||||||
|
|
||||||
<CheckboxWithLabel checked={notificationPolicy.filter_private_mentions} onChange={this.handleFilterPrivateMentions}>
|
|
||||||
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
|
|
||||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
|
|
||||||
</CheckboxWithLabel>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section role='group' aria-labelledby='notifications-filter-bar'>
|
<section role='group' aria-labelledby='notifications-filter-bar'>
|
||||||
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>
|
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>
|
||||||
|
|
@ -115,6 +74,16 @@ class ColumnSettings extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section role='group' aria-labelledby='notifications-beta'>
|
||||||
|
<h3 id='notifications-beta'>
|
||||||
|
<FormattedMessage id='notifications.column_settings.beta.category' defaultMessage='Experimental features' />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['groupingBeta']} onChange={onChange} label={groupingShowStr} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section role='group' aria-labelledby='notifications-unread-markers'>
|
<section role='group' aria-labelledby='notifications-unread-markers'>
|
||||||
<h3 id='notifications-unread-markers'>
|
<h3 id='notifications-unread-markers'>
|
||||||
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,62 @@
|
||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||||
import { fetchNotificationPolicy } from 'flavours/glitch/actions/notification_policies';
|
import { fetchNotificationPolicy } from 'flavours/glitch/actions/notification_policies';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { selectSettingsNotificationsMinimizeFilteredBanner } from 'flavours/glitch/selectors/settings';
|
||||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
import { toCappedNumber } from 'flavours/glitch/utils/numbers';
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
filteredNotifications: {
|
||||||
|
id: 'notification_requests.title',
|
||||||
|
defaultMessage: 'Filtered notifications',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FilteredNotificationsIconButton: React.FC<{
|
||||||
|
className?: string;
|
||||||
|
}> = ({ className }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
const policy = useAppSelector((state) => state.notificationPolicy);
|
||||||
|
const minimizeSetting = useAppSelector(
|
||||||
|
selectSettingsNotificationsMinimizeFilteredBanner,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
history.push('/notifications/requests');
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
if (policy === null || policy.summary.pending_notifications_count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!minimizeSetting) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
aria-label={intl.formatMessage(messages.filteredNotifications)}
|
||||||
|
title={intl.formatMessage(messages.filteredNotifications)}
|
||||||
|
onClick={handleClick}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<Icon id='filtered-notifications' icon={InventoryIcon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const FilteredNotificationsBanner: React.FC = () => {
|
export const FilteredNotificationsBanner: React.FC = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const policy = useAppSelector((state) => state.notificationPolicy);
|
const policy = useAppSelector((state) => state.notificationPolicy);
|
||||||
|
const minimizeSetting = useAppSelector(
|
||||||
|
selectSettingsNotificationsMinimizeFilteredBanner,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void dispatch(fetchNotificationPolicy());
|
void dispatch(fetchNotificationPolicy());
|
||||||
|
|
@ -30,12 +74,18 @@ export const FilteredNotificationsBanner: React.FC = () => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (minimizeSetting) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className='filtered-notifications-banner'
|
className='filtered-notifications-banner'
|
||||||
to='/notifications/requests'
|
to='/notifications/requests'
|
||||||
>
|
>
|
||||||
<Icon icon={InventoryIcon} id='filtered-notifications' />
|
<div className='notification-group__icon'>
|
||||||
|
<Icon icon={InventoryIcon} id='filtered-notifications' />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='filtered-notifications-banner__text'>
|
<div className='filtered-notifications-banner__text'>
|
||||||
<strong>
|
<strong>
|
||||||
|
|
@ -47,22 +97,11 @@ export const FilteredNotificationsBanner: React.FC = () => {
|
||||||
<span>
|
<span>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='filtered_notifications_banner.pending_requests'
|
id='filtered_notifications_banner.pending_requests'
|
||||||
defaultMessage='Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know'
|
defaultMessage='From {count, plural, =0 {no one} one {one person} other {# people}} you may know'
|
||||||
values={{ count: policy.summary.pending_requests_count }}
|
values={{ count: policy.summary.pending_requests_count }}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='filtered-notifications-banner__badge'>
|
|
||||||
<div className='filtered-notifications-banner__badge__badge'>
|
|
||||||
{toCappedNumber(policy.summary.pending_notifications_count)}
|
|
||||||
</div>
|
|
||||||
<FormattedMessage
|
|
||||||
id='filtered_notifications_banner.mentions'
|
|
||||||
defaultMessage='{count, plural, one {mention} other {mentions}}'
|
|
||||||
values={{ count: policy.summary.pending_notifications_count }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import GavelIcon from '@/material-icons/400-24px/gavel.svg?react';
|
import GavelIcon from '@/material-icons/400-24px/gavel.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import type { AccountWarningAction } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
// This needs to be kept in sync with app/models/account_warning.rb
|
// This needs to be kept in sync with app/models/account_warning.rb
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
@ -36,19 +39,18 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
action:
|
action: AccountWarningAction;
|
||||||
| 'none'
|
|
||||||
| 'disable'
|
|
||||||
| 'mark_statuses_as_sensitive'
|
|
||||||
| 'delete_statuses'
|
|
||||||
| 'sensitive'
|
|
||||||
| 'silence'
|
|
||||||
| 'suspend';
|
|
||||||
id: string;
|
id: string;
|
||||||
hidden: boolean;
|
hidden?: boolean;
|
||||||
|
unread?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
|
export const ModerationWarning: React.FC<Props> = ({
|
||||||
|
action,
|
||||||
|
id,
|
||||||
|
hidden,
|
||||||
|
unread,
|
||||||
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
|
|
@ -56,23 +58,32 @@ export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<div
|
||||||
href={`/disputes/strikes/${id}`}
|
role='button'
|
||||||
target='_blank'
|
className={classNames(
|
||||||
rel='noopener noreferrer'
|
'notification-group notification-group--link notification-group--moderation-warning focusable',
|
||||||
className='notification__moderation-warning'
|
{ 'notification-group--unread': unread },
|
||||||
|
)}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<Icon id='warning' icon={GavelIcon} />
|
<div className='notification-group__icon'>
|
||||||
|
<Icon id='warning' icon={GavelIcon} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className='notification__moderation-warning__content'>
|
<div className='notification-group__main'>
|
||||||
<p>{intl.formatMessage(messages[action])}</p>
|
<p>{intl.formatMessage(messages[action])}</p>
|
||||||
<span className='link-button'>
|
<a
|
||||||
|
href={`/disputes/strikes/${id}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='link-button'
|
||||||
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='notification.moderation-warning.learn_more'
|
id='notification.moderation-warning.learn_more'
|
||||||
defaultMessage='Learn more'
|
defaultMessage='Learn more'
|
||||||
/>
|
/>
|
||||||
</span>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ class Notification extends ImmutablePureComponent {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const { notification, onMention } = this.props;
|
const { notification, onMention } = this.props;
|
||||||
onMention(notification.get('account'), this.props.history);
|
onMention(notification.get('account'));
|
||||||
};
|
};
|
||||||
|
|
||||||
handleHotkeyFavourite = () => {
|
handleHotkeyFavourite = () => {
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||||
return (
|
return (
|
||||||
<div className='notification-request'>
|
<div className='notification-request'>
|
||||||
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
||||||
<Avatar account={account} size={36} />
|
<Avatar account={account} size={40} counter={toCappedNumber(notificationsCount)} />
|
||||||
|
|
||||||
<div className='notification-request__name'>
|
<div className='notification-request__name'>
|
||||||
<div className='notification-request__name__display-name'>
|
<div className='notification-request__name__display-name'>
|
||||||
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi>
|
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi>
|
||||||
<span className='filtered-notifications-banner__badge'>{toCappedNumber(notificationsCount)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span>@{account?.get('acct')}</span>
|
<span>@{account?.get('acct')}</span>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { updateNotificationsPolicy } from 'flavours/glitch/actions/notification_policies';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||||
|
|
||||||
|
export const PolicyControls: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const notificationPolicy = useAppSelector(
|
||||||
|
(state) => state.notificationPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterNotFollowing = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
void dispatch(
|
||||||
|
updateNotificationsPolicy({ filter_not_following: checked }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterNotFollowers = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
void dispatch(
|
||||||
|
updateNotificationsPolicy({ filter_not_followers: checked }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterNewAccounts = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
void dispatch(
|
||||||
|
updateNotificationsPolicy({ filter_new_accounts: checked }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFilterPrivateMentions = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
void dispatch(
|
||||||
|
updateNotificationsPolicy({ filter_private_mentions: checked }),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!notificationPolicy) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.title'
|
||||||
|
defaultMessage='Filter out notifications from…'
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<CheckboxWithLabel
|
||||||
|
checked={notificationPolicy.filter_not_following}
|
||||||
|
onChange={handleFilterNotFollowing}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_not_following_title'
|
||||||
|
defaultMessage="People you don't follow"
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_not_following_hint'
|
||||||
|
defaultMessage='Until you manually approve them'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
|
||||||
|
<CheckboxWithLabel
|
||||||
|
checked={notificationPolicy.filter_not_followers}
|
||||||
|
onChange={handleFilterNotFollowers}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_not_followers_title'
|
||||||
|
defaultMessage='People not following you'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_not_followers_hint'
|
||||||
|
defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}'
|
||||||
|
values={{ days: 3 }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
|
||||||
|
<CheckboxWithLabel
|
||||||
|
checked={notificationPolicy.filter_new_accounts}
|
||||||
|
onChange={handleFilterNewAccounts}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_new_accounts_title'
|
||||||
|
defaultMessage='New accounts'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_new_accounts.hint'
|
||||||
|
defaultMessage='Created within the past {days, plural, one {one day} other {# days}}'
|
||||||
|
values={{ days: 30 }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
|
||||||
|
<CheckboxWithLabel
|
||||||
|
checked={notificationPolicy.filter_private_mentions}
|
||||||
|
onChange={handleFilterPrivateMentions}
|
||||||
|
>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_private_mentions_title'
|
||||||
|
defaultMessage='Unsolicited private mentions'
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
<span className='hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='notifications.policy.filter_private_mentions_hint'
|
||||||
|
defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
|
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { domain } from 'flavours/glitch/initial_state';
|
import { domain } from 'flavours/glitch/initial_state';
|
||||||
|
|
@ -13,7 +15,7 @@ const messages = defineMessages({
|
||||||
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
user_domain_block: { id: 'notification.relationships_severance_event.user_domain_block', defaultMessage: 'You have blocked {target}, removing {followersCount} of your followers and {followingCount, plural, one {# account} other {# accounts}} you follow.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
|
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden, unread }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
|
|
@ -21,14 +23,14 @@ export const RelationshipsSeveranceEvent = ({ type, target, followingCount, foll
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
|
<div role='button' className={classNames('notification-group notification-group--link notification-group--relationships-severance-event focusable', { 'notification-group--unread': unread })} tabIndex='0'>
|
||||||
<Icon id='heart_broken' icon={HeartBrokenIcon} />
|
<div className='notification-group__icon'><Icon id='heart_broken' icon={HeartBrokenIcon} /></div>
|
||||||
|
|
||||||
<div className='notification__relationships-severance-event__content'>
|
<div className='notification-group__main'>
|
||||||
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
|
<p>{intl.formatMessage(messages[type], { from: <strong>{domain}</strong>, target: <strong>{target}</strong>, followingCount, followersCount })}</p>
|
||||||
<span className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></span>
|
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='link-button'><FormattedMessage id='notification.relationships_severance_event.learn_more' defaultMessage='Learn more' /></a>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -42,4 +44,5 @@ RelationshipsSeveranceEvent.propTypes = {
|
||||||
followersCount: PropTypes.number.isRequired,
|
followersCount: PropTypes.number.isRequired,
|
||||||
followingCount: PropTypes.number.isRequired,
|
followingCount: PropTypes.number.isRequired,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
unread: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,16 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
|
import { initializeNotifications } from 'flavours/glitch/actions/notifications_migration';
|
||||||
|
|
||||||
import { showAlert } from '../../../actions/alerts';
|
import { showAlert } from '../../../actions/alerts';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
|
||||||
import { updateNotificationsPolicy } from '../../../actions/notification_policies';
|
|
||||||
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
|
|
||||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import ColumnSettings from '../components/column_settings';
|
import ColumnSettings from '../components/column_settings';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
|
|
||||||
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
|
|
||||||
permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' },
|
permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -25,10 +24,9 @@ const mapStateToProps = state => ({
|
||||||
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||||
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||||
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||||
notificationPolicy: state.notificationPolicy,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
||||||
onChange (path, checked) {
|
onChange (path, checked) {
|
||||||
if (path[0] === 'push') {
|
if (path[0] === 'push') {
|
||||||
|
|
@ -58,32 +56,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
}
|
}
|
||||||
|
} else if(path[0] === 'groupingBeta') {
|
||||||
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
|
dispatch(initializeNotifications());
|
||||||
} else {
|
} else {
|
||||||
dispatch(changeSetting(['notifications', ...path], checked));
|
dispatch(changeSetting(['notifications', ...path], checked));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onClear () {
|
onClear () {
|
||||||
dispatch(openModal({
|
dispatch(openModal({ modalType: 'CONFIRM_CLEAR_NOTIFICATIONS' }));
|
||||||
modalType: 'CONFIRM',
|
|
||||||
modalProps: {
|
|
||||||
message: intl.formatMessage(messages.clearMessage),
|
|
||||||
confirm: intl.formatMessage(messages.clearConfirm),
|
|
||||||
onConfirm: () => dispatch(clearNotifications()),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRequestNotificationPermission () {
|
onRequestNotificationPermission () {
|
||||||
dispatch(requestBrowserPermission());
|
dispatch(requestBrowserPermission());
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangePolicy (param, checked) {
|
|
||||||
dispatch(updateNotificationsPolicy({
|
|
||||||
[param]: checked,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,9 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
import {
|
import {
|
||||||
reblog,
|
toggleReblog,
|
||||||
favourite,
|
toggleFavourite,
|
||||||
unreblog,
|
|
||||||
unfavourite,
|
|
||||||
} from '../../../actions/interactions';
|
} from '../../../actions/interactions';
|
||||||
import { openModal } from '../../../actions/modal';
|
|
||||||
import { boostModal } from '../../../initial_state';
|
|
||||||
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
||||||
import Notification from '../components/notification';
|
import Notification from '../components/notification';
|
||||||
|
|
||||||
|
|
@ -31,32 +27,16 @@ const makeMapStateToProps = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
onMention: (account, router) => {
|
onMention: (account) => {
|
||||||
dispatch(mentionCompose(account, router));
|
dispatch(mentionCompose(account));
|
||||||
},
|
|
||||||
|
|
||||||
onModalReblog (status, privacy) {
|
|
||||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onReblog (status, e) {
|
onReblog (status, e) {
|
||||||
if (status.get('reblogged')) {
|
dispatch(toggleReblog(status.get('id'), e.shiftKey));
|
||||||
dispatch(unreblog({ statusId: status.get('id') }));
|
|
||||||
} else {
|
|
||||||
if (e.shiftKey || !boostModal) {
|
|
||||||
this.onModalReblog(status);
|
|
||||||
} else {
|
|
||||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFavourite (status) {
|
onFavourite (status, e) {
|
||||||
if (status.get('favourited')) {
|
dispatch(toggleFavourite(status.get('id'), e.shiftKey));
|
||||||
dispatch(unfavourite(status));
|
|
||||||
} else {
|
|
||||||
dispatch(favourite(status));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,10 @@ import { LoadGap } from '../../components/load_gap';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
|
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
|
||||||
|
|
||||||
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
|
import {
|
||||||
|
FilteredNotificationsBanner,
|
||||||
|
FilteredNotificationsIconButton,
|
||||||
|
} from './components/filtered_notifications_banner';
|
||||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import FilterBarContainer from './containers/filter_bar_container';
|
import FilterBarContainer from './containers/filter_bar_container';
|
||||||
|
|
@ -237,7 +240,7 @@ class Notifications extends PureComponent {
|
||||||
<LoadGap
|
<LoadGap
|
||||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
param={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||||
onClick={this.handleLoadGap}
|
onClick={this.handleLoadGap}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -258,6 +261,13 @@ class Notifications extends PureComponent {
|
||||||
|
|
||||||
let scrollContainer;
|
let scrollContainer;
|
||||||
|
|
||||||
|
const prepend = (
|
||||||
|
<>
|
||||||
|
{needsNotificationPermission && <NotificationsPermissionBanner />}
|
||||||
|
<FilteredNotificationsBanner />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
scrollContainer = (
|
scrollContainer = (
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
|
|
@ -267,7 +277,7 @@ class Notifications extends PureComponent {
|
||||||
showLoading={isLoading && notifications.size === 0}
|
showLoading={isLoading && notifications.size === 0}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
numPending={numPending}
|
numPending={numPending}
|
||||||
prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
|
prepend={prepend}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
onLoadMore={this.handleLoadOlder}
|
onLoadMore={this.handleLoadOlder}
|
||||||
|
|
@ -283,7 +293,9 @@ class Notifications extends PureComponent {
|
||||||
scrollContainer = <NotSignedInIndicator />;
|
scrollContainer = <NotSignedInIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const extraButtons = [];
|
const extraButtons = [
|
||||||
|
<FilteredNotificationsIconButton key='filtered-notifications-icon' className='column-header__button' />,
|
||||||
|
];
|
||||||
|
|
||||||
if (canMarkAsRead) {
|
if (canMarkAsRead) {
|
||||||
extraButtons.push(
|
extraButtons.push(
|
||||||
|
|
@ -356,8 +368,6 @@ class Notifications extends PureComponent {
|
||||||
|
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
|
||||||
<FilteredNotificationsBanner />
|
|
||||||
|
|
||||||
{scrollContainer}
|
{scrollContainer}
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import { useSelector, useDispatch } from 'react-redux';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||||
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
|
||||||
import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
|
import { fetchNotificationRequest, fetchNotificationsForRequest, expandNotificationsForRequest, acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
|
|
@ -101,7 +101,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
||||||
showBackButton
|
showBackButton
|
||||||
extraButton={!removed && (
|
extraButton={!removed && (
|
||||||
<>
|
<>
|
||||||
<IconButton className='column-header__button' iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
<IconButton className='column-header__button' iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||||
<IconButton className='column-header__button' iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
<IconButton className='column-header__button' iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,52 @@ import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||||
import { fetchNotificationRequests, expandNotificationRequests } from 'flavours/glitch/actions/notifications';
|
import { fetchNotificationRequests, expandNotificationRequests } from 'flavours/glitch/actions/notifications';
|
||||||
|
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||||
import Column from 'flavours/glitch/components/column';
|
import Column from 'flavours/glitch/components/column';
|
||||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
|
||||||
import { NotificationRequest } from './components/notification_request';
|
import { NotificationRequest } from './components/notification_request';
|
||||||
|
import { PolicyControls } from './components/policy_controls';
|
||||||
|
import SettingToggle from './components/setting_toggle';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
||||||
|
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ColumnSettings = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const settings = useSelector((state) => state.settings.get('notifications'));
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(key, checked) => {
|
||||||
|
dispatch(changeSetting(['notifications', ...key], checked));
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='column-settings'>
|
||||||
|
<section>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle
|
||||||
|
prefix='notifications'
|
||||||
|
settings={settings}
|
||||||
|
settingPath={['minimizeFilteredBanner']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<FormattedMessage id='notification_requests.minimize_banner' defaultMessage='Minimize filtred notifications banner' />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<PolicyControls />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NotificationRequests = ({ multiColumn }) => {
|
export const NotificationRequests = ({ multiColumn }) => {
|
||||||
const columnRef = useRef();
|
const columnRef = useRef();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
@ -48,7 +84,9 @@ export const NotificationRequests = ({ multiColumn }) => {
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
showBackButton
|
showBackButton
|
||||||
/>
|
>
|
||||||
|
<ColumnSettings />
|
||||||
|
</ColumnHeader>
|
||||||
|
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
scrollKey='notification_requests'
|
scrollKey='notification_requests'
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { NOTIFICATIONS_GROUP_MAX_AVATARS } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
const AvatarWrapper: React.FC<{ accountId: string }> = ({ accountId }) => {
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
title={`@${account.acct}`}
|
||||||
|
data-hover-card-account={account.id}
|
||||||
|
>
|
||||||
|
<Avatar account={account} size={28} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AvatarGroup: React.FC<{ accountIds: string[] }> = ({
|
||||||
|
accountIds,
|
||||||
|
}) => (
|
||||||
|
<div className='notification-group__avatar-group'>
|
||||||
|
{accountIds.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS).map((accountId) => (
|
||||||
|
<AvatarWrapper key={accountId} accountId={accountId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { List as ImmutableList, RecordOf } from 'immutable';
|
||||||
|
|
||||||
|
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||||
|
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||||
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import type { Status } from 'flavours/glitch/models/status';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { EmbeddedStatusContent } from './embedded_status_content';
|
||||||
|
|
||||||
|
export type Mention = RecordOf<{ url: string; acct: string }>;
|
||||||
|
|
||||||
|
export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||||
|
statusId,
|
||||||
|
}) => {
|
||||||
|
const history = useHistory();
|
||||||
|
const clickCoordinatesRef = useRef<[number, number] | null>();
|
||||||
|
|
||||||
|
const status = useAppSelector(
|
||||||
|
(state) => state.statuses.get(statusId) as Status | undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
state.accounts.get(status?.get('account') as string),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseDown = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||||
|
({ clientX, clientY }) => {
|
||||||
|
clickCoordinatesRef.current = [clientX, clientY];
|
||||||
|
},
|
||||||
|
[clickCoordinatesRef],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||||
|
({ clientX, clientY, target, button }) => {
|
||||||
|
const [startX, startY] = clickCoordinatesRef.current ?? [0, 0];
|
||||||
|
const [deltaX, deltaY] = [
|
||||||
|
Math.abs(clientX - startX),
|
||||||
|
Math.abs(clientY - startY),
|
||||||
|
];
|
||||||
|
|
||||||
|
let element: HTMLDivElement | null = target as HTMLDivElement;
|
||||||
|
|
||||||
|
while (element) {
|
||||||
|
if (
|
||||||
|
element.localName === 'button' ||
|
||||||
|
element.localName === 'a' ||
|
||||||
|
element.localName === 'label'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element = element.parentNode as HTMLDivElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX + deltaY < 5 && button === 0 && account) {
|
||||||
|
history.push(`/@${account.acct}/${statusId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
clickCoordinatesRef.current = null;
|
||||||
|
},
|
||||||
|
[clickCoordinatesRef, statusId, account, history],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||||
|
({ currentTarget }) => {
|
||||||
|
const emojis =
|
||||||
|
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
const newSrc = emoji.getAttribute('data-original');
|
||||||
|
if (newSrc) emoji.src = newSrc;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
||||||
|
({ currentTarget }) => {
|
||||||
|
const emojis =
|
||||||
|
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
||||||
|
|
||||||
|
for (const emoji of emojis) {
|
||||||
|
const newSrc = emoji.getAttribute('data-static');
|
||||||
|
if (newSrc) emoji.src = newSrc;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign status attributes to variables with a forced type, as status is not yet properly typed
|
||||||
|
const contentHtml = status.get('contentHtml') as string;
|
||||||
|
const poll = status.get('poll');
|
||||||
|
const language = status.get('language') as string;
|
||||||
|
const mentions = status.get('mentions') as ImmutableList<Mention>;
|
||||||
|
const mediaAttachmentsSize = (
|
||||||
|
status.get('media_attachments') as ImmutableList<unknown>
|
||||||
|
).size;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='notification-group__embedded-status'
|
||||||
|
role='button'
|
||||||
|
tabIndex={-1}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<div className='notification-group__embedded-status__account'>
|
||||||
|
<Avatar account={account} size={16} />
|
||||||
|
<DisplayName account={account} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EmbeddedStatusContent
|
||||||
|
className='notification-group__embedded-status__content reply-indicator__content translate'
|
||||||
|
content={contentHtml}
|
||||||
|
language={language}
|
||||||
|
mentions={mentions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(poll || mediaAttachmentsSize > 0) && (
|
||||||
|
<div className='notification-group__embedded-status__attachments reply-indicator__attachments'>
|
||||||
|
{!!poll && (
|
||||||
|
<>
|
||||||
|
<Icon icon={BarChart4BarsIcon} id='bar-chart-4-bars' />
|
||||||
|
<FormattedMessage
|
||||||
|
id='reply_indicator.poll'
|
||||||
|
defaultMessage='Poll'
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{mediaAttachmentsSize > 0 && (
|
||||||
|
<>
|
||||||
|
<Icon icon={PhotoLibraryIcon} id='photo-library' />
|
||||||
|
<FormattedMessage
|
||||||
|
id='reply_indicator.attachments'
|
||||||
|
defaultMessage='{count, plural, one {# attachment} other {# attachments}}'
|
||||||
|
values={{ count: mediaAttachmentsSize }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { List } from 'immutable';
|
||||||
|
|
||||||
|
import type { History } from 'history';
|
||||||
|
|
||||||
|
import type { Mention } from './embedded_status';
|
||||||
|
|
||||||
|
const handleMentionClick = (
|
||||||
|
history: History,
|
||||||
|
mention: Mention,
|
||||||
|
e: MouseEvent,
|
||||||
|
) => {
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
history.push(`/@${mention.get('acct')}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHashtagClick = (
|
||||||
|
history: History,
|
||||||
|
hashtag: string,
|
||||||
|
e: MouseEvent,
|
||||||
|
) => {
|
||||||
|
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
history.push(`/tags/${hashtag.replace(/^#/, '')}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbeddedStatusContent: React.FC<{
|
||||||
|
content: string;
|
||||||
|
mentions: List<Mention>;
|
||||||
|
language: string;
|
||||||
|
className?: string;
|
||||||
|
}> = ({ content, mentions, language, className }) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleContentRef = useCallback(
|
||||||
|
(node: HTMLDivElement | null) => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = node.querySelectorAll<HTMLAnchorElement>('a');
|
||||||
|
|
||||||
|
for (const link of links) {
|
||||||
|
if (link.classList.contains('status-link')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
link.classList.add('status-link');
|
||||||
|
|
||||||
|
const mention = mentions.find((item) => link.href === item.get('url'));
|
||||||
|
|
||||||
|
if (mention) {
|
||||||
|
link.addEventListener(
|
||||||
|
'click',
|
||||||
|
handleMentionClick.bind(null, history, mention),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
link.setAttribute('title', `@${mention.get('acct')}`);
|
||||||
|
link.setAttribute('href', `/@${mention.get('acct')}`);
|
||||||
|
} else if (
|
||||||
|
link.textContent?.[0] === '#' ||
|
||||||
|
link.previousSibling?.textContent?.endsWith('#')
|
||||||
|
) {
|
||||||
|
link.addEventListener(
|
||||||
|
'click',
|
||||||
|
handleHashtagClick.bind(null, history, link.text),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`);
|
||||||
|
} else {
|
||||||
|
link.setAttribute('title', link.href);
|
||||||
|
link.classList.add('unhandled-link');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mentions, history],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={className}
|
||||||
|
ref={handleContentRef}
|
||||||
|
lang={language}
|
||||||
|
dangerouslySetInnerHTML={{ __html: content }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
export const NamesList: React.FC<{
|
||||||
|
accountIds: string[];
|
||||||
|
total: number;
|
||||||
|
seeMoreHref?: string;
|
||||||
|
}> = ({ accountIds, total, seeMoreHref }) => {
|
||||||
|
const lastAccountId = accountIds[0] ?? '0';
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(lastAccountId));
|
||||||
|
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
const displayedName = (
|
||||||
|
<Link
|
||||||
|
to={`/@${account.acct}`}
|
||||||
|
title={`@${account.acct}`}
|
||||||
|
data-hover-card-account={account.id}
|
||||||
|
>
|
||||||
|
<bdi dangerouslySetInnerHTML={{ __html: account.display_name_html }} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (total === 1) {
|
||||||
|
return displayedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seeMoreHref)
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='name_and_others_with_link'
|
||||||
|
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a>'
|
||||||
|
values={{
|
||||||
|
name: displayedName,
|
||||||
|
count: total - 1,
|
||||||
|
a: (chunks) => <Link to={seeMoreHref}>{chunks}</Link>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='name_and_others'
|
||||||
|
defaultMessage='{name} and {count, plural, one {# other} other {# others}}'
|
||||||
|
values={{ name: displayedName, count: total - 1 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import FlagIcon from '@/material-icons/400-24px/flag-fill.svg?react';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||||
|
import type { NotificationGroupAdminReport } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
// This needs to be kept in sync with app/models/report.rb
|
||||||
|
const messages = defineMessages({
|
||||||
|
other: {
|
||||||
|
id: 'report_notification.categories.other_sentence',
|
||||||
|
defaultMessage: 'other',
|
||||||
|
},
|
||||||
|
spam: {
|
||||||
|
id: 'report_notification.categories.spam_sentence',
|
||||||
|
defaultMessage: 'spam',
|
||||||
|
},
|
||||||
|
legal: {
|
||||||
|
id: 'report_notification.categories.legal_sentence',
|
||||||
|
defaultMessage: 'illegal content',
|
||||||
|
},
|
||||||
|
violation: {
|
||||||
|
id: 'report_notification.categories.violation_sentence',
|
||||||
|
defaultMessage: 'rule violation',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NotificationAdminReport: React.FC<{
|
||||||
|
notification: NotificationGroupAdminReport;
|
||||||
|
unread?: boolean;
|
||||||
|
}> = ({ notification, notification: { report }, unread }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const targetAccount = useAppSelector((state) =>
|
||||||
|
state.accounts.get(report.targetAccountId),
|
||||||
|
);
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
state.accounts.get(notification.sampleAccountIds[0] ?? '0'),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!account || !targetAccount) return null;
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
name: (
|
||||||
|
<bdi
|
||||||
|
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
target: (
|
||||||
|
<bdi
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: targetAccount.get('display_name_html'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
category: intl.formatMessage(messages[report.category]),
|
||||||
|
count: report.status_ids.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (report.status_ids.length > 0) {
|
||||||
|
if (report.category === 'other') {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.report_account_other'
|
||||||
|
defaultMessage='{name} reported {count, plural, one {one post} other {# posts}} from {target}'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.report_account'
|
||||||
|
defaultMessage='{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (report.category === 'other') {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.report_statuses_other'
|
||||||
|
defaultMessage='{name} reported {target}'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.report_statuses'
|
||||||
|
defaultMessage='{name} reported {target} for {category}'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/admin/reports/${report.id}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className={classNames(
|
||||||
|
'notification-group notification-group--link notification-group--admin-report focusable',
|
||||||
|
{ 'notification-group--unread': unread },
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className='notification-group__icon'>
|
||||||
|
<Icon id='flag' icon={FlagIcon} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification-group__main'>
|
||||||
|
<div className='notification-group__main__header'>
|
||||||
|
<div className='notification-group__main__header__label'>
|
||||||
|
{message}
|
||||||
|
<RelativeTimestamp timestamp={report.created_at} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{report.comment.length > 0 && (
|
||||||
|
<div className='notification-group__embedded-status__content'>
|
||||||
|
“{report.comment}”
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
|
import type { NotificationGroupAdminSignUp } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.admin.sign_up'
|
||||||
|
defaultMessage='{name} signed up'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationAdminSignUp: React.FC<{
|
||||||
|
notification: NotificationGroupAdminSignUp;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => (
|
||||||
|
<NotificationGroupWithStatus
|
||||||
|
type='admin-sign-up'
|
||||||
|
icon={PersonAddIcon}
|
||||||
|
iconId='person-add'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
timestamp={notification.latest_page_notification_at}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
labelRenderer={labelRenderer}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||||
|
import type { NotificationGroupFavourite } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.favourite'
|
||||||
|
defaultMessage='{name} favorited your status'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationFavourite: React.FC<{
|
||||||
|
notification: NotificationGroupFavourite;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => {
|
||||||
|
const { statusId } = notification;
|
||||||
|
const statusAccount = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.accounts.get(state.statuses.getIn([statusId, 'account']) as string)
|
||||||
|
?.acct,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationGroupWithStatus
|
||||||
|
type='favourite'
|
||||||
|
icon={StarIcon}
|
||||||
|
iconId='star'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
statusId={notification.statusId}
|
||||||
|
timestamp={notification.latest_page_notification_at}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
labelRenderer={labelRenderer}
|
||||||
|
labelSeeMoreHref={
|
||||||
|
statusAccount ? `/@${statusAccount}/${statusId}/favourites` : undefined
|
||||||
|
}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
|
import type { NotificationGroupFollow } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow'
|
||||||
|
defaultMessage='{name} followed you'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationFollow: React.FC<{
|
||||||
|
notification: NotificationGroupFollow;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => (
|
||||||
|
<NotificationGroupWithStatus
|
||||||
|
type='follow'
|
||||||
|
icon={PersonAddIcon}
|
||||||
|
iconId='person-add'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
timestamp={notification.latest_page_notification_at}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
labelRenderer={labelRenderer}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
|
import {
|
||||||
|
authorizeFollowRequest,
|
||||||
|
rejectFollowRequest,
|
||||||
|
} from 'flavours/glitch/actions/accounts';
|
||||||
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import type { NotificationGroupFollowRequest } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||||
|
reject: { id: 'follow_request.reject', defaultMessage: 'Reject' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.follow_request'
|
||||||
|
defaultMessage='{name} has requested to follow you'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationFollowRequest: React.FC<{
|
||||||
|
notification: NotificationGroupFollowRequest;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onAuthorize = useCallback(() => {
|
||||||
|
dispatch(authorizeFollowRequest(notification.sampleAccountIds[0]));
|
||||||
|
}, [dispatch, notification.sampleAccountIds]);
|
||||||
|
|
||||||
|
const onReject = useCallback(() => {
|
||||||
|
dispatch(rejectFollowRequest(notification.sampleAccountIds[0]));
|
||||||
|
}, [dispatch, notification.sampleAccountIds]);
|
||||||
|
|
||||||
|
const actions = (
|
||||||
|
<div className='notification-group__actions'>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.reject)}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onReject}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.authorize)}
|
||||||
|
icon='check'
|
||||||
|
iconComponent={CheckIcon}
|
||||||
|
onClick={onAuthorize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationGroupWithStatus
|
||||||
|
type='follow-request'
|
||||||
|
icon={PersonAddIcon}
|
||||||
|
iconId='person-add'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
timestamp={notification.latest_page_notification_at}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
labelRenderer={labelRenderer}
|
||||||
|
actions={actions}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import { navigateToProfile } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { mentionComposeById } from 'flavours/glitch/actions/compose';
|
||||||
|
import type { NotificationGroup as NotificationGroupModel } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { NotificationAdminReport } from './notification_admin_report';
|
||||||
|
import { NotificationAdminSignUp } from './notification_admin_sign_up';
|
||||||
|
import { NotificationFavourite } from './notification_favourite';
|
||||||
|
import { NotificationFollow } from './notification_follow';
|
||||||
|
import { NotificationFollowRequest } from './notification_follow_request';
|
||||||
|
import { NotificationMention } from './notification_mention';
|
||||||
|
import { NotificationModerationWarning } from './notification_moderation_warning';
|
||||||
|
import { NotificationPoll } from './notification_poll';
|
||||||
|
import { NotificationReaction } from './notification_reaction';
|
||||||
|
import { NotificationReblog } from './notification_reblog';
|
||||||
|
import { NotificationSeveredRelationships } from './notification_severed_relationships';
|
||||||
|
import { NotificationStatus } from './notification_status';
|
||||||
|
import { NotificationUpdate } from './notification_update';
|
||||||
|
|
||||||
|
export const NotificationGroup: React.FC<{
|
||||||
|
notificationGroupId: NotificationGroupModel['group_key'];
|
||||||
|
unread: boolean;
|
||||||
|
onMoveUp: (groupId: string) => void;
|
||||||
|
onMoveDown: (groupId: string) => void;
|
||||||
|
}> = ({ notificationGroupId, unread, onMoveUp, onMoveDown }) => {
|
||||||
|
const notificationGroup = useAppSelector((state) =>
|
||||||
|
state.notificationGroups.groups.find(
|
||||||
|
(item) => item.type !== 'gap' && item.group_key === notificationGroupId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const accountId =
|
||||||
|
notificationGroup?.type === 'gap'
|
||||||
|
? undefined
|
||||||
|
: notificationGroup?.sampleAccountIds[0];
|
||||||
|
|
||||||
|
const handlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
moveUp: () => {
|
||||||
|
onMoveUp(notificationGroupId);
|
||||||
|
},
|
||||||
|
|
||||||
|
moveDown: () => {
|
||||||
|
onMoveDown(notificationGroupId);
|
||||||
|
},
|
||||||
|
|
||||||
|
openProfile: () => {
|
||||||
|
if (accountId) dispatch(navigateToProfile(accountId));
|
||||||
|
},
|
||||||
|
|
||||||
|
mention: () => {
|
||||||
|
if (accountId) dispatch(mentionComposeById(accountId));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[dispatch, notificationGroupId, accountId, onMoveUp, onMoveDown],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!notificationGroup || notificationGroup.type === 'gap') return null;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
|
||||||
|
switch (notificationGroup.type) {
|
||||||
|
case 'reblog':
|
||||||
|
content = (
|
||||||
|
<NotificationReblog unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'favourite':
|
||||||
|
content = (
|
||||||
|
<NotificationFavourite
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'reaction':
|
||||||
|
content = (
|
||||||
|
<NotificationReaction
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'severed_relationships':
|
||||||
|
content = (
|
||||||
|
<NotificationSeveredRelationships
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'mention':
|
||||||
|
content = (
|
||||||
|
<NotificationMention unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'follow':
|
||||||
|
content = (
|
||||||
|
<NotificationFollow unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'follow_request':
|
||||||
|
content = (
|
||||||
|
<NotificationFollowRequest
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'poll':
|
||||||
|
content = (
|
||||||
|
<NotificationPoll unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'status':
|
||||||
|
content = (
|
||||||
|
<NotificationStatus unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
content = (
|
||||||
|
<NotificationUpdate unread={unread} notification={notificationGroup} />
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'admin.sign_up':
|
||||||
|
content = (
|
||||||
|
<NotificationAdminSignUp
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'admin.report':
|
||||||
|
content = (
|
||||||
|
<NotificationAdminReport
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'moderation_warning':
|
||||||
|
content = (
|
||||||
|
<NotificationModerationWarning
|
||||||
|
unread={unread}
|
||||||
|
notification={notificationGroup}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HotKeys handlers={handlers}>{content}</HotKeys>;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
|
||||||
|
import { replyComposeById } from 'flavours/glitch/actions/compose';
|
||||||
|
import { navigateToStatus } from 'flavours/glitch/actions/statuses';
|
||||||
|
import type { IconProp } from 'flavours/glitch/components/icon';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import { AvatarGroup } from './avatar_group';
|
||||||
|
import { EmbeddedStatus } from './embedded_status';
|
||||||
|
import { NamesList } from './names_list';
|
||||||
|
|
||||||
|
export type LabelRenderer = (
|
||||||
|
values: Record<string, React.ReactNode>,
|
||||||
|
) => JSX.Element;
|
||||||
|
|
||||||
|
export const NotificationGroupWithStatus: React.FC<{
|
||||||
|
icon: IconProp;
|
||||||
|
iconId: string;
|
||||||
|
statusId?: string;
|
||||||
|
actions?: JSX.Element;
|
||||||
|
count: number;
|
||||||
|
accountIds: string[];
|
||||||
|
timestamp: string;
|
||||||
|
labelRenderer: LabelRenderer;
|
||||||
|
labelSeeMoreHref?: string;
|
||||||
|
type: string;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({
|
||||||
|
icon,
|
||||||
|
iconId,
|
||||||
|
timestamp,
|
||||||
|
accountIds,
|
||||||
|
actions,
|
||||||
|
count,
|
||||||
|
statusId,
|
||||||
|
labelRenderer,
|
||||||
|
labelSeeMoreHref,
|
||||||
|
type,
|
||||||
|
unread,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const label = useMemo(
|
||||||
|
() =>
|
||||||
|
labelRenderer({
|
||||||
|
name: (
|
||||||
|
<NamesList
|
||||||
|
accountIds={accountIds}
|
||||||
|
total={count}
|
||||||
|
seeMoreHref={labelSeeMoreHref}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
[labelRenderer, accountIds, count, labelSeeMoreHref],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlers = useMemo(
|
||||||
|
() => ({
|
||||||
|
open: () => {
|
||||||
|
dispatch(navigateToStatus(statusId));
|
||||||
|
},
|
||||||
|
|
||||||
|
reply: () => {
|
||||||
|
dispatch(replyComposeById(statusId));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[dispatch, statusId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HotKeys handlers={handlers}>
|
||||||
|
<div
|
||||||
|
role='button'
|
||||||
|
className={classNames(
|
||||||
|
`notification-group focusable notification-group--${type}`,
|
||||||
|
{ 'notification-group--unread': unread },
|
||||||
|
)}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<div className='notification-group__icon'>
|
||||||
|
<Icon icon={icon} id={iconId} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification-group__main'>
|
||||||
|
<div className='notification-group__main__header'>
|
||||||
|
<div className='notification-group__main__header__wrapper'>
|
||||||
|
<AvatarGroup accountIds={accountIds} />
|
||||||
|
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification-group__main__header__label'>
|
||||||
|
{label}
|
||||||
|
{timestamp && <RelativeTimestamp timestamp={timestamp} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{statusId && (
|
||||||
|
<div className='notification-group__main__status'>
|
||||||
|
<EmbeddedStatus statusId={statusId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</HotKeys>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
|
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
|
||||||
|
import type { StatusVisibility } from 'flavours/glitch/api_types/statuses';
|
||||||
|
import type { NotificationGroupMention } from 'flavours/glitch/models/notification_group';
|
||||||
|
import { useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationWithStatus } from './notification_with_status';
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.mention'
|
||||||
|
defaultMessage='{name} mentioned you'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const privateMentionLabelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.private_mention'
|
||||||
|
defaultMessage='{name} privately mentioned you'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationMention: React.FC<{
|
||||||
|
notification: NotificationGroupMention;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => {
|
||||||
|
const statusVisibility = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.statuses.getIn([
|
||||||
|
notification.statusId,
|
||||||
|
'visibility',
|
||||||
|
]) as StatusVisibility,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationWithStatus
|
||||||
|
type='mention'
|
||||||
|
icon={statusVisibility === 'direct' ? AlternateEmailIcon : ReplyIcon}
|
||||||
|
iconId='reply'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
statusId={notification.statusId}
|
||||||
|
labelRenderer={
|
||||||
|
statusVisibility === 'direct'
|
||||||
|
? privateMentionLabelRenderer
|
||||||
|
: labelRenderer
|
||||||
|
}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { ModerationWarning } from 'flavours/glitch/features/notifications/components/moderation_warning';
|
||||||
|
import type { NotificationGroupModerationWarning } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
export const NotificationModerationWarning: React.FC<{
|
||||||
|
notification: NotificationGroupModerationWarning;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification: { moderationWarning }, unread }) => (
|
||||||
|
<ModerationWarning
|
||||||
|
action={moderationWarning.action}
|
||||||
|
id={moderationWarning.id}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import BarChart4BarsIcon from '@/material-icons/400-20px/bar_chart_4_bars.svg?react';
|
||||||
|
import { me } from 'flavours/glitch/initial_state';
|
||||||
|
import type { NotificationGroupPoll } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
import { NotificationWithStatus } from './notification_with_status';
|
||||||
|
|
||||||
|
const labelRendererOther = () => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.poll'
|
||||||
|
defaultMessage='A poll you voted in has ended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelRendererOwn = () => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.own_poll'
|
||||||
|
defaultMessage='Your poll has ended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationPoll: React.FC<{
|
||||||
|
notification: NotificationGroupPoll;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => (
|
||||||
|
<NotificationWithStatus
|
||||||
|
type='poll'
|
||||||
|
icon={BarChart4BarsIcon}
|
||||||
|
iconId='bar-chart-4-bars'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
statusId={notification.statusId}
|
||||||
|
labelRenderer={
|
||||||
|
notification.sampleAccountIds[0] === me
|
||||||
|
? labelRendererOwn
|
||||||
|
: labelRendererOther
|
||||||
|
}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import MoodIcon from '@/material-icons/400-24px/mood.svg?react';
|
||||||
|
import type { NotificationGroupReaction } from 'flavours/glitch/models/notification_group';
|
||||||
|
|
||||||
|
import type { LabelRenderer } from './notification_group_with_status';
|
||||||
|
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||||
|
|
||||||
|
const labelRenderer: LabelRenderer = (values) => (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.reaction'
|
||||||
|
defaultMessage='{name} reacted to your status'
|
||||||
|
values={values}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const NotificationReaction: React.FC<{
|
||||||
|
notification: NotificationGroupReaction;
|
||||||
|
unread: boolean;
|
||||||
|
}> = ({ notification, unread }) => {
|
||||||
|
return (
|
||||||
|
<NotificationGroupWithStatus
|
||||||
|
type='reaction'
|
||||||
|
icon={MoodIcon}
|
||||||
|
iconId='react'
|
||||||
|
accountIds={notification.sampleAccountIds}
|
||||||
|
statusId={notification.statusId}
|
||||||
|
timestamp={notification.latest_page_notification_at}
|
||||||
|
count={notification.notifications_count}
|
||||||
|
labelRenderer={labelRenderer}
|
||||||
|
unread={unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue