mirror of
https://iceshrimp.dev/blueb/Chuckya-fe-standalone.git
synced 2026-01-10 21:13:15 -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}",
|
||||
"postCreateCommand": "bin/setup",
|
||||
"postCreateCommand": "COREPACK_ENABLE_DOWNLOAD_PROMPT=0 bin/setup",
|
||||
"waitFor": "postCreateCommand",
|
||||
|
||||
"customizations": {
|
||||
|
|
|
|||
1
.github/workflows/crowdin-upload.yml
vendored
1
.github/workflows/crowdin-upload.yml
vendored
|
|
@ -19,6 +19,7 @@ on:
|
|||
jobs:
|
||||
upload-translations:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'mastodon/mastodon'
|
||||
|
||||
steps:
|
||||
- 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).
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# 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-navigation', '~> 4.4'
|
||||
gem 'stoplight', '~> 4.1'
|
||||
gem 'strong_migrations', '1.8.0'
|
||||
gem 'strong_migrations'
|
||||
gem 'tty-prompt', '~> 0.23', require: false
|
||||
gem 'twitter-text', '~> 3.1.0'
|
||||
gem 'tzinfo-data', '~> 1.2023'
|
||||
|
|
@ -100,7 +100,7 @@ gem 'json-ld'
|
|||
gem 'json-ld-preloaded', '~> 3.2'
|
||||
gem 'rdf-normalize', '~> 0.5'
|
||||
|
||||
gem 'opentelemetry-api', '~> 1.2.5'
|
||||
gem 'opentelemetry-api', '~> 1.3.0'
|
||||
|
||||
group :opentelemetry do
|
||||
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)
|
||||
faraday (>= 1, < 3)
|
||||
multi_json
|
||||
email_spec (2.2.2)
|
||||
email_spec (2.3.0)
|
||||
htmlentities (~> 4.3.3)
|
||||
launchy (~> 2.1)
|
||||
launchy (>= 2.1, < 4.0)
|
||||
mail (~> 2.7)
|
||||
erubi (1.13.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
excon (0.110.0)
|
||||
fabrication (2.31.0)
|
||||
faker (3.4.1)
|
||||
faker (3.4.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
|
|
@ -289,7 +289,7 @@ GEM
|
|||
ruby-progressbar (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (3.25.3)
|
||||
google-protobuf (3.25.4)
|
||||
googleapis-common-protos-types (1.14.0)
|
||||
google-protobuf (~> 3.18)
|
||||
haml (6.3.0)
|
||||
|
|
@ -357,17 +357,18 @@ GEM
|
|||
aes_key_wrap
|
||||
bindata
|
||||
httpclient
|
||||
json-ld (3.3.1)
|
||||
json-ld (3.3.2)
|
||||
htmlentities (~> 4.3)
|
||||
json-canonicalization (~> 1.0)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
multi_json (~> 1.15)
|
||||
rack (>= 2.2, < 4)
|
||||
rdf (~> 3.3)
|
||||
rexml (~> 3.2)
|
||||
json-ld-preloaded (3.3.0)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (4.3.0)
|
||||
json-schema (4.3.1)
|
||||
addressable (>= 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
|
|
@ -440,7 +441,7 @@ GEM
|
|||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.12)
|
||||
net-imap (0.4.14)
|
||||
date
|
||||
net-protocol
|
||||
net-ldap (0.19.0)
|
||||
|
|
@ -451,7 +452,7 @@ GEM
|
|||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.6)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nsa (0.3.0)
|
||||
|
|
@ -492,10 +493,10 @@ GEM
|
|||
openssl (3.2.0)
|
||||
openssl-signature_algorithm (1.3.0)
|
||||
openssl (> 2.0)
|
||||
opentelemetry-api (1.2.5)
|
||||
opentelemetry-api (1.3.0)
|
||||
opentelemetry-common (0.20.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-exporter-otlp (0.28.0)
|
||||
opentelemetry-exporter-otlp (0.28.1)
|
||||
google-protobuf (>= 3.18)
|
||||
googleapis-common-protos-types (~> 1.3)
|
||||
opentelemetry-api (~> 1.1)
|
||||
|
|
@ -512,14 +513,14 @@ GEM
|
|||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (~> 0.21)
|
||||
opentelemetry-instrumentation-action_view (0.7.0)
|
||||
opentelemetry-instrumentation-action_view (0.7.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.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-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-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_record (0.7.2)
|
||||
|
|
@ -531,32 +532,32 @@ GEM
|
|||
opentelemetry-instrumentation-base (0.22.3)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-registry (~> 0.1)
|
||||
opentelemetry-instrumentation-concurrent_ruby (0.21.3)
|
||||
opentelemetry-instrumentation-concurrent_ruby (0.21.4)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-excon (0.22.3)
|
||||
opentelemetry-instrumentation-excon (0.22.4)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-faraday (0.24.5)
|
||||
opentelemetry-instrumentation-faraday (0.24.6)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-http (0.23.3)
|
||||
opentelemetry-instrumentation-http (0.23.4)
|
||||
opentelemetry-api (~> 1.0)
|
||||
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-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-net_http (0.22.6)
|
||||
opentelemetry-instrumentation-net_http (0.22.7)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-pg (0.27.3)
|
||||
opentelemetry-instrumentation-pg (0.27.4)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-helpers-sql-obfuscation
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (0.24.5)
|
||||
opentelemetry-instrumentation-rack (0.24.6)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rails (0.31.0)
|
||||
opentelemetry-instrumentation-rails (0.31.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||
|
|
@ -565,20 +566,20 @@ GEM
|
|||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-redis (0.25.6)
|
||||
opentelemetry-instrumentation-redis (0.25.7)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-sidekiq (0.25.6)
|
||||
opentelemetry-instrumentation-sidekiq (0.25.7)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-registry (0.3.1)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-sdk (1.4.1)
|
||||
opentelemetry-sdk (1.5.0)
|
||||
opentelemetry-api (~> 1.1)
|
||||
opentelemetry-common (~> 0.20)
|
||||
opentelemetry-registry (~> 0.2)
|
||||
opentelemetry-semantic_conventions
|
||||
opentelemetry-semantic_conventions (1.10.0)
|
||||
opentelemetry-semantic_conventions (1.10.1)
|
||||
opentelemetry-api (~> 1.0)
|
||||
orm_adapter (0.5.0)
|
||||
ox (2.14.18)
|
||||
|
|
@ -589,7 +590,7 @@ GEM
|
|||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.6)
|
||||
pg (1.5.7)
|
||||
pghero (3.6.0)
|
||||
activerecord (>= 6.1)
|
||||
premailer (1.23.0)
|
||||
|
|
@ -607,13 +608,13 @@ GEM
|
|||
railties (>= 7.0.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (6.0.0)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.2)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
|
|
@ -675,8 +676,9 @@ GEM
|
|||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdf (3.3.1)
|
||||
rdf (3.3.2)
|
||||
bcp47_spec (~> 0.2)
|
||||
bigdecimal (~> 3.1, >= 3.1.5)
|
||||
link_header (~> 0.0, >= 0.0.8)
|
||||
rdf-normalize (0.7.0)
|
||||
rdf (~> 3.3)
|
||||
|
|
@ -696,7 +698,7 @@ GEM
|
|||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
rexml (3.3.1)
|
||||
rexml (3.3.4)
|
||||
strscan
|
||||
rotp (6.3.0)
|
||||
rouge (4.2.1)
|
||||
|
|
@ -733,7 +735,7 @@ GEM
|
|||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 8)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.65.0)
|
||||
rubocop (1.65.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
|
|
@ -766,21 +768,22 @@ GEM
|
|||
ruby-saml (1.16.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
rexml
|
||||
ruby-vips (2.2.1)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
rufus-scheduler (3.9.1)
|
||||
fugit (~> 1.1, >= 1.1.6)
|
||||
safety_net_attestation (0.4.0)
|
||||
jwt (~> 2.0)
|
||||
sanitize (6.1.1)
|
||||
sanitize (6.1.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
scenic (1.8.0)
|
||||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
selenium-webdriver (4.22.0)
|
||||
selenium-webdriver (4.23.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
|
@ -820,8 +823,8 @@ GEM
|
|||
stoplight (4.1.0)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.1)
|
||||
strong_migrations (1.8.0)
|
||||
activerecord (>= 5.2)
|
||||
strong_migrations (2.0.0)
|
||||
activerecord (>= 6.1)
|
||||
strscan (3.1.0)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
|
|
@ -893,7 +896,7 @@ GEM
|
|||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webrick (1.8.1)
|
||||
websocket (1.2.10)
|
||||
websocket (1.2.11)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
|
@ -982,7 +985,7 @@ DEPENDENCIES
|
|||
omniauth-rails_csrf_protection (~> 1.0)
|
||||
omniauth-saml (~> 2.0)
|
||||
omniauth_openid_connect (~> 0.6.1)
|
||||
opentelemetry-api (~> 1.2.5)
|
||||
opentelemetry-api (~> 1.3.0)
|
||||
opentelemetry-exporter-otlp (~> 0.28.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.7.1)
|
||||
opentelemetry-instrumentation-active_model_serializers (~> 0.20.1)
|
||||
|
|
@ -1045,7 +1048,7 @@ DEPENDENCIES
|
|||
simplecov-lcov (~> 0.8)
|
||||
stackprof
|
||||
stoplight (~> 4.1)
|
||||
strong_migrations (= 1.8.0)
|
||||
strong_migrations
|
||||
test-prof
|
||||
thor (~> 1.2)
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ module Admin
|
|||
def show
|
||||
authorize :instance, :show?
|
||||
@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
|
||||
|
||||
def destroy
|
||||
|
|
|
|||
|
|
@ -2,7 +2,15 @@
|
|||
|
||||
module Admin
|
||||
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
|
||||
authorize @tag, :show?
|
||||
|
|
@ -31,5 +39,13 @@ module Admin
|
|||
def tag_params
|
||||
params.require(:tag).permit(:name, :display_name, :trendable, :usable, :listable)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -30,10 +30,10 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
protected
|
||||
|
||||
def limit_param(default_limit)
|
||||
def limit_param(default_limit, max_limit = nil)
|
||||
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
|
||||
|
||||
def params_slice(*keys)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
|||
include AccountableConcern
|
||||
|
||||
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:write', :'admin:write:domain_allows' }, except: [:index, :show]
|
||||
|
|
@ -47,18 +48,13 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def set_domain_allow
|
||||
@domain_allow = DomainAllow.find(params[:id])
|
||||
end
|
||||
|
||||
def filtered_domain_allows
|
||||
# TODO: no filtering yet
|
||||
DomainAllow.all
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_admin_domain_allows_url(pagination_params(max_id: pagination_max_id)) if records_continue?
|
||||
end
|
||||
|
|
@ -72,7 +68,7 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
|||
end
|
||||
|
||||
def records_continue?
|
||||
@domain_allows.size == limit_param(LIMIT)
|
||||
@domain_allows.size == limit_param(LIMIT, MAX_LIMIT)
|
||||
end
|
||||
|
||||
def resource_params
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||
include AccountableConcern
|
||||
|
||||
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:write', :'admin:write:domain_blocks' }, except: [:index, :show]
|
||||
|
|
@ -59,18 +60,13 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def set_domain_block
|
||||
@domain_block = DomainBlock.find(params[:id])
|
||||
end
|
||||
|
||||
def filtered_domain_blocks
|
||||
# TODO: no filtering yet
|
||||
DomainBlock.all
|
||||
end
|
||||
|
||||
def domain_block_params
|
||||
params.permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
|
||||
end
|
||||
|
|
@ -88,7 +84,7 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
|||
end
|
||||
|
||||
def records_continue?
|
||||
@domain_blocks.size == limit_param(LIMIT)
|
||||
@domain_blocks.size == limit_param(LIMIT, MAX_LIMIT)
|
||||
end
|
||||
|
||||
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 :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
|
||||
|
||||
|
|
@ -32,6 +33,16 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
|||
render_empty
|
||||
end
|
||||
|
||||
def accept_bulk
|
||||
@requests.each { |request| AcceptNotificationRequestService.new.call(request) }
|
||||
render_empty
|
||||
end
|
||||
|
||||
def dismiss_bulk
|
||||
@requests.each(&:destroy!)
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_requests
|
||||
|
|
@ -53,6 +64,10 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
|||
@request = NotificationRequest.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def set_requests
|
||||
@requests = NotificationRequest.where(account: current_account, id: Array(params[:id]).uniq.map(&:to_i))
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
||||
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
|
||||
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
|
||||
|
||||
def index
|
||||
with_read_replica do
|
||||
|
|
@ -17,6 +19,14 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
render json: @notifications, each_serializer: REST::NotificationSerializer, relationships: @relationships
|
||||
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
|
||||
@notification = current_account.notifications.without_suspended.find(params[:id])
|
||||
render json: @notification, serializer: REST::NotificationSerializer
|
||||
|
|
@ -63,6 +73,10 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
)
|
||||
end
|
||||
|
||||
def notification_marker
|
||||
current_user.markers.find_by(timeline: 'notifications')
|
||||
end
|
||||
|
||||
def target_statuses_from_notifications
|
||||
@notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class Api::V1::ReportsController < Api::BaseController
|
|||
@report = ReportService.new.call(
|
||||
current_account,
|
||||
reported_account,
|
||||
report_params
|
||||
report_params.merge(application: doorkeeper_token.application)
|
||||
)
|
||||
|
||||
render json: @report, serializer: REST::ReportSerializer
|
||||
|
|
|
|||
|
|
@ -7,20 +7,49 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
|||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
DEFAULT_NOTIFICATIONS_LIMIT = 40
|
||||
DEFAULT_NOTIFICATIONS_COUNT_LIMIT = 100
|
||||
MAX_NOTIFICATIONS_COUNT_LIMIT = 1_000
|
||||
|
||||
def index
|
||||
with_read_replica do
|
||||
@notifications = load_notifications
|
||||
@group_metadata = load_group_metadata
|
||||
@grouped_notifications = load_grouped_notifications
|
||||
@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
|
||||
|
||||
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
|
||||
|
||||
def show
|
||||
@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
|
||||
|
||||
def clear
|
||||
|
|
@ -36,25 +65,35 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_notifications
|
||||
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
|
||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_notifications') do
|
||||
notifications = browserable_account_notifications.includes(from_account: [:account_stat, :user]).to_a_grouped_paginated_by_id(
|
||||
limit_param(DEFAULT_NOTIFICATIONS_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
|
||||
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
|
||||
preload_collection(target_statuses, Status)
|
||||
Notification.preload_cache_collection_target_statuses(notifications) do |target_statuses|
|
||||
preload_collection(target_statuses, Status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def load_group_metadata
|
||||
return {} if @notifications.empty?
|
||||
|
||||
browserable_account_notifications
|
||||
.where(group_key: @notifications.filter_map(&:group_key))
|
||||
.where(id: (@notifications.last.id)..(@notifications.first.id))
|
||||
.group(:group_key)
|
||||
.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 }] }
|
||||
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_group_metadata') do
|
||||
browserable_account_notifications
|
||||
.where(group_key: @notifications.filter_map(&:group_key))
|
||||
.where(id: (@notifications.last.id)..(@notifications.first.id))
|
||||
.group(:group_key)
|
||||
.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
|
||||
|
||||
def browserable_account_notifications
|
||||
|
|
@ -65,6 +104,10 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
|||
)
|
||||
end
|
||||
|
||||
def notification_marker
|
||||
current_user.markers.find_by(timeline: 'notifications')
|
||||
end
|
||||
|
||||
def target_statuses_from_notifications
|
||||
@notifications.filter_map(&:target_status)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ class ApplicationController < ActionController::Base
|
|||
helper_method :current_theme
|
||||
helper_method :single_user_mode?
|
||||
helper_method :use_seamless_external_login?
|
||||
helper_method :omniauth_only?
|
||||
helper_method :sso_account_settings
|
||||
helper_method :limited_federation_mode?
|
||||
helper_method :body_class_string
|
||||
|
|
@ -140,10 +139,6 @@ class ApplicationController < ActionController::Base
|
|||
Devise.pam_authentication || Devise.ldap_authentication
|
||||
end
|
||||
|
||||
def omniauth_only?
|
||||
ENV['OMNIAUTH_ONLY'] == 'true'
|
||||
end
|
||||
|
||||
def sso_account_settings
|
||||
ENV.fetch('SSO_ACCOUNT_SETTINGS', nil)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
|
||||
layout 'auth'
|
||||
|
||||
before_action :set_body_classes
|
||||
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||
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?
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
def after_resending_confirmation_instructions_path_for(_resource_name)
|
||||
if user_signed_in?
|
||||
if current_user.confirmed? && current_user.approved?
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
class Auth::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :check_self_destruct!
|
||||
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
||||
before_action :set_body_classes
|
||||
|
||||
layout 'auth'
|
||||
|
||||
|
|
@ -24,10 +23,6 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
redirect_to new_password_path(resource_name)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
def reset_password_token_is_valid?
|
||||
resource_class.with_reset_password_token(params[:reset_password_token]).present?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
private
|
||||
|
||||
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
|
||||
|
||||
def set_invite
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
include Auth::TwoFactorAuthenticationConcern
|
||||
|
||||
before_action :set_body_classes
|
||||
|
||||
content_security_policy only: :new do |p|
|
||||
p.form_action(false)
|
||||
end
|
||||
|
|
@ -103,10 +101,6 @@ class Auth::SessionsController < Devise::SessionsController
|
|||
|
||||
private
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
def home_paths(resource)
|
||||
paths = [about_path, '/explore']
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ class Auth::SetupController < ApplicationController
|
|||
|
||||
before_action :authenticate_user!
|
||||
before_action :require_unconfirmed_or_pending!
|
||||
before_action :set_body_classes
|
||||
before_action :set_user
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
|
@ -35,10 +34,6 @@ class Auth::SetupController < ApplicationController
|
|||
@user = current_user
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ module Auth::TwoFactorAuthenticationConcern
|
|||
def prompt_for_two_factor(user)
|
||||
register_attempt_in_session(user)
|
||||
|
||||
@body_classes = 'lighter'
|
||||
@webauthn_enabled = user.webauthn_enabled?
|
||||
@scheme_type = if user.webauthn_enabled? && user_params[:otp_attempt].blank?
|
||||
'webauthn'
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ module ChallengableConcern
|
|||
end
|
||||
|
||||
def render_challenge
|
||||
@body_classes = 'lighter'
|
||||
render 'auth/challenges/new', layout: 'auth'
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ class MailSubscriptionsController < ApplicationController
|
|||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :set_body_classes
|
||||
before_action :set_user
|
||||
before_action :set_type
|
||||
|
||||
|
|
@ -25,10 +24,6 @@ class MailSubscriptionsController < ApplicationController
|
|||
not_found unless @user
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'lighter'
|
||||
end
|
||||
|
||||
def set_type
|
||||
@type = email_type_from_param
|
||||
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
|
||||
|
||||
if theme == 'system'
|
||||
stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') +
|
||||
stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||
''.html_safe.tap do |tags|
|
||||
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
|
||||
stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous'
|
||||
end
|
||||
|
|
@ -16,8 +18,10 @@ module ThemeHelper
|
|||
_, theme = flavour_and_skin
|
||||
|
||||
if theme == 'system'
|
||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') +
|
||||
tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)')
|
||||
''.html_safe.tap do |tags|
|
||||
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
|
||||
tag.meta name: 'theme-color', content: theme_color_for(theme)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -316,8 +316,8 @@ function loaded() {
|
|||
|
||||
const message =
|
||||
statusEl.dataset.spoiler === 'expanded'
|
||||
? localeData['status.show_less'] ?? 'Show less'
|
||||
: localeData['status.show_more'] ?? 'Show more';
|
||||
? (localeData['status.show_less'] ?? 'Show less')
|
||||
: (localeData['status.show_more'] ?? 'Show more');
|
||||
spoilerLink.textContent = new IntlMessageFormat(
|
||||
message,
|
||||
locale,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { browserHistory } from 'flavours/glitch/components/router';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
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) {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchPinnedAccountsSuggestionsRequest());
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import { createAction } from '@reduxjs/toolkit';
|
|||
|
||||
import type { LayoutType } from '../is_mobile';
|
||||
|
||||
export const focusApp = createAction('APP_FOCUS');
|
||||
export const unfocusApp = createAction('APP_UNFOCUS');
|
||||
|
||||
interface ChangeLayoutPayload {
|
||||
layout: LayoutType;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import axios from 'axios';
|
|||
import { throttle } from 'lodash';
|
||||
|
||||
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 { tagHistory } from 'flavours/glitch/settings';
|
||||
import { recoverHashtags } from 'flavours/glitch/utils/hashtag';
|
||||
|
|
@ -94,9 +95,9 @@ const messages = defineMessages({
|
|||
saved: { id: 'compose.saved.body', defaultMessage: 'Post saved.' },
|
||||
});
|
||||
|
||||
export const ensureComposeIsVisible = (getState, routerHistory) => {
|
||||
export const ensureComposeIsVisible = (getState) => {
|
||||
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) => {
|
||||
const prependCWRe = getState().getIn(['local_settings', 'prepend_cw_re']);
|
||||
dispatch({
|
||||
|
|
@ -126,7 +127,19 @@ export function replyCompose(status, routerHistory) {
|
|||
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({
|
||||
type: COMPOSE_FOCUS,
|
||||
defaultText,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
|
||||
export function mentionCompose(account, routerHistory) {
|
||||
export function mentionCompose(account) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: COMPOSE_MENTION,
|
||||
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) => {
|
||||
dispatch({
|
||||
type: COMPOSE_DIRECT,
|
||||
account: account,
|
||||
});
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
};
|
||||
}
|
||||
|
||||
export function submitCompose(routerHistory, overridePrivacy = null) {
|
||||
export function submitCompose(overridePrivacy = null) {
|
||||
return function (dispatch, getState) {
|
||||
let status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
|
|
@ -230,11 +249,10 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
|||
'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']),
|
||||
},
|
||||
}).then(function (response) {
|
||||
if (routerHistory
|
||||
&& (routerHistory.location.pathname === '/publish' || routerHistory.location.pathname === '/statuses/new')
|
||||
if ((browserHistory.location.pathname === '/publish' || browserHistory.location.pathname === '/statuses/new')
|
||||
&& window.history.state
|
||||
&& !getState().getIn(['compose', 'advanced_options', 'threaded_mode'])) {
|
||||
routerHistory.goBack();
|
||||
browserHistory.goBack();
|
||||
}
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
|
|
@ -272,7 +290,7 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
|||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { boostModal, favouriteModal } from 'flavours/glitch/initial_state';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
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_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) => {
|
||||
const status = getState().get('statuses').get(statusId);
|
||||
let alreadyAdded = false;
|
||||
|
|
|
|||
|
|
@ -75,9 +75,17 @@ interface MarkerParam {
|
|||
}
|
||||
|
||||
function getLastNotificationId(state: RootState): string | undefined {
|
||||
// @ts-expect-error state.notifications is not yet typed
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
|
||||
return state.getIn(['notifications', 'lastReadId']);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
const enableBeta = state.settings.getIn(
|
||||
['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) => {
|
||||
|
|
|
|||
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 {
|
||||
apiGetNotificationPolicy,
|
||||
apiUpdateNotificationsPolicy,
|
||||
|
|
@ -14,3 +16,7 @@ export const updateNotificationsPolicy = createDataLoadingThunk(
|
|||
'notificationPolicy/update',
|
||||
(policy: Partial<NotificationPolicy>) => apiUpdateNotificationsPolicy(policy),
|
||||
);
|
||||
|
||||
export const decreasePendingNotificationsCount = createAction<number>(
|
||||
'notificationPolicy/decreasePendingNotificationCount',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
importFetchedStatuses,
|
||||
} from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import { decreasePendingNotificationsCount } from './notification_policies';
|
||||
import { notificationsUpdate } from "./notifications_typed";
|
||||
import { register as registerPushNotifications } from './push_notifications';
|
||||
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_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||
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 = () => ({
|
||||
type: NOTIFICATIONS_LOAD_PENDING,
|
||||
});
|
||||
|
|
@ -187,7 +193,7 @@ const noOp = () => {};
|
|||
|
||||
let expandNotificationsController = new AbortController();
|
||||
|
||||
export function expandNotifications({ maxId, forceLoad } = {}, done = noOp) {
|
||||
export function expandNotifications({ maxId, forceLoad = false } = {}, done = noOp) {
|
||||
return (dispatch, getState) => {
|
||||
const activeFilter = getState().getIn(['settings', 'notifications', 'quickFilter', 'active']);
|
||||
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) {
|
||||
return {
|
||||
type: NOTIFICATIONS_SCROLL_TOP,
|
||||
|
|
@ -533,11 +529,13 @@ export const fetchNotificationRequestFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const acceptNotificationRequest = id => (dispatch) => {
|
||||
export const acceptNotificationRequest = (id) => (dispatch, getState) => {
|
||||
const count = selectNotificationCountForRequest(getState(), id);
|
||||
dispatch(acceptNotificationRequestRequest(id));
|
||||
|
||||
api().post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||
dispatch(acceptNotificationRequestSuccess(id));
|
||||
dispatch(decreasePendingNotificationsCount(count));
|
||||
}).catch(err => {
|
||||
dispatch(acceptNotificationRequestFail(id, err));
|
||||
});
|
||||
|
|
@ -559,11 +557,13 @@ export const acceptNotificationRequestFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const dismissNotificationRequest = id => (dispatch) => {
|
||||
export const dismissNotificationRequest = (id) => (dispatch, getState) => {
|
||||
const count = selectNotificationCountForRequest(getState(), id);
|
||||
dispatch(dismissNotificationRequestRequest(id));
|
||||
|
||||
api().post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||
dispatch(dismissNotificationRequestSuccess(id));
|
||||
dispatch(decreasePendingNotificationsCount(count));
|
||||
}).catch(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 type { ApiAccountJSON } from '../api_types/accounts';
|
||||
// To be replaced once ApiNotificationJSON type exists
|
||||
interface FakeApiNotificationJSON {
|
||||
type: string;
|
||||
account: ApiAccountJSON;
|
||||
}
|
||||
import type { ApiNotificationJSON } from 'flavours/glitch/api_types/notifications';
|
||||
|
||||
export const notificationsUpdate = createAction(
|
||||
'notifications/update',
|
||||
|
|
@ -13,7 +8,7 @@ export const notificationsUpdate = createAction(
|
|||
playSound,
|
||||
...args
|
||||
}: {
|
||||
notification: FakeApiNotificationJSON;
|
||||
notification: ApiNotificationJSON;
|
||||
usePendingItems: boolean;
|
||||
playSound: boolean;
|
||||
}) => ({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { browserHistory } from 'flavours/glitch/components/router';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
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]);
|
||||
|
||||
if (status.get('poll')) {
|
||||
|
|
@ -105,7 +107,7 @@ export const editStatus = (id, routerHistory) => (dispatch, getState) => {
|
|||
|
||||
api().get(`/api/v1/statuses/${id}/source`).then(response => {
|
||||
dispatch(fetchStatusSourceSuccess());
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
dispatch(setComposeToStatus(status, response.data.text, response.data.spoiler_text, response.data.content_type));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusSourceFail(error));
|
||||
|
|
@ -125,7 +127,7 @@ export const fetchStatusSourceFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||
export function deleteStatus(id, withRedraft = false) {
|
||||
return (dispatch, getState) => {
|
||||
let status = getState().getIn(['statuses', id]);
|
||||
|
||||
|
|
@ -142,7 +144,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status, response.data.text, response.data.content_type));
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
ensureComposeIsVisible(getState);
|
||||
}
|
||||
}).catch(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) {
|
||||
return {
|
||||
type: STATUS_COLLAPSE,
|
||||
|
|
@ -349,3 +366,15 @@ export const undoStatusTranslation = (id, pollId) => ({
|
|||
id,
|
||||
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,
|
||||
} from './announcements';
|
||||
import { updateConversations } from './conversations';
|
||||
import { processNewNotificationForGroups } from './notification_groups';
|
||||
import { updateNotifications, expandNotifications } from './notifications';
|
||||
import { updateStatus } from './statuses';
|
||||
import {
|
||||
|
|
@ -98,10 +99,16 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
|
|||
case 'delete':
|
||||
dispatch(deleteFromTimelines(data.payload));
|
||||
break;
|
||||
case 'notification':
|
||||
case 'notification': {
|
||||
// @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;
|
||||
}
|
||||
case 'conversation':
|
||||
// @ts-expect-error
|
||||
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;
|
||||
inline?: boolean;
|
||||
animate?: boolean;
|
||||
counter?: number | string;
|
||||
counterBorderColor?: string;
|
||||
}
|
||||
|
||||
export const Avatar: React.FC<Props> = ({
|
||||
|
|
@ -19,6 +21,8 @@ export const Avatar: React.FC<Props> = ({
|
|||
size = 20,
|
||||
inline = false,
|
||||
style: styleFromParent,
|
||||
counter,
|
||||
counterBorderColor,
|
||||
}) => {
|
||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
||||
|
||||
|
|
@ -43,7 +47,15 @@ export const Avatar: React.FC<Props> = ({
|
|||
style={style}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useCallback } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
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';
|
||||
|
||||
|
|
@ -13,17 +15,15 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
interface Props {
|
||||
export const Domain: React.FC<{
|
||||
domain: string;
|
||||
onUnblockDomain: (domain: string) => void;
|
||||
}
|
||||
|
||||
export const Domain: React.FC<Props> = ({ domain, onUnblockDomain }) => {
|
||||
}> = ({ domain }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDomainUnblock = useCallback(() => {
|
||||
onUnblockDomain(domain);
|
||||
}, [domain, onUnblockDomain]);
|
||||
dispatch(unblockDomain(domain));
|
||||
}, [dispatch, domain]);
|
||||
|
||||
return (
|
||||
<div className='domain'>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ let id = 0;
|
|||
class DropdownMenu extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
scrollable: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
|
@ -39,6 +39,7 @@ class DropdownMenu extends PureComponent {
|
|||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -164,7 +165,7 @@ class Dropdown extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
|
||||
items: PropTypes.array.isRequired,
|
||||
loading: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
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 {
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
|
|
@ -59,29 +58,14 @@ export const FollowButton: React.FC<{
|
|||
|
||||
if (accountId === me) {
|
||||
return;
|
||||
} else if (relationship.following || relationship.requested) {
|
||||
} else if (account && (relationship.following || relationship.requested)) {
|
||||
dispatch(
|
||||
openModal({
|
||||
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));
|
||||
},
|
||||
},
|
||||
}),
|
||||
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
|
||||
);
|
||||
} else {
|
||||
dispatch(followAccount(accountId));
|
||||
}
|
||||
}, [dispatch, intl, accountId, relationship, account, signedIn]);
|
||||
}, [dispatch, accountId, relationship, account, signedIn]);
|
||||
|
||||
let label;
|
||||
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ const messages = defineMessages({
|
|||
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||
});
|
||||
|
||||
interface Props {
|
||||
interface Props<T> {
|
||||
disabled: boolean;
|
||||
maxId: string;
|
||||
onClick: (maxId: string) => void;
|
||||
param: T;
|
||||
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 handleClick = useCallback(() => {
|
||||
onClick(maxId);
|
||||
}, [maxId, onClick]);
|
||||
onClick(param);
|
||||
}, [param, onClick]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ type LocationState = MastodonLocationState | null | undefined;
|
|||
|
||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||
|
||||
const browserHistory = createBrowserHistory<LocationState>();
|
||||
export const browserHistory = createBrowserHistory<LocationState>();
|
||||
const originalReplace = browserHistory.replace.bind(browserHistory);
|
||||
|
||||
export function useAppHistory() {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ class Status extends ImmutablePureComponent {
|
|||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
scrollKey: PropTypes.string,
|
||||
skipPrepend: PropTypes.bool,
|
||||
avatarSize: PropTypes.number,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
pictureInPicture: ImmutablePropTypes.contains({
|
||||
|
|
@ -445,7 +447,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleHotkeyReply = e => {
|
||||
e.preventDefault();
|
||||
this.props.onReply(this.props.status, this.props.history);
|
||||
this.props.onReply(this.props.status);
|
||||
};
|
||||
|
||||
handleHotkeyFavourite = (e) => {
|
||||
|
|
@ -462,7 +464,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
handleHotkeyMention = e => {
|
||||
e.preventDefault();
|
||||
this.props.onMention(this.props.status.get('account'), this.props.history);
|
||||
this.props.onMention(this.props.status.get('account'));
|
||||
};
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
|
|
@ -523,12 +525,14 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, pictureInPicture, previousId, nextInReplyToId, rootId, skipPrepend, avatarSize = 46 } = this.props;
|
||||
|
||||
const {
|
||||
parseClick,
|
||||
setCollapsed,
|
||||
} = this;
|
||||
|
||||
const {
|
||||
intl,
|
||||
status,
|
||||
account,
|
||||
settings,
|
||||
|
|
@ -538,13 +542,6 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenVideo,
|
||||
onOpenMedia,
|
||||
notification,
|
||||
hidden,
|
||||
unread,
|
||||
featured,
|
||||
pictureInPicture,
|
||||
previousId,
|
||||
nextInReplyToId,
|
||||
rootId,
|
||||
history,
|
||||
identity,
|
||||
...other
|
||||
|
|
@ -594,8 +591,8 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
if (hidden) {
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div ref={this.handleRef} className='status focusable' tabIndex={0}>
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div ref={this.handleRef} className='status focusable' tabIndex={unfocusable ? null : 0}>
|
||||
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
|
||||
<span>{status.get('content')}</span>
|
||||
</div>
|
||||
|
|
@ -615,8 +612,8 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={minHandlers}>
|
||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
|
||||
<HotKeys handlers={minHandlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={unfocusable ? null : 0} ref={this.handleRef}>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
|
||||
{' '}
|
||||
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
|
||||
|
|
@ -796,17 +793,17 @@ class Status extends ImmutablePureComponent {
|
|||
contentMedia.push(hashtagBar);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<HotKeys handlers={handlers} tabIndex={unfocusable ? null : -1}>
|
||||
<div
|
||||
className={classNames('status__wrapper', 'focusable', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, collapsed: isCollapsed })}
|
||||
{...selectorAttribs}
|
||||
tabIndex={0}
|
||||
tabIndex={unfocusable ? null : 0}
|
||||
data-featured={featured ? 'true' : null}
|
||||
aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}
|
||||
ref={this.handleRef}
|
||||
data-nosnippet={status.getIn(['account', 'noindex'], true) || undefined}
|
||||
>
|
||||
{prepend}
|
||||
{!skipPrepend && prepend}
|
||||
|
||||
<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 })}
|
||||
|
|
@ -822,6 +819,7 @@ class Status extends ImmutablePureComponent {
|
|||
friend={account}
|
||||
collapsed={isCollapsed}
|
||||
parseClick={parseClick}
|
||||
avatarSize={avatarSize}
|
||||
/>
|
||||
<StatusIcons
|
||||
status={status}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
const { signedIn } = this.props.identity;
|
||||
|
||||
if (signedIn) {
|
||||
this.props.onReply(this.props.status, this.props.history);
|
||||
this.props.onReply(this.props.status);
|
||||
} else {
|
||||
this.props.onInteractionModal('reply', this.props.status);
|
||||
}
|
||||
|
|
@ -153,15 +153,15 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
this.props.onDelete(this.props.status, this.props.history);
|
||||
this.props.onDelete(this.props.status);
|
||||
};
|
||||
|
||||
handleRedraftClick = () => {
|
||||
this.props.onDelete(this.props.status, this.props.history, true);
|
||||
this.props.onDelete(this.props.status, true);
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.onEdit(this.props.status, this.props.history);
|
||||
this.props.onEdit(this.props.status);
|
||||
};
|
||||
|
||||
handlePinClick = () => {
|
||||
|
|
@ -169,11 +169,11 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleMentionClick = () => {
|
||||
this.props.onMention(this.props.status.get('account'), this.props.history);
|
||||
this.props.onMention(this.props.status.get('account'));
|
||||
};
|
||||
|
||||
handleDirectClick = () => {
|
||||
this.props.onDirect(this.props.status.get('account'), this.props.history);
|
||||
this.props.onDirect(this.props.status.get('account'));
|
||||
};
|
||||
|
||||
handleMuteClick = () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export default class StatusHeader extends PureComponent {
|
|||
static propTypes = {
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
friend: ImmutablePropTypes.map,
|
||||
avatarSize: PropTypes.number,
|
||||
parseClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -33,13 +34,14 @@ export default class StatusHeader extends PureComponent {
|
|||
const {
|
||||
status,
|
||||
friend,
|
||||
avatarSize,
|
||||
} = this.props;
|
||||
|
||||
const account = status.get('account');
|
||||
|
||||
let statusAvatar;
|
||||
if (friend === undefined || friend === null) {
|
||||
statusAvatar = <Avatar account={account} size={46} />;
|
||||
statusAvatar = <Avatar account={account} size={avatarSize} />;
|
||||
} else {
|
||||
statusAvatar = <AvatarOverlay account={account} friend={friend} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
<LoadGap
|
||||
key={'gap:' + statusIds.get(index + 1)}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? statusIds.get(index - 1) : null}
|
||||
param={index > 0 ? statusIds.get(index - 1) : null}
|
||||
onClick={onLoadMore}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export default class StatusPrepend extends PureComponent {
|
|||
return (
|
||||
<FormattedMessage
|
||||
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 { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
muteAccount,
|
||||
unmuteAccount,
|
||||
} from '../actions/accounts';
|
||||
import { openModal } from '../actions/modal';
|
||||
import { initMuteModal } from '../actions/mutes';
|
||||
import Account from '../components/account';
|
||||
import { makeGetAccount } from '../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
|
|
@ -29,18 +25,11 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onFollow (account) {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(openModal({
|
||||
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'))),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
||||
} else {
|
||||
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 { initBlockModal } from 'flavours/glitch/actions/blocks';
|
||||
|
|
@ -12,18 +10,15 @@ import {
|
|||
initAddFilter,
|
||||
} from 'flavours/glitch/actions/filters';
|
||||
import {
|
||||
reblog,
|
||||
favourite,
|
||||
toggleReblog,
|
||||
toggleFavourite,
|
||||
bookmark,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
addReaction,
|
||||
removeReaction,
|
||||
} from 'flavours/glitch/actions/interactions';
|
||||
import { changeLocalSetting } from 'flavours/glitch/actions/local_settings';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import { deployPictureInPicture } from 'flavours/glitch/actions/picture_in_picture';
|
||||
|
|
@ -32,33 +27,17 @@ import {
|
|||
muteStatus,
|
||||
unmuteStatus,
|
||||
deleteStatus,
|
||||
hideStatus,
|
||||
revealStatus,
|
||||
toggleStatusSpoilers,
|
||||
editStatus,
|
||||
translateStatus,
|
||||
undoStatusTranslation,
|
||||
} from 'flavours/glitch/actions/statuses';
|
||||
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 { 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 getStatus = makeGetStatus();
|
||||
const getPictureInPicture = makeGetPictureInPicture();
|
||||
|
|
@ -93,47 +72,22 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||
const mapDispatchToProps = (dispatch, { contextType }) => ({
|
||||
|
||||
onReply (status, router) {
|
||||
onReply (status) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
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)),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
|
||||
} 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) {
|
||||
dispatch((_, getState) => {
|
||||
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 } }));
|
||||
}
|
||||
});
|
||||
dispatch(toggleReblog(status.get('id'), e.shiftKey));
|
||||
},
|
||||
|
||||
onBookmark (status) {
|
||||
|
|
@ -144,26 +98,8 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onModalFavourite (status) {
|
||||
dispatch(favourite(status));
|
||||
},
|
||||
|
||||
onFavourite (status, e) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
if (e.shiftKey || !favouriteModal) {
|
||||
this.onModalFavourite(status);
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
modalType: 'FAVOURITE',
|
||||
modalProps: {
|
||||
status,
|
||||
onFavourite: this.onModalFavourite,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
dispatch(toggleFavourite(status.get('id'), e.shiftKey));
|
||||
},
|
||||
|
||||
onPin (status) {
|
||||
|
|
@ -192,35 +128,21 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
}));
|
||||
},
|
||||
|
||||
onDelete (status, history, withRedraft = false) {
|
||||
onDelete (status, withRedraft = false) {
|
||||
if (!deleteModal) {
|
||||
dispatch(deleteStatus(status.get('id'), history, withRedraft));
|
||||
dispatch(deleteStatus(status.get('id'), withRedraft));
|
||||
} else {
|
||||
dispatch(openModal({
|
||||
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)),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
|
||||
}
|
||||
},
|
||||
|
||||
onEdit (status, history) {
|
||||
onEdit (status) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.editMessage),
|
||||
confirm: intl.formatMessage(messages.editConfirm),
|
||||
onConfirm: () => dispatch(editStatus(status.get('id'), history)),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_EDIT_STATUS', modalProps: { statusId: status.get('id') } }));
|
||||
} else {
|
||||
dispatch(editStatus(status.get('id'), history));
|
||||
dispatch(editStatus(status.get('id')));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -233,12 +155,12 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
onDirect (account) {
|
||||
dispatch(directCompose(account));
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
onMention (account) {
|
||||
dispatch(mentionCompose(account));
|
||||
},
|
||||
|
||||
onOpenMedia (statusId, media, index, lang) {
|
||||
|
|
@ -281,11 +203,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
|||
},
|
||||
|
||||
onToggleHidden (status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
dispatch(toggleStatusSpoilers(status.get('id')));
|
||||
},
|
||||
|
||||
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 =
|
||||
statusEl.dataset.spoiler === 'expanded'
|
||||
? localeData['status.show_less'] ?? 'Show less'
|
||||
: localeData['status.show_more'] ?? 'Show more';
|
||||
? (localeData['status.show_less'] ?? 'Show less')
|
||||
: (localeData['status.show_more'] ?? 'Show more');
|
||||
spoilerLink.textContent = new IntlMessageFormat(
|
||||
message,
|
||||
locale,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ import PropTypes from 'prop-types';
|
|||
|
||||
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 ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import ActionBar from '../../account/components/action_bar';
|
||||
import InnerHeader from '../../account/components/header';
|
||||
|
||||
|
|
@ -36,7 +34,6 @@ class Header extends ImmutablePureComponent {
|
|||
hideTabs: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
handleFollow = () => {
|
||||
|
|
@ -48,11 +45,11 @@ class Header extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleMention = () => {
|
||||
this.props.onMention(this.props.account, this.props.history);
|
||||
this.props.onMention(this.props.account);
|
||||
};
|
||||
|
||||
handleDirect = () => {
|
||||
this.props.onDirect(this.props.account, this.props.history);
|
||||
this.props.onDirect(this.props.account);
|
||||
};
|
||||
|
||||
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 {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
pinAccount,
|
||||
|
|
@ -22,11 +21,6 @@ import { initReport } from '../../../actions/reports';
|
|||
import { makeGetAccount, getAccountHidden } from '../../../selectors';
|
||||
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 getAccount = makeGetAccount();
|
||||
|
||||
|
|
@ -39,18 +33,11 @@ const makeMapStateToProps = () => {
|
|||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onFollow (account) {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(openModal({
|
||||
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'))),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
|
|
@ -75,12 +62,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onMention (account, router) {
|
||||
dispatch(mentionCompose(account, router));
|
||||
onMention (account) {
|
||||
dispatch(mentionCompose(account));
|
||||
},
|
||||
|
||||
onDirect (account, router) {
|
||||
dispatch(directCompose(account, router));
|
||||
onDirect (account) {
|
||||
dispatch(directCompose(account));
|
||||
},
|
||||
|
||||
onReblogToggle (account) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { useDispatch } from 'react-redux';
|
|||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
|
|
@ -23,8 +22,6 @@ const messages = defineMessages({
|
|||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
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 = () => {
|
||||
|
|
@ -32,16 +29,8 @@ export const ActionBar = () => {
|
|||
const intl = useIntl();
|
||||
|
||||
const handleLogoutClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl]);
|
||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
||||
}, [dispatch]);
|
||||
|
||||
let menu = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
|
||||
import { length } from 'stringz';
|
||||
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import { Button } from '../../../components/button';
|
||||
|
|
@ -81,7 +79,6 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
maxChars: PropTypes.number,
|
||||
...WithOptionalRouterPropTypes
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
|
@ -141,9 +138,9 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
// Submit unless there are media with missing descriptions
|
||||
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'));
|
||||
this.props.onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overridePrivacy);
|
||||
this.props.onMediaDescriptionConfirm(firstWithoutDescription.get('id'), overridePrivacy);
|
||||
} 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 { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
import { EmbeddedStatusContent } from 'flavours/glitch/features/notifications_v2/components/embedded_status_content';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
|
|
@ -32,8 +33,6 @@ export const EditIndicator = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='edit-indicator'>
|
||||
<div className='edit-indicator__header'>
|
||||
|
|
@ -48,7 +47,12 @@ export const EditIndicator = () => {
|
|||
</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) && (
|
||||
<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 { Icon } from 'flavours/glitch/components/icon';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { EmbeddedStatusContent } from 'flavours/glitch/features/notifications_v2/components/embedded_status_content';
|
||||
|
||||
export const ReplyIndicator = () => {
|
||||
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||
|
|
@ -18,8 +19,6 @@ export const ReplyIndicator = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__line' />
|
||||
|
|
@ -33,7 +32,12 @@ export const ReplyIndicator = () => {
|
|||
<DisplayName account={account} />
|
||||
</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) && (
|
||||
<div className='reply-indicator__attachments'>
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(changeCompose(text));
|
||||
},
|
||||
|
||||
onSubmit (router, overridePrivacy = null) {
|
||||
dispatch(submitCompose(router, overridePrivacy));
|
||||
onSubmit (overridePrivacy = null) {
|
||||
dispatch(submitCompose(overridePrivacy));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
|
|
@ -110,14 +110,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(insertEmojiCompose(position, data, needsSpace));
|
||||
},
|
||||
|
||||
onMediaDescriptionConfirm (routerHistory, mediaId, overridePrivacy = null) {
|
||||
onMediaDescriptionConfirm (mediaId, overridePrivacy = null) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.missingDescriptionMessage),
|
||||
confirm: intl.formatMessage(messages.missingDescriptionConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(submitCompose(routerHistory, overridePrivacy));
|
||||
dispatch(submitCompose(overridePrivacy));
|
||||
},
|
||||
secondary: intl.formatMessage(messages.missingDescriptionEdit),
|
||||
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 glitchedElephant2 from 'flavours/glitch/images/mbstobon-ui-1.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 { changeComposing, mountCompose, unmountCompose } from '../../actions/compose';
|
||||
|
|
@ -45,8 +44,6 @@ const messages = defineMessages({
|
|||
settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
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) => ({
|
||||
|
|
@ -88,20 +85,12 @@ class Compose extends PureComponent {
|
|||
}
|
||||
|
||||
handleLogoutClick = e => {
|
||||
const { dispatch, intl } = this.props;
|
||||
const { dispatch } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
||||
|
||||
return false;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
|||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { markConversationRead, deleteConversation } from 'flavours/glitch/actions/conversations';
|
||||
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 AvatarComposite from 'flavours/glitch/components/avatar_composite';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
|
@ -37,8 +37,6 @@ const messages = defineMessages({
|
|||
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute 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(
|
||||
|
|
@ -121,19 +119,12 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
|||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(lastStatus, history)),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status: lastStatus } }));
|
||||
} else {
|
||||
dispatch(replyCompose(lastStatus, history));
|
||||
dispatch(replyCompose(lastStatus));
|
||||
}
|
||||
});
|
||||
}, [dispatch, lastStatus, history, intl]);
|
||||
}, [dispatch, lastStatus]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
dispatch(deleteConversation(id));
|
||||
|
|
@ -156,11 +147,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
|||
}, [dispatch, lastStatus]);
|
||||
|
||||
const handleShowMore = useCallback(() => {
|
||||
if (lastStatus.get('hidden')) {
|
||||
dispatch(revealStatus(lastStatus.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(lastStatus.get('id')));
|
||||
}
|
||||
dispatch(toggleStatusSpoilers(lastStatus.get('id')));
|
||||
|
||||
if (lastStatus.get('spoiler_text')) {
|
||||
setExpanded(!expanded);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import classNames from 'classnames';
|
|||
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
|
|
@ -29,20 +28,12 @@ const messages = defineMessages({
|
|||
id: 'account.cancel_follow_request',
|
||||
defaultMessage: 'Withdraw follow request',
|
||||
},
|
||||
cancelFollowRequestConfirm: {
|
||||
id: 'confirmations.cancel_follow_request.confirm',
|
||||
defaultMessage: 'Withdraw request',
|
||||
},
|
||||
requested: {
|
||||
id: 'account.requested',
|
||||
defaultMessage: 'Awaiting approval. Click to cancel follow request',
|
||||
},
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: {
|
||||
id: 'confirmations.unfollow.confirm',
|
||||
defaultMessage: 'Unfollow',
|
||||
},
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
|
|
@ -89,48 +80,17 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
|
|||
const handleFollow = useCallback(() => {
|
||||
if (!account) return;
|
||||
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (
|
||||
account.getIn(['relationship', 'following']) ||
|
||||
account.getIn(['relationship', 'requested'])
|
||||
) {
|
||||
dispatch(
|
||||
openModal({
|
||||
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')));
|
||||
},
|
||||
},
|
||||
}),
|
||||
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
|
||||
);
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
}, [account, dispatch, intl]);
|
||||
}, [account, dispatch]);
|
||||
|
||||
const handleBlock = useCallback(() => {
|
||||
if (account?.relationship?.blocking) {
|
||||
|
|
|
|||
|
|
@ -11,16 +11,15 @@ import { connect } from 'react-redux';
|
|||
import { debounce } from 'lodash';
|
||||
|
||||
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 { LoadingIndicator } from '../../components/loading_indicator';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import DomainContainer from '../../containers/domain_container';
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
|
|
@ -70,7 +69,7 @@ class Blocks extends ImmutablePureComponent {
|
|||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{domains.map(domain =>
|
||||
<DomainContainer key={domain} domain={domain} />,
|
||||
<Domain key={domain} domain={domain} />,
|
||||
)}
|
||||
</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 ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
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 { connectListStream } from 'flavours/glitch/actions/streaming';
|
||||
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';
|
||||
|
||||
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' },
|
||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
||||
|
|
@ -125,25 +123,10 @@ class ListTimeline extends PureComponent {
|
|||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId, intl } = this.props;
|
||||
const { dispatch, columnId } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal({
|
||||
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');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_LIST', modalProps: { listId: id, columnId } }));
|
||||
};
|
||||
|
||||
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 { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
||||
|
||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||
import ClearColumnButton from './clear_column_button';
|
||||
import GrantPermissionButton from './grant_permission_button';
|
||||
import PillBarButton from './pill_bar_button';
|
||||
import { PolicyControls } from './policy_controls';
|
||||
import SettingToggle from './setting_toggle';
|
||||
|
||||
class ColumnSettings extends PureComponent {
|
||||
|
|
@ -25,34 +25,17 @@ class ColumnSettings extends PureComponent {
|
|||
alertsEnabled: PropTypes.bool,
|
||||
browserSupport: PropTypes.bool,
|
||||
browserPermission: PropTypes.string,
|
||||
notificationPolicy: PropTypes.object.isRequired,
|
||||
onChangePolicy: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onPushChange = (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 () {
|
||||
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 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 filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
|
|
@ -80,31 +63,7 @@ class ColumnSettings extends PureComponent {
|
|||
</section>
|
||||
)}
|
||||
|
||||
<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={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>
|
||||
<PolicyControls />
|
||||
|
||||
<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>
|
||||
|
|
@ -115,6 +74,16 @@ class ColumnSettings extends PureComponent {
|
|||
</div>
|
||||
</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'>
|
||||
<h3 id='notifications-unread-markers'>
|
||||
<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 { fetchNotificationPolicy } from 'flavours/glitch/actions/notification_policies';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { selectSettingsNotificationsMinimizeFilteredBanner } from 'flavours/glitch/selectors/settings';
|
||||
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 = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const policy = useAppSelector((state) => state.notificationPolicy);
|
||||
const minimizeSetting = useAppSelector(
|
||||
selectSettingsNotificationsMinimizeFilteredBanner,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
void dispatch(fetchNotificationPolicy());
|
||||
|
|
@ -30,12 +74,18 @@ export const FilteredNotificationsBanner: React.FC = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (minimizeSetting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className='filtered-notifications-banner'
|
||||
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'>
|
||||
<strong>
|
||||
|
|
@ -47,22 +97,11 @@ export const FilteredNotificationsBanner: React.FC = () => {
|
|||
<span>
|
||||
<FormattedMessage
|
||||
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 }}
|
||||
/>
|
||||
</span>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import GavelIcon from '@/material-icons/400-24px/gavel.svg?react';
|
||||
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
|
||||
const messages = defineMessages({
|
||||
|
|
@ -36,19 +39,18 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
interface Props {
|
||||
action:
|
||||
| 'none'
|
||||
| 'disable'
|
||||
| 'mark_statuses_as_sensitive'
|
||||
| 'delete_statuses'
|
||||
| 'sensitive'
|
||||
| 'silence'
|
||||
| 'suspend';
|
||||
action: AccountWarningAction;
|
||||
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();
|
||||
|
||||
if (hidden) {
|
||||
|
|
@ -56,23 +58,32 @@ export const ModerationWarning: React.FC<Props> = ({ action, id, hidden }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/disputes/strikes/${id}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='notification__moderation-warning'
|
||||
<div
|
||||
role='button'
|
||||
className={classNames(
|
||||
'notification-group notification-group--link notification-group--moderation-warning focusable',
|
||||
{ '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>
|
||||
<span className='link-button'>
|
||||
<a
|
||||
href={`/disputes/strikes/${id}`}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='link-button'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='notification.moderation-warning.learn_more'
|
||||
defaultMessage='Learn more'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class Notification extends ImmutablePureComponent {
|
|||
e.preventDefault();
|
||||
|
||||
const { notification, onMention } = this.props;
|
||||
onMention(notification.get('account'), this.props.history);
|
||||
onMention(notification.get('account'));
|
||||
};
|
||||
|
||||
handleHotkeyFavourite = () => {
|
||||
|
|
|
|||
|
|
@ -38,12 +38,11 @@ export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
|||
return (
|
||||
<div className='notification-request'>
|
||||
<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__display-name'>
|
||||
<bdi><strong dangerouslySetInnerHTML={{ __html: account?.get('display_name_html') }} /></bdi>
|
||||
<span className='filtered-notifications-banner__badge'>{toCappedNumber(notificationsCount)}</span>
|
||||
</div>
|
||||
|
||||
<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 classNames from 'classnames';
|
||||
|
||||
import HeartBrokenIcon from '@/material-icons/400-24px/heart_broken-fill.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
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.' },
|
||||
});
|
||||
|
||||
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden }) => {
|
||||
export const RelationshipsSeveranceEvent = ({ type, target, followingCount, followersCount, hidden, unread }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (hidden) {
|
||||
|
|
@ -21,14 +23,14 @@ export const RelationshipsSeveranceEvent = ({ type, target, followingCount, foll
|
|||
}
|
||||
|
||||
return (
|
||||
<a href='/severed_relationships' target='_blank' rel='noopener noreferrer' className='notification__relationships-severance-event'>
|
||||
<Icon id='heart_broken' icon={HeartBrokenIcon} />
|
||||
<div role='button' className={classNames('notification-group notification-group--link notification-group--relationships-severance-event focusable', { 'notification-group--unread': unread })} tabIndex='0'>
|
||||
<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>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -42,4 +44,5 @@ RelationshipsSeveranceEvent.propTypes = {
|
|||
followersCount: PropTypes.number.isRequired,
|
||||
followingCount: PropTypes.number.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
unread: PropTypes.bool,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,17 +2,16 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||
|
||||
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 { openModal } from '../../../actions/modal';
|
||||
import { updateNotificationsPolicy } from '../../../actions/notification_policies';
|
||||
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
|
||||
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
|
||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||
import { changeSetting } from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
||||
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' },
|
||||
});
|
||||
|
||||
|
|
@ -25,10 +24,9 @@ const mapStateToProps = state => ({
|
|||
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||
notificationPolicy: state.notificationPolicy,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onChange (path, checked) {
|
||||
if (path[0] === 'push') {
|
||||
|
|
@ -58,32 +56,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
} else if(path[0] === 'groupingBeta') {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
dispatch(initializeNotifications());
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
},
|
||||
|
||||
onClear () {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.clearMessage),
|
||||
confirm: intl.formatMessage(messages.clearConfirm),
|
||||
onConfirm: () => dispatch(clearNotifications()),
|
||||
},
|
||||
}));
|
||||
dispatch(openModal({ modalType: 'CONFIRM_CLEAR_NOTIFICATIONS' }));
|
||||
},
|
||||
|
||||
onRequestNotificationPermission () {
|
||||
dispatch(requestBrowserPermission());
|
||||
},
|
||||
|
||||
onChangePolicy (param, checked) {
|
||||
dispatch(updateNotificationsPolicy({
|
||||
[param]: checked,
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||
|
|
|
|||
|
|
@ -2,13 +2,9 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { mentionCompose } from '../../../actions/compose';
|
||||
import {
|
||||
reblog,
|
||||
favourite,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
toggleReblog,
|
||||
toggleFavourite,
|
||||
} from '../../../actions/interactions';
|
||||
import { openModal } from '../../../actions/modal';
|
||||
import { boostModal } from '../../../initial_state';
|
||||
import { makeGetNotification, makeGetStatus, makeGetReport } from '../../../selectors';
|
||||
import Notification from '../components/notification';
|
||||
|
||||
|
|
@ -31,32 +27,16 @@ const makeMapStateToProps = () => {
|
|||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onMention: (account, router) => {
|
||||
dispatch(mentionCompose(account, router));
|
||||
},
|
||||
|
||||
onModalReblog (status, privacy) {
|
||||
dispatch(reblog({ statusId: status.get('id'), visibility: privacy }));
|
||||
onMention: (account) => {
|
||||
dispatch(mentionCompose(account));
|
||||
},
|
||||
|
||||
onReblog (status, e) {
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog({ statusId: status.get('id') }));
|
||||
} else {
|
||||
if (e.shiftKey || !boostModal) {
|
||||
this.onModalReblog(status);
|
||||
} else {
|
||||
dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } }));
|
||||
}
|
||||
}
|
||||
dispatch(toggleReblog(status.get('id'), e.shiftKey));
|
||||
},
|
||||
|
||||
onFavourite (status) {
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
onFavourite (status, e) {
|
||||
dispatch(toggleFavourite(status.get('id'), e.shiftKey));
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,10 @@ import { LoadGap } from '../../components/load_gap';
|
|||
import ScrollableList from '../../components/scrollable_list';
|
||||
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 ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import FilterBarContainer from './containers/filter_bar_container';
|
||||
|
|
@ -237,7 +240,7 @@ class Notifications extends PureComponent {
|
|||
<LoadGap
|
||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
||||
disabled={isLoading}
|
||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||
param={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
||||
onClick={this.handleLoadGap}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -258,6 +261,13 @@ class Notifications extends PureComponent {
|
|||
|
||||
let scrollContainer;
|
||||
|
||||
const prepend = (
|
||||
<>
|
||||
{needsNotificationPermission && <NotificationsPermissionBanner />}
|
||||
<FilteredNotificationsBanner />
|
||||
</>
|
||||
);
|
||||
|
||||
if (signedIn) {
|
||||
scrollContainer = (
|
||||
<ScrollableList
|
||||
|
|
@ -267,7 +277,7 @@ class Notifications extends PureComponent {
|
|||
showLoading={isLoading && notifications.size === 0}
|
||||
hasMore={hasMore}
|
||||
numPending={numPending}
|
||||
prepend={needsNotificationPermission && <NotificationsPermissionBanner />}
|
||||
prepend={prepend}
|
||||
alwaysPrepend
|
||||
emptyMessage={emptyMessage}
|
||||
onLoadMore={this.handleLoadOlder}
|
||||
|
|
@ -283,7 +293,9 @@ class Notifications extends PureComponent {
|
|||
scrollContainer = <NotSignedInIndicator />;
|
||||
}
|
||||
|
||||
const extraButtons = [];
|
||||
const extraButtons = [
|
||||
<FilteredNotificationsIconButton key='filtered-notifications-icon' className='column-header__button' />,
|
||||
];
|
||||
|
||||
if (canMarkAsRead) {
|
||||
extraButtons.push(
|
||||
|
|
@ -356,8 +368,6 @@ class Notifications extends PureComponent {
|
|||
|
||||
{filterBarContainer}
|
||||
|
||||
<FilteredNotificationsBanner />
|
||||
|
||||
{scrollContainer}
|
||||
|
||||
<Helmet>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { Helmet } from 'react-helmet';
|
|||
|
||||
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 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 Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
|
|
@ -101,7 +101,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
|||
showBackButton
|
||||
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)} />
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,52 @@ import { useSelector, useDispatch } from 'react-redux';
|
|||
|
||||
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
|
||||
import { fetchNotificationRequests, expandNotificationRequests } from 'flavours/glitch/actions/notifications';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
|
||||
import { NotificationRequest } from './components/notification_request';
|
||||
import { PolicyControls } from './components/policy_controls';
|
||||
import SettingToggle from './components/setting_toggle';
|
||||
|
||||
const messages = defineMessages({
|
||||
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 }) => {
|
||||
const columnRef = useRef();
|
||||
const intl = useIntl();
|
||||
|
|
@ -48,7 +84,9 @@ export const NotificationRequests = ({ multiColumn }) => {
|
|||
onClick={handleHeaderClick}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
>
|
||||
<ColumnSettings />
|
||||
</ColumnHeader>
|
||||
|
||||
<ScrollableList
|
||||
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