Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
fd1f0891d9
525 changed files with 9484 additions and 4272 deletions
|
@ -123,7 +123,7 @@ module.exports = defineConfig({
|
||||||
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
|
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
|
||||||
'react/self-closing-comp': 'error',
|
'react/self-closing-comp': 'error',
|
||||||
|
|
||||||
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js
|
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
|
||||||
'jsx-a11y/accessible-emoji': 'warn',
|
'jsx-a11y/accessible-emoji': 'warn',
|
||||||
'jsx-a11y/click-events-have-key-events': 'off',
|
'jsx-a11y/click-events-have-key-events': 'off',
|
||||||
'jsx-a11y/label-has-associated-control': 'off',
|
'jsx-a11y/label-has-associated-control': 'off',
|
||||||
|
@ -176,7 +176,7 @@ module.exports = defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
// See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js
|
// See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
|
||||||
'import/extensions': [
|
'import/extensions': [
|
||||||
'error',
|
'error',
|
||||||
'always',
|
'always',
|
||||||
|
|
|
@ -20,7 +20,7 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
|
||||||
|
|
||||||
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
|
||||||
# Example: v4.2.0-nightly.2023.11.09+something
|
# Example: v4.2.0-nightly.2023.11.09+something
|
||||||
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
|
# Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
|
||||||
ARG MASTODON_VERSION_PRERELEASE=""
|
ARG MASTODON_VERSION_PRERELEASE=""
|
||||||
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
|
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
|
||||||
ARG MASTODON_VERSION_METADATA=""
|
ARG MASTODON_VERSION_METADATA=""
|
||||||
|
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
|
||||||
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||||
ARG RAILS_SERVE_STATIC_FILES="true"
|
ARG RAILS_SERVE_STATIC_FILES="true"
|
||||||
# Allow to use YJIT compiler
|
# Allow to use YJIT compiler
|
||||||
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md
|
# See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
|
||||||
ARG RUBY_YJIT_ENABLE="1"
|
ARG RUBY_YJIT_ENABLE="1"
|
||||||
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||||
ARG TZ="Etc/UTC"
|
ARG TZ="Etc/UTC"
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -112,7 +112,7 @@ group :test do
|
||||||
# RSpec helpers for email specs
|
# RSpec helpers for email specs
|
||||||
gem 'email_spec'
|
gem 'email_spec'
|
||||||
|
|
||||||
# Extra RSpec extenion methods and helpers for sidekiq
|
# Extra RSpec extension methods and helpers for sidekiq
|
||||||
gem 'rspec-sidekiq', '~> 4.0'
|
gem 'rspec-sidekiq', '~> 4.0'
|
||||||
|
|
||||||
# Browser integration testing
|
# Browser integration testing
|
||||||
|
|
13
Gemfile.lock
13
Gemfile.lock
|
@ -465,11 +465,11 @@ GEM
|
||||||
statsd-ruby (~> 1.4, >= 1.4.0)
|
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||||
oj (3.16.3)
|
oj (3.16.3)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
omniauth (2.1.1)
|
omniauth (2.1.2)
|
||||||
hashie (>= 3.4.6)
|
hashie (>= 3.4.6)
|
||||||
rack (>= 2.2.3)
|
rack (>= 2.2.3)
|
||||||
rack-protection
|
rack-protection
|
||||||
omniauth-cas (3.0.0.beta.1)
|
omniauth-cas (3.0.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
nokogiri (~> 1.12)
|
nokogiri (~> 1.12)
|
||||||
omniauth (~> 2.1)
|
omniauth (~> 2.1)
|
||||||
|
@ -505,7 +505,7 @@ GEM
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.5)
|
pg (1.5.6)
|
||||||
pghero (3.4.1)
|
pghero (3.4.1)
|
||||||
activerecord (>= 6)
|
activerecord (>= 6)
|
||||||
posix-spawn (0.3.15)
|
posix-spawn (0.3.15)
|
||||||
|
@ -543,8 +543,9 @@ GEM
|
||||||
httpclient
|
httpclient
|
||||||
json-jwt (>= 1.11.0)
|
json-jwt (>= 1.11.0)
|
||||||
rack (>= 2.1.0)
|
rack (>= 2.1.0)
|
||||||
rack-protection (3.0.5)
|
rack-protection (3.2.0)
|
||||||
rack
|
base64 (>= 0.1.0)
|
||||||
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-session (1.0.2)
|
rack-session (1.0.2)
|
||||||
|
@ -743,7 +744,7 @@ GEM
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
terrapin (1.0.1)
|
terrapin (1.0.1)
|
||||||
climate_control
|
climate_control
|
||||||
test-prof (1.3.1)
|
test-prof (1.3.2)
|
||||||
thor (1.3.1)
|
thor (1.3.1)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
timeout (0.4.1)
|
timeout (0.4.1)
|
||||||
|
|
|
@ -53,7 +53,7 @@ module Admin
|
||||||
end
|
end
|
||||||
|
|
||||||
def resource_params
|
def resource_params
|
||||||
params.require(:rule).permit(:text, :priority)
|
params.require(:rule).permit(:text, :hint, :priority)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ class Api::BaseController < ApplicationController
|
||||||
include Api::AccessTokenTrackingConcern
|
include Api::AccessTokenTrackingConcern
|
||||||
include Api::CachingConcern
|
include Api::CachingConcern
|
||||||
include Api::ContentSecurityPolicy
|
include Api::ContentSecurityPolicy
|
||||||
|
include Api::ErrorHandling
|
||||||
|
|
||||||
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
skip_before_action :require_functional!, unless: :limited_federation_mode?
|
||||||
|
|
||||||
|
@ -18,51 +19,6 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
|
||||||
render json: { error: e.to_s }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotUnique do
|
|
||||||
render json: { error: 'Duplicate record' }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Date::Error do
|
|
||||||
render json: { error: 'Invalid date supplied' }, status: 422
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActiveRecord::RecordNotFound do
|
|
||||||
render json: { error: 'Record not found' }, status: 404
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
|
||||||
render json: { error: 'Remote data could not be fetched' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from OpenSSL::SSL::SSLError do
|
|
||||||
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::NotPermittedError do
|
|
||||||
render json: { error: 'This action is not allowed' }, status: 403
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
|
||||||
Rails.logger.warn "Storage server error: #{e}"
|
|
||||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
|
|
||||||
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from Mastodon::RateLimitExceededError do
|
|
||||||
render json: { error: I18n.t('errors.429') }, status: 429
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
|
|
||||||
render json: { error: e.to_s }, status: 400
|
|
||||||
end
|
|
||||||
|
|
||||||
def doorkeeper_unauthorized_render_options(error: nil)
|
def doorkeeper_unauthorized_render_options(error: nil)
|
||||||
{ json: { error: error.try(:description) || 'Not authorized' } }
|
{ json: { error: error.try(:description) || 'Not authorized' } }
|
||||||
end
|
end
|
||||||
|
@ -73,6 +29,14 @@ class Api::BaseController < ApplicationController
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
pagination_collection.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
pagination_collection.first.id
|
||||||
|
end
|
||||||
|
|
||||||
def set_pagination_headers(next_path = nil, prev_path = nil)
|
def set_pagination_headers(next_path = nil, prev_path = nil)
|
||||||
links = []
|
links = []
|
||||||
links << [next_path, [%w(rel next)]] if next_path
|
links << [next_path, [%w(rel next)]] if next_path
|
||||||
|
|
|
@ -51,11 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@statuses.last.id
|
@statuses
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@statuses.first.id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -137,12 +137,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
||||||
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@accounts.last.id
|
@accounts
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@accounts.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -77,12 +77,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
|
||||||
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
|
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@canonical_email_blocks.last.id
|
@canonical_email_blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@canonical_email_blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -73,12 +73,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
|
||||||
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
|
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@domain_allows.last.id
|
@domain_allows
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@domain_allows.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -84,12 +84,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
|
||||||
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
|
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@domain_blocks.last.id
|
@domain_blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@domain_blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -70,12 +70,8 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
|
||||||
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
|
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@email_domain_blocks.last.id
|
@email_domain_blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@email_domain_blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -75,12 +75,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
|
||||||
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
|
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@ip_blocks.last.id
|
@ip_blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@ip_blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -101,12 +101,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
|
||||||
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
|
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@reports.last.id
|
@reports
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@reports.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -56,12 +56,8 @@ class Api::V1::Admin::TagsController < Api::BaseController
|
||||||
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
|
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@tags.last.id
|
@tags
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@tags.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -54,12 +54,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
|
||||||
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
|
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@providers.last.id
|
@providers
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@providers.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -40,12 +40,8 @@ class Api::V1::BlocksController < Api::BaseController
|
||||||
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
|
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
paginated_blocks.last.id
|
paginated_blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
paginated_blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -43,12 +43,8 @@ class Api::V1::BookmarksController < Api::BaseController
|
||||||
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
results.last.id
|
results
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
results.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -41,12 +41,8 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
|
||||||
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@encrypted_messages.last.id
|
@encrypted_messages
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@encrypted_messages.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -50,12 +50,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
|
||||||
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
|
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@blocks.last.id
|
@blocks
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@blocks.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -44,12 +44,8 @@ class Api::V1::EndorsementsController < Api::BaseController
|
||||||
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
|
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@accounts.last.id
|
@accounts
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@accounts.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -43,12 +43,8 @@ class Api::V1::FavouritesController < Api::BaseController
|
||||||
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
results.last.id
|
results
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
results.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -34,12 +34,8 @@ class Api::V1::FollowedTagsController < Api::BaseController
|
||||||
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
|
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@results.last.id
|
@results
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@results.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -71,12 +71,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
||||||
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
|
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@accounts.last.id
|
@accounts
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@accounts.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
|
@ -40,12 +40,8 @@ class Api::V1::MutesController < Api::BaseController
|
||||||
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
|
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
paginated_mutes.last.id
|
paginated_mutes
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
paginated_mutes.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def records_continue?
|
def records_continue?
|
||||||
|
|
37
app/controllers/api/v1/notifications/policies_controller.rb
Normal file
37
app/controllers/api/v1/notifications/policies_controller.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Notifications::PoliciesController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :show
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: :update
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_policy
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @policy, serializer: REST::NotificationPolicySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@policy.update!(resource_params)
|
||||||
|
render json: @policy, serializer: REST::NotificationPolicySerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_policy
|
||||||
|
@policy = NotificationPolicy.find_or_initialize_by(account: current_account)
|
||||||
|
|
||||||
|
with_read_replica do
|
||||||
|
@policy.summarize!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_params
|
||||||
|
params.permit(
|
||||||
|
:filter_not_following,
|
||||||
|
:filter_not_followers,
|
||||||
|
:filter_new_accounts,
|
||||||
|
:filter_private_mentions
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
79
app/controllers/api/v1/notifications/requests_controller.rb
Normal file
79
app/controllers/api/v1/notifications/requests_controller.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Notifications::RequestsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, only: :index
|
||||||
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, except: :index
|
||||||
|
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_request, except: :index
|
||||||
|
|
||||||
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
def index
|
||||||
|
with_read_replica do
|
||||||
|
@requests = load_requests
|
||||||
|
@relationships = relationships
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: @requests, each_serializer: REST::NotificationRequestSerializer, relationships: @relationships
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @request, serializer: REST::NotificationRequestSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept
|
||||||
|
AcceptNotificationRequestService.new.call(@request)
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
def dismiss
|
||||||
|
@request.update!(dismissed: true)
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_requests
|
||||||
|
requests = NotificationRequest.where(account: current_account).where(dismissed: truthy_param?(:dismissed) || false).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||||
|
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||||
|
params_slice(:max_id, :since_id, :min_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
NotificationRequest.preload_cache_collection(requests) do |statuses|
|
||||||
|
cache_collection(statuses, Status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def relationships
|
||||||
|
StatusRelationshipsPresenter.new(@requests.map(&:last_status), current_user&.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_request
|
||||||
|
@request = NotificationRequest.where(account: current_account).find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_pagination_headers
|
||||||
|
set_pagination_headers(next_path, prev_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_path
|
||||||
|
api_v1_notifications_requests_url pagination_params(max_id: pagination_max_id) unless @requests.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def prev_path
|
||||||
|
api_v1_notifications_requests_url pagination_params(min_id: pagination_since_id) unless @requests.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_max_id
|
||||||
|
@requests.last.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_since_id
|
||||||
|
@requests.first.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_params(core_params)
|
||||||
|
params.slice(:dismissed).permit(:dismissed).merge(core_params)
|
||||||
|
end
|
||||||
|
end
|
|
@ -58,7 +58,8 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
current_account.notifications.without_suspended.browserable(
|
current_account.notifications.without_suspended.browserable(
|
||||||
types: Array(browserable_params[:types]),
|
types: Array(browserable_params[:types]),
|
||||||
exclude_types: Array(browserable_params[:exclude_types]),
|
exclude_types: Array(browserable_params[:exclude_types]),
|
||||||
from_account_id: browserable_params[:account_id]
|
from_account_id: browserable_params[:account_id],
|
||||||
|
include_filtered: truthy_param?(:include_filtered)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -78,19 +79,15 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
|
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@notifications.last.id
|
@notifications
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@notifications.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def browserable_params
|
def browserable_params
|
||||||
params.permit(:account_id, types: [], exclude_types: [])
|
params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_params(core_params)
|
def pagination_params(core_params)
|
||||||
params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params)
|
params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,11 +63,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
||||||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@statuses.last.id
|
@statuses
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@statuses.first.id
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,12 +9,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
|
||||||
set_pagination_headers(next_path, prev_path)
|
set_pagination_headers(next_path, prev_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pagination_max_id
|
def pagination_collection
|
||||||
@statuses.last.id
|
@statuses
|
||||||
end
|
|
||||||
|
|
||||||
def pagination_since_id
|
|
||||||
@statuses.first.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def next_path_params
|
def next_path_params
|
||||||
|
|
|
@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def single_user_mode?
|
def single_user_mode?
|
||||||
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
|
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def use_seamless_external_login?
|
def use_seamless_external_login?
|
||||||
|
|
52
app/controllers/concerns/api/error_handling.rb
Normal file
52
app/controllers/concerns/api/error_handling.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api::ErrorHandling
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
rescue_from ActiveRecord::RecordInvalid, Mastodon::ValidationError do |e|
|
||||||
|
render json: { error: e.to_s }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotUnique do
|
||||||
|
render json: { error: 'Duplicate record' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Date::Error do
|
||||||
|
render json: { error: 'Invalid date supplied' }, status: 422
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
|
render json: { error: 'Record not found' }, status: 404
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
||||||
|
render json: { error: 'Remote data could not be fetched' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from OpenSSL::SSL::SSLError do
|
||||||
|
render json: { error: 'Remote SSL certificate could not be verified' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::NotPermittedError do
|
||||||
|
render json: { error: 'This action is not allowed' }, status: 403
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||||
|
Rails.logger.warn "Storage server error: #{e}"
|
||||||
|
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight do
|
||||||
|
render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from Mastodon::RateLimitExceededError do
|
||||||
|
render json: { error: I18n.t('errors.429') }, status: 429
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActionController::ParameterMissing, Mastodon::InvalidParameterError do |e|
|
||||||
|
render json: { error: e.to_s }, status: 400
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -214,7 +214,7 @@ module ApplicationHelper
|
||||||
state_params[:moved_to_account] = current_account.moved_to_account
|
state_params[:moved_to_account] = current_account.moved_to_account
|
||||||
end
|
end
|
||||||
|
|
||||||
state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode?
|
state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
|
||||||
|
|
||||||
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
|
||||||
# rubocop:disable Rails/OutputSafety
|
# rubocop:disable Rails/OutputSafety
|
||||||
|
|
|
@ -198,6 +198,7 @@ module LanguagesHelper
|
||||||
ldn: ['Láadan', 'Láadan'].freeze,
|
ldn: ['Láadan', 'Láadan'].freeze,
|
||||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||||
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
|
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
|
||||||
|
nds: ['Low German', 'Plattdüütsch'].freeze,
|
||||||
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
|
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
|
||||||
sco: ['Scots', 'Scots'].freeze,
|
sco: ['Scots', 'Scots'].freeze,
|
||||||
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
|
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
|
||||||
|
|
|
@ -825,11 +825,12 @@ export function addPollOption(title) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changePollOption(index, title) {
|
export function changePollOption(index, title, maxOptions) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||||
index,
|
index,
|
||||||
title,
|
title,
|
||||||
|
maxOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||||
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
||||||
|
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
});
|
});
|
||||||
|
@ -402,3 +434,270 @@ export function setBrowserPermission (value) {
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fetchNotificationPolicy = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchNotificationPolicyRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationPolicySuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationPolicyFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationPolicyRequest = () => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationPolicySuccess = policy => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
|
||||||
|
policy,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationPolicyFail = error => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateNotificationsPolicy = params => (dispatch, getState) => {
|
||||||
|
dispatch(fetchNotificationPolicyRequest());
|
||||||
|
|
||||||
|
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationPolicySuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationPolicyFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequests = () => (dispatch, getState) => {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
|
||||||
|
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationRequestsRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
|
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationRequestsFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsRequest = () => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsSuccess = (requests, next) => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
|
||||||
|
requests,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsFail = error => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequests = () => (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['notificationRequests', 'next']);
|
||||||
|
|
||||||
|
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandNotificationRequestsRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
|
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(expandNotificationRequestsFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandNotificationRequestsRequest = () => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequestsSuccess = (requests, next) => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
|
||||||
|
requests,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequestsFail = error => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
const current = getState().getIn(['notificationRequests', 'current']);
|
||||||
|
|
||||||
|
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationRequestSuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestSuccess = request => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
dispatch(acceptNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||||
|
dispatch(acceptNotificationRequestSuccess(id));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(acceptNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const acceptNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequestSuccess = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
dispatch(dismissNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||||
|
dispatch(dismissNotificationRequestSuccess(id));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(dismissNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dismissNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequestSuccess = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
|
||||||
|
const current = getState().getIn(['notificationRequests', 'current']);
|
||||||
|
const params = { account_id: accountId };
|
||||||
|
|
||||||
|
if (current.getIn(['item', 'account']) === accountId) {
|
||||||
|
if (current.getIn(['notifications', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.getIn(['notifications', 'items'])?.size > 0) {
|
||||||
|
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationsForRequestRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
|
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationsForRequestFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestRequest = () => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||||
|
notifications,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestFail = (error) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
|
||||||
|
|
||||||
|
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandNotificationsForRequestRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
|
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(expandNotificationsForRequestFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestRequest = () => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
|
||||||
|
notifications,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestFail = (error) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent, useCallback } from 'react';
|
import { PureComponent, useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
@ -11,7 +11,7 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
|
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
|
import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
@ -23,10 +23,12 @@ const messages = defineMessages({
|
||||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||||
|
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const BackButton = ({ pinned, show }) => {
|
const BackButton = ({ pinned, show, onlyIcon }) => {
|
||||||
const history = useAppHistory();
|
const history = useAppHistory();
|
||||||
|
const intl = useIntl();
|
||||||
const { multiColumn } = useColumnsContext();
|
const { multiColumn } = useColumnsContext();
|
||||||
|
|
||||||
const handleBackClick = useCallback(() => {
|
const handleBackClick = useCallback(() => {
|
||||||
|
@ -39,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
|
||||||
|
|
||||||
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
||||||
|
|
||||||
if(!showButton) return null;
|
if (!showButton) return null;
|
||||||
|
|
||||||
return (<button onClick={handleBackClick} className='column-header__back-button'>
|
|
||||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
|
||||||
</button>);
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
|
||||||
|
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||||
|
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
BackButton.propTypes = {
|
BackButton.propTypes = {
|
||||||
pinned: PropTypes.bool,
|
pinned: PropTypes.bool,
|
||||||
show: PropTypes.bool,
|
show: PropTypes.bool,
|
||||||
|
onlyIcon: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ColumnHeader extends PureComponent {
|
class ColumnHeader extends PureComponent {
|
||||||
|
@ -145,27 +149,31 @@ class ColumnHeader extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiColumn && pinned) {
|
if (multiColumn && pinned) {
|
||||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||||
|
|
||||||
moveButtons = (
|
moveButtons = (
|
||||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
<div className='column-header__setting-arrows'>
|
||||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (multiColumn && this.props.onPin) {
|
} else if (multiColumn && this.props.onPin) {
|
||||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
backButton = <BackButton pinned={pinned} show={showBackButton} />;
|
backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
|
||||||
|
|
||||||
const collapsedContent = [
|
const collapsedContent = [
|
||||||
extraContent,
|
extraContent,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
collapsedContent.push(pinButton);
|
collapsedContent.push(
|
||||||
collapsedContent.push(moveButtons);
|
<div key='buttons' className='column-header__advanced-buttons'>
|
||||||
|
{pinButton}
|
||||||
|
{moveButtons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||||
|
@ -177,7 +185,7 @@ class ColumnHeader extends PureComponent {
|
||||||
onClick={this.handleToggleClick}
|
onClick={this.handleToggleClick}
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
<Icon id='sliders' icon={TuneIcon} />
|
<Icon id='sliders' icon={SettingsIcon} />
|
||||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -190,16 +198,19 @@ class ColumnHeader extends PureComponent {
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName}>
|
||||||
<h1 className={buttonClassName}>
|
<h1 className={buttonClassName}>
|
||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<button onClick={this.handleTitleClick}>
|
<>
|
||||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
{showBackButton && backButton}
|
||||||
{title}
|
|
||||||
</button>
|
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||||
|
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasTitle && backButton}
|
{!hasTitle && showBackButton && backButton}
|
||||||
|
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{hasTitle && backButton}
|
|
||||||
{extraButton}
|
{extraButton}
|
||||||
{collapseButton}
|
{collapseButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
|
import { CircularProgress } from 'flavours/glitch/components/circular_progress';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
|
@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
|
||||||
}) : (
|
}) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={!open ? icon : 'close'}
|
icon={!open ? icon : 'close'}
|
||||||
iconComponent={!open ? iconComponent : CloseIcon}
|
iconComponent={iconComponent}
|
||||||
title={title}
|
title={title}
|
||||||
active={open}
|
active={open}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
|
||||||
import { openModal } from 'flavours/glitch/actions/modal';
|
import { openModal } from 'flavours/glitch/actions/modal';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import InlineAccount from 'flavours/glitch/components/inline_account';
|
import InlineAccount from 'flavours/glitch/components/inline_account';
|
||||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||||
<button className='dropdown-menu__text-button'>
|
<button className='dropdown-menu__text-button'>
|
||||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import Card from '../features/status/components/card';
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
|
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||||
import { displayMedia, visibleReactions } from '../initial_state';
|
import { displayMedia, visibleReactions } from '../initial_state';
|
||||||
|
|
||||||
import AttachmentList from './attachment_list';
|
import AttachmentList from './attachment_list';
|
||||||
|
@ -73,9 +74,7 @@ export const defaultMediaVisibility = (status, settings) => {
|
||||||
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextType = SensitiveMediaContext;
|
||||||
identity: PropTypes.object,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
containerId: PropTypes.string,
|
containerId: PropTypes.string,
|
||||||
|
@ -132,8 +131,7 @@ class Status extends ImmutablePureComponent {
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
autoCollapsed: false,
|
autoCollapsed: false,
|
||||||
isExpanded: undefined,
|
isExpanded: undefined,
|
||||||
showMedia: undefined,
|
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
|
||||||
statusId: undefined,
|
|
||||||
revealBehindCW: undefined,
|
revealBehindCW: undefined,
|
||||||
showCard: false,
|
showCard: false,
|
||||||
forceFilter: undefined,
|
forceFilter: undefined,
|
||||||
|
@ -218,12 +216,6 @@ class Status extends ImmutablePureComponent {
|
||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
|
||||||
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
|
|
||||||
update.statusId = nextProps.status.get('id');
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
|
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
|
||||||
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
|
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
|
||||||
if (update.revealBehindCW) {
|
if (update.revealBehindCW) {
|
||||||
|
@ -319,6 +311,18 @@ class Status extends ImmutablePureComponent {
|
||||||
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
|
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
|
||||||
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
this.props.updateScrollBottom(snapshot.height - snapshot.top);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This will potentially cause a wasteful redraw, but in most cases `Status` components are used
|
||||||
|
// with a `key` directly depending on their `id`, preventing re-use of the component across
|
||||||
|
// different IDs.
|
||||||
|
// But just in case this does change, reset the state on status change.
|
||||||
|
|
||||||
|
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
|
||||||
|
this.setState({
|
||||||
|
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
|
||||||
|
forceFilter: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
@ -170,7 +170,8 @@ class About extends PureComponent {
|
||||||
<ol className='rules-list'>
|
<ol className='rules-list'>
|
||||||
{server.get('rules').map(rule => (
|
{server.get('rules').map(rule => (
|
||||||
<li key={rule.get('id')}>
|
<li key={rule.get('id')}>
|
||||||
<span className='rules-list__text'>{rule.get('text')}</span>
|
<div className='rules-list__text'>{rule.get('text')}</div>
|
||||||
|
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -188,18 +189,20 @@ class About extends PureComponent {
|
||||||
<>
|
<>
|
||||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||||
|
|
||||||
<div className='about__domain-blocks'>
|
{domainBlocks.get('items').size > 0 && (
|
||||||
{domainBlocks.get('items').map(block => (
|
<div className='about__domain-blocks'>
|
||||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
{domainBlocks.get('items').map(block => (
|
||||||
<div className='about__domain-blocks__domain__header'>
|
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||||
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
<div className='about__domain-blocks__domain__header'>
|
||||||
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||||
</div>
|
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { Button } from 'flavours/glitch/components/button';
|
||||||
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
|
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||||
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
|
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
||||||
|
@ -206,7 +207,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = '';
|
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
|
@ -347,15 +348,10 @@ class Header extends ImmutablePureComponent {
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{!hidden && (
|
{!hidden && bellBtn}
|
||||||
<>
|
{!hidden && shareBtn}
|
||||||
{actionBtn}
|
|
||||||
{bellBtn}
|
|
||||||
{shareBtn}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||||
|
{!hidden && actionBtn}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange, intl } = this.props;
|
const { settings, onChange, intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,10 +59,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||||
|
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
|
||||||
|
|
||||||
const handleChange = useCallback(({ target: { value } }) => {
|
const handleChange = useCallback(({ target: { value } }) => {
|
||||||
dispatch(changePollOption(index, value));
|
dispatch(changePollOption(index, value, maxOptions));
|
||||||
}, [dispatch, index]);
|
}, [dispatch, index, maxOptions]);
|
||||||
|
|
||||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||||
dispatch(fetchComposeSuggestions(token));
|
dispatch(fetchComposeSuggestions(token));
|
||||||
|
|
|
@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
import { LoadMore } from 'flavours/glitch/components/load_more';
|
import { LoadMore } from 'flavours/glitch/components/load_more';
|
||||||
|
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search-results'>
|
<div className='search-results'>
|
||||||
<div className='search-results__header'>
|
|
||||||
<Icon id='search' icon={SearchIcon} />
|
|
||||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{accounts}
|
{accounts}
|
||||||
{hashtags}
|
{hashtags}
|
||||||
{statuses}
|
{statuses}
|
||||||
|
|
|
@ -26,18 +26,20 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange, intl } = this.props;
|
const { settings, onChange, intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
<section>
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
|
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<div className='column-settings__row'>
|
||||||
|
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||||
<div className='column-settings__row'>
|
</div>
|
||||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
</section>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,28 +45,37 @@ const ColumnSettings = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle
|
<div className='column-settings__row'>
|
||||||
settings={settings}
|
<SettingToggle
|
||||||
settingPath={['onlyMedia']}
|
settings={settings}
|
||||||
onChange={onChange}
|
settingPath={['onlyMedia']}
|
||||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
onChange={onChange}
|
||||||
/>
|
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||||
<SettingToggle
|
/>
|
||||||
settings={settings}
|
|
||||||
settingPath={['allowLocalOnly']}
|
<SettingToggle
|
||||||
onChange={onChange}
|
settings={settings}
|
||||||
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
|
settingPath={['allowLocalOnly']}
|
||||||
/>
|
onChange={onChange}
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
|
||||||
<SettingText
|
/>
|
||||||
settings={settings}
|
</div>
|
||||||
settingPath={['regex', 'body']}
|
</section>
|
||||||
onChange={onChange}
|
|
||||||
label={intl.formatMessage(messages.filter_regex)}
|
<section>
|
||||||
/>
|
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
|
||||||
</div>
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingText
|
||||||
|
settings={settings}
|
||||||
|
settingPath={['regex', 'body']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={intl.formatMessage(messages.filter_regex)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -109,28 +109,28 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange } = this.props;
|
const { settings, onChange } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<div className='setting-toggle'>
|
<div className='column-settings__row'>
|
||||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||||
|
|
||||||
<span className='setting-toggle__label'>
|
<div className='setting-toggle'>
|
||||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||||
</span>
|
|
||||||
|
<span className='setting-toggle__label'>
|
||||||
|
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.open && (
|
{this.state.open && (
|
||||||
<div className='column-settings__hashtags'>
|
<div className='column-settings__hashtags'>
|
||||||
{this.modeSelect('any')}
|
{this.modeSelect('any')}
|
||||||
{this.modeSelect('all')}
|
{this.modeSelect('all')}
|
||||||
{this.modeSelect('none')}
|
{this.modeSelect('none')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
<div className='column-settings__row'>
|
|
||||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,75 +35,68 @@ export const ColumnSettings: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<span className='column-settings__section'>
|
<section>
|
||||||
<FormattedMessage
|
<div className='column-settings__row'>
|
||||||
id='home.column_settings.basic'
|
<SettingToggle
|
||||||
defaultMessage='Basic'
|
prefix='home_timeline'
|
||||||
/>
|
settings={settings}
|
||||||
</span>
|
settingPath={['shows', 'reblog']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.column_settings.show_reblogs'
|
||||||
|
defaultMessage='Show boosts'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<SettingToggle
|
||||||
<SettingToggle
|
prefix='home_timeline'
|
||||||
prefix='home_timeline'
|
settings={settings}
|
||||||
settings={settings}
|
settingPath={['shows', 'reply']}
|
||||||
settingPath={['shows', 'reblog']}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='home.column_settings.show_replies'
|
||||||
id='home.column_settings.show_reblogs'
|
defaultMessage='Show replies'
|
||||||
defaultMessage='Show boosts'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<SettingToggle
|
||||||
<SettingToggle
|
prefix='home_timeline'
|
||||||
prefix='home_timeline'
|
settings={settings}
|
||||||
settings={settings}
|
settingPath={['shows', 'direct']}
|
||||||
settingPath={['shows', 'reply']}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='home.column_settings.show_direct'
|
||||||
id='home.column_settings.show_replies'
|
defaultMessage='Show private mentions'
|
||||||
defaultMessage='Show replies'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<section aria-labelledby='home-column-advanced'>
|
||||||
<SettingToggle
|
<h3 id='home-column-advanced'>
|
||||||
prefix='home_timeline'
|
<FormattedMessage
|
||||||
settings={settings}
|
id='home.column_settings.advanced'
|
||||||
settingPath={['shows', 'direct']}
|
defaultMessage='Advanced'
|
||||||
onChange={onChange}
|
/>
|
||||||
label={
|
</h3>
|
||||||
<FormattedMessage
|
|
||||||
id='home.column_settings.show_direct'
|
|
||||||
defaultMessage='Show private mentions'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span className='column-settings__section'>
|
<div className='column-settings__row'>
|
||||||
<FormattedMessage
|
<SettingText
|
||||||
id='home.column_settings.advanced'
|
prefix='home_timeline'
|
||||||
defaultMessage='Advanced'
|
settings={settings}
|
||||||
/>
|
settingPath={['regex', 'body']}
|
||||||
</span>
|
onChange={onChange}
|
||||||
|
label={intl.formatMessage(messages.filter_regex)}
|
||||||
<div className='column-settings__row'>
|
/>
|
||||||
<SettingText
|
</div>
|
||||||
prefix='home_timeline'
|
</section>
|
||||||
settings={settings}
|
|
||||||
settingPath={['regex', 'body']}
|
|
||||||
onChange={onChange}
|
|
||||||
label={intl.formatMessage(messages.filter_regex)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -193,35 +193,38 @@ class ListTimeline extends PureComponent {
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
>
|
>
|
||||||
<div className='column-settings__row column-header__links'>
|
<div className='column-settings'>
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
<section className='column-header__links'>
|
||||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||||
</button>
|
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div className='setting-toggle'>
|
<section>
|
||||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
<div className='setting-toggle'>
|
||||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||||
</label>
|
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{ replies_policy !== undefined && (
|
|
||||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
|
||||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
|
||||||
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
|
|
||||||
</span>
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
{ ['none', 'list', 'followed'].map(policy => (
|
|
||||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
|
||||||
|
{replies_policy !== undefined && (
|
||||||
|
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||||
|
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
{ ['none', 'list', 'followed'].map(policy => (
|
||||||
|
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
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,
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
||||||
|
|
||||||
|
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||||
import ClearColumnButton from './clear_column_button';
|
import ClearColumnButton from './clear_column_button';
|
||||||
import GrantPermissionButton from './grant_permission_button';
|
import GrantPermissionButton from './grant_permission_button';
|
||||||
import PillBarButton from './pill_bar_button';
|
import PillBarButton from './pill_bar_button';
|
||||||
|
@ -27,14 +28,32 @@ export default class ColumnSettings extends PureComponent {
|
||||||
alertsEnabled: PropTypes.bool,
|
alertsEnabled: PropTypes.bool,
|
||||||
browserSupport: PropTypes.bool,
|
browserSupport: PropTypes.bool,
|
||||||
browserPermission: PropTypes.string,
|
browserPermission: PropTypes.string,
|
||||||
|
notificationPolicy: ImmutablePropTypes.map,
|
||||||
|
onChangePolicy: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
onPushChange = (path, checked) => {
|
onPushChange = (path, checked) => {
|
||||||
this.props.onChange(['push', ...path], checked);
|
this.props.onChange(['push', ...path], checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleFilterNotFollowing = checked => {
|
||||||
|
this.props.onChangePolicy('filter_not_following', checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFilterNotFollowers = checked => {
|
||||||
|
this.props.onChangePolicy('filter_not_followers', checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFilterNewAccounts = checked => {
|
||||||
|
this.props.onChangePolicy('filter_new_accounts', checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleFilterPrivateMentions = checked => {
|
||||||
|
this.props.onChangePolicy('filter_private_mentions', checked);
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
|
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
|
||||||
|
|
||||||
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
|
||||||
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
|
||||||
|
@ -47,48 +66,68 @@ export default class ColumnSettings extends PureComponent {
|
||||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
|
{alertsEnabled && browserSupport && browserPermission === 'denied' && (
|
||||||
<div className='column-settings__row column-settings__row--with-margin'>
|
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
|
||||||
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{alertsEnabled && browserSupport && browserPermission === 'default' && (
|
{alertsEnabled && browserSupport && browserPermission === 'default' && (
|
||||||
<div className='column-settings__row column-settings__row--with-margin'>
|
<span className='warning-hint'>
|
||||||
<span className='warning-hint'>
|
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
|
||||||
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<ClearColumnButton onClick={onClear} />
|
<ClearColumnButton onClick={onClear} />
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
<section>
|
||||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
|
||||||
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_following')} onChange={this.handleFilterNotFollowing}>
|
||||||
</div>
|
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
|
||||||
</div>
|
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_followers')} onChange={this.handleFilterNotFollowers}>
|
||||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
|
||||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
<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>
|
||||||
</span>
|
</CheckboxWithLabel>
|
||||||
|
|
||||||
|
<CheckboxWithLabel checked={notificationPolicy.get('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.get('filter_private_mentions')} onChange={this.handleFilterPrivateMentions}>
|
||||||
|
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
|
||||||
|
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
|
||||||
|
</CheckboxWithLabel>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section role='group' aria-labelledby='notifications-filter-bar'>
|
||||||
|
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
|
||||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-follow'>
|
<section role='group' aria-labelledby='notifications-unread-markers'>
|
||||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
<h3 id='notifications-unread-markers'>
|
||||||
|
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section role='group' aria-labelledby='notifications-follow'>
|
||||||
|
<h3 id='notifications-follow'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -96,10 +135,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-follow-request'>
|
<section role='group' aria-labelledby='notifications-follow-request'>
|
||||||
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span>
|
<h3 id='notifications-follow-request'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -107,10 +146,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-favourite'>
|
<section role='group' aria-labelledby='notifications-favourite'>
|
||||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
|
<h3 id='notifications-favourite'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -118,10 +157,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-reaction'>
|
<section role='group' aria-labelledby='notifications-reaction'>
|
||||||
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
|
<h3 id='notifications-reaction'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -129,10 +168,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-mention'>
|
<section role='group' aria-labelledby='notifications-mention'>
|
||||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
<h3 id='notifications-mention'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -140,10 +179,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-reblog'>
|
<section role='group' aria-labelledby='notifications-reblog'>
|
||||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
<h3 id='notifications-reblog'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -151,10 +190,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-poll'>
|
<section role='group' aria-labelledby='notifications-poll'>
|
||||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
<h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -162,10 +201,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-status'>
|
<section role='group' aria-labelledby='notifications-status'>
|
||||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
|
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -173,10 +212,10 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-update'>
|
<section role='group' aria-labelledby='notifications-update'>
|
||||||
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
|
<h3 id='notifications-update'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -184,11 +223,11 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
|
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
|
||||||
<div role='group' aria-labelledby='notifications-admin-sign-up'>
|
<section role='group' aria-labelledby='notifications-admin-sign-up'>
|
||||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span>
|
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -196,12 +235,12 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
|
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
|
||||||
<div role='group' aria-labelledby='notifications-admin-report'>
|
<section role='group' aria-labelledby='notifications-admin-report'>
|
||||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span>
|
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></h3>
|
||||||
|
|
||||||
<div className='column-settings__pillbar'>
|
<div className='column-settings__pillbar'>
|
||||||
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
|
||||||
|
@ -209,7 +248,7 @@ export default class ColumnSettings extends PureComponent {
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
|
||||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||||
|
import { fetchNotificationPolicy } from 'flavours/glitch/actions/notifications';
|
||||||
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
import { toCappedNumber } from 'flavours/glitch/utils/numbers';
|
||||||
|
|
||||||
|
export const FilteredNotificationsBanner = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const policy = useSelector(state => state.get('notificationPolicy'));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNotificationPolicy());
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
dispatch(fetchNotificationPolicy());
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (policy === null || policy.getIn(['summary', 'pending_notifications_count']) * 1 === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link className='filtered-notifications-banner' to='/notifications/requests'>
|
||||||
|
<Icon icon={ArchiveIcon} />
|
||||||
|
|
||||||
|
<div className='filtered-notifications-banner__text'>
|
||||||
|
<strong><FormattedMessage id='filtered_notifications_banner.title' defaultMessage='Filtered notifications' /></strong>
|
||||||
|
<span><FormattedMessage id='filtered_notifications_banner.pending_requests' defaultMessage='Notifications from {count, plural, =0 {no one} one {one person} other {# people}} you may know' values={{ count: policy.getIn(['summary', 'pending_requests_count']) }} /></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='filtered-notifications-banner__badge'>
|
||||||
|
{toCappedNumber(policy.getIn(['summary', 'pending_notifications_count']))}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,65 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import DoneIcon from '@/material-icons/400-24px/done.svg?react';
|
||||||
|
import VolumeOffIcon from '@/material-icons/400-24px/volume_off.svg?react';
|
||||||
|
import { acceptNotificationRequest, dismissNotificationRequest } from 'flavours/glitch/actions/notifications';
|
||||||
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||||
|
import { toCappedNumber } from 'flavours/glitch/utils/numbers';
|
||||||
|
|
||||||
|
const getAccount = makeGetAccount();
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||||
|
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NotificationRequest = ({ id, accountId, notificationsCount }) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const account = useSelector(state => getAccount(state, accountId));
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const handleDismiss = useCallback(() => {
|
||||||
|
dispatch(dismissNotificationRequest(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleAccept = useCallback(() => {
|
||||||
|
dispatch(acceptNotificationRequest(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='notification-request'>
|
||||||
|
<Link to={`/notifications/requests/${id}`} className='notification-request__link'>
|
||||||
|
<Avatar account={account} size={36} />
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className='notification-request__actions'>
|
||||||
|
<IconButton iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||||
|
<IconButton iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationRequest.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
accountId: PropTypes.string.isRequired,
|
||||||
|
notificationsCount: PropTypes.string.isRequired,
|
||||||
|
};
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { showAlert } from '../../../actions/alerts';
|
import { showAlert } from '../../../actions/alerts';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications';
|
import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications';
|
||||||
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
|
||||||
import { changeSetting } from '../../../actions/settings';
|
import { changeSetting } from '../../../actions/settings';
|
||||||
import ColumnSettings from '../components/column_settings';
|
import ColumnSettings from '../components/column_settings';
|
||||||
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
||||||
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||||
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||||
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||||
|
notificationPolicy: state.get('notificationPolicy'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
|
@ -73,6 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(requestBrowserPermission());
|
dispatch(requestBrowserPermission());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onChangePolicy (param, checked) {
|
||||||
|
dispatch(updateNotificationsPolicy({
|
||||||
|
[param]: checked,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||||
|
|
|
@ -37,6 +37,7 @@ import { LoadGap } from '../../components/load_gap';
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
|
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
|
||||||
|
|
||||||
|
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
|
||||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import FilterBarContainer from './containers/filter_bar_container';
|
import FilterBarContainer from './containers/filter_bar_container';
|
||||||
|
@ -357,6 +358,9 @@ class Notifications extends PureComponent {
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
{filterBarContainer}
|
{filterBarContainer}
|
||||||
|
|
||||||
|
<FilteredNotificationsBanner />
|
||||||
|
|
||||||
{scrollContainer}
|
{scrollContainer}
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
|
|
@ -0,0 +1,147 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||||
|
import DoneIcon from '@/material-icons/400-24px/done.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';
|
||||||
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
|
import { SensitiveMediaContextProvider } from 'flavours/glitch/features/ui/util/sensitive_media_context';
|
||||||
|
|
||||||
|
import NotificationContainer from './containers/notification_container';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'notification_requests.notifications_from', defaultMessage: 'Notifications from {name}' },
|
||||||
|
accept: { id: 'notification_requests.accept', defaultMessage: 'Accept' },
|
||||||
|
dismiss: { id: 'notification_requests.dismiss', defaultMessage: 'Dismiss' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectChild = (ref, index, alignTop) => {
|
||||||
|
const container = ref.current.node;
|
||||||
|
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
if (alignTop && container.scrollTop > element.offsetTop) {
|
||||||
|
element.scrollIntoView(true);
|
||||||
|
} else if (!alignTop && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||||
|
element.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationRequest = ({ multiColumn, params: { id } }) => {
|
||||||
|
const columnRef = useRef();
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const notificationRequest = useSelector(state => state.getIn(['notificationRequests', 'current', 'item', 'id']) === id ? state.getIn(['notificationRequests', 'current', 'item']) : null);
|
||||||
|
const accountId = notificationRequest?.get('account');
|
||||||
|
const account = useSelector(state => state.getIn(['accounts', accountId]));
|
||||||
|
const notifications = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'items']));
|
||||||
|
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'current', 'notifications', 'isLoading']));
|
||||||
|
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'current', 'notifications', 'next']));
|
||||||
|
const removed = useSelector(state => state.getIn(['notificationRequests', 'current', 'removed']));
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, [columnRef]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
dispatch(expandNotificationsForRequest());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleDismiss = useCallback(() => {
|
||||||
|
dispatch(dismissNotificationRequest(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleAccept = useCallback(() => {
|
||||||
|
dispatch(acceptNotificationRequest(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleMoveUp = useCallback(id => {
|
||||||
|
const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||||
|
selectChild(columnRef, elementIndex, true);
|
||||||
|
}, [columnRef, notifications]);
|
||||||
|
|
||||||
|
const handleMoveDown = useCallback(id => {
|
||||||
|
const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||||
|
selectChild(columnRef, elementIndex, false);
|
||||||
|
}, [columnRef, notifications]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNotificationRequest(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) {
|
||||||
|
dispatch(fetchNotificationsForRequest(accountId));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId]);
|
||||||
|
|
||||||
|
const columnTitle = intl.formatMessage(messages.title, { name: account?.get('display_name') || account?.get('username') });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={columnTitle}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='archive'
|
||||||
|
iconComponent={ArchiveIcon}
|
||||||
|
title={columnTitle}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
extraButton={!removed && (
|
||||||
|
<>
|
||||||
|
<IconButton className='column-header__button' iconComponent={VolumeOffIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||||
|
<IconButton className='column-header__button' iconComponent={DoneIcon} onClick={handleAccept} title={intl.formatMessage(messages.accept)} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SensitiveMediaContextProvider hideMediaByDefault>
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey={`notification_requests/${id}`}
|
||||||
|
trackScroll={!multiColumn}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading && notifications.size === 0}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
>
|
||||||
|
{notifications.map(item => (
|
||||||
|
item && <NotificationContainer
|
||||||
|
key={item.get('id')}
|
||||||
|
notification={item}
|
||||||
|
accountId={item.get('account')}
|
||||||
|
onMoveUp={handleMoveUp}
|
||||||
|
onMoveDown={handleMoveDown}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
</SensitiveMediaContextProvider>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{columnTitle}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationRequest.propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationRequest;
|
|
@ -0,0 +1,85 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import ArchiveIcon from '@/material-icons/400-24px/archive.svg?react';
|
||||||
|
import { fetchNotificationRequests, expandNotificationRequests } from 'flavours/glitch/actions/notifications';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NotificationRequests = ({ multiColumn }) => {
|
||||||
|
const columnRef = useRef();
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const isLoading = useSelector(state => state.getIn(['notificationRequests', 'isLoading']));
|
||||||
|
const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
|
||||||
|
const hasMore = useSelector(state => !!state.getIn(['notificationRequests', 'next']));
|
||||||
|
|
||||||
|
const handleHeaderClick = useCallback(() => {
|
||||||
|
columnRef.current?.scrollTop();
|
||||||
|
}, [columnRef]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
dispatch(expandNotificationRequests());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchNotificationRequests());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='archive'
|
||||||
|
iconComponent={ArchiveIcon}
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onClick={handleHeaderClick}
|
||||||
|
multiColumn={multiColumn}
|
||||||
|
showBackButton
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey='notification_requests'
|
||||||
|
trackScroll={!multiColumn}
|
||||||
|
bindToDocument={!multiColumn}
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading && notificationRequests.size === 0}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
emptyMessage={<FormattedMessage id='empty_column.notification_requests' defaultMessage='All clear! There is nothing here. When you receive new notifications, they will appear here according to your settings.' />}
|
||||||
|
>
|
||||||
|
{notificationRequests.map(request => (
|
||||||
|
<NotificationRequest
|
||||||
|
key={request.get('id')}
|
||||||
|
id={request.get('id')}
|
||||||
|
accountId={request.get('account')}
|
||||||
|
notificationsCount={request.get('notifications_count')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollableList>
|
||||||
|
|
||||||
|
<Helmet>
|
||||||
|
<title>{intl.formatMessage(messages.title)}</title>
|
||||||
|
<meta name='robots' content='noindex' />
|
||||||
|
</Helmet>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NotificationRequests.propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationRequests;
|
|
@ -25,18 +25,22 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange, intl } = this.props;
|
const { settings, onChange, intl } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
{!settings.getIn(['other', 'onlyRemote']) && <SettingToggle settings={settings} settingPath={['other', 'allowLocalOnly']} onChange={onChange} label={<FormattedMessage id='community.column_settings.allow_local_only' defaultMessage='Show local-only toots' />} />}
|
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
|
||||||
</div>
|
{!settings.getIn(['other', 'onlyRemote']) && <SettingToggle settings={settings} settingPath={['other', 'allowLocalOnly']} onChange={onChange} label={<FormattedMessage id='community.column_settings.allow_local_only' defaultMessage='Show local-only toots' />} />}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<section>
|
||||||
|
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormattedDate } from 'react-intl';
|
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
@ -8,14 +8,10 @@ import { Link, withRouter } from 'react-router-dom';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|
||||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
|
||||||
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
|
||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||||
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
|
||||||
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
|
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
|
||||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
|
||||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
|
||||||
import PollContainer from 'flavours/glitch/containers/poll_container';
|
import PollContainer from 'flavours/glitch/containers/poll_container';
|
||||||
|
@ -140,10 +136,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
let applicationLink = '';
|
let applicationLink = '';
|
||||||
let reblogLink = '';
|
let reblogLink = '';
|
||||||
const reblogIcon = 'retweet';
|
|
||||||
const reblogIconComponent = RepeatIcon;
|
|
||||||
let favouriteLink = '';
|
let favouriteLink = '';
|
||||||
let edited = '';
|
|
||||||
|
|
||||||
// Depending on user settings, some media are considered as parts of the
|
// Depending on user settings, some media are considered as parts of the
|
||||||
// contents (affected by CW) while other will be displayed outside of the
|
// contents (affected by CW) while other will be displayed outside of the
|
||||||
|
@ -246,68 +239,53 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('application')) {
|
if (status.get('application')) {
|
||||||
applicationLink = <> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibilityLink = <> · <VisibilityIcon visibility={status.get('visibility')} /></>;
|
const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
|
||||||
|
|
||||||
if (!['unlisted', 'public'].includes(status.get('visibility'))) {
|
if (!['unlisted', 'public'].includes(status.get('visibility'))) {
|
||||||
reblogLink = null;
|
reblogLink = null;
|
||||||
} else if (this.props.history) {
|
} else if (this.props.history) {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
||||||
{' · '}
|
<span className='detailed-status__reblogs'>
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
</span>
|
||||||
<span className='detailed-status__reblogs'>
|
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
</Link>
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
reblogLink = (
|
reblogLink = (
|
||||||
<>
|
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
{' · '}
|
<span className='detailed-status__reblogs'>
|
||||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
<Icon id={reblogIcon} icon={reblogIconComponent} />
|
</span>
|
||||||
<span className='detailed-status__reblogs'>
|
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
</a>
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.history) {
|
if (this.props.history) {
|
||||||
favouriteLink = (
|
favouriteLink = (
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
||||||
<Icon id='star' icon={StarIcon} />
|
|
||||||
<span className='detailed-status__favorites'>
|
<span className='detailed-status__favorites'>
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
</span>
|
</span>
|
||||||
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
favouriteLink = (
|
favouriteLink = (
|
||||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
||||||
<Icon id='star' icon={StarIcon} />
|
|
||||||
<span className='detailed-status__favorites'>
|
<span className='detailed-status__favorites'>
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
</span>
|
</span>
|
||||||
|
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('edited_at')) {
|
|
||||||
edited = (
|
|
||||||
<>
|
|
||||||
{' · '}
|
|
||||||
<EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
||||||
contentMedia.push(hashtagBar);
|
contentMedia.push(hashtagBar);
|
||||||
|
|
||||||
|
@ -345,9 +323,23 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
<div className='detailed-status__meta'>
|
||||||
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
<div className='detailed-status__meta__line'>
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
|
||||||
</a>{edited}{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
|
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{visibilityLink}
|
||||||
|
|
||||||
|
{applicationLink}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
|
||||||
|
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
{reblogLink}
|
||||||
|
·
|
||||||
|
{favouriteLink}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,27 +1,30 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { useRouteMatch, NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { Icon } from 'flavours/glitch/components/icon';
|
import { Icon } from 'flavours/glitch/components/icon';
|
||||||
|
|
||||||
const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
|
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
|
||||||
|
const match = useRouteMatch(to);
|
||||||
const className = classNames('column-link', { 'column-link--transparent': transparent });
|
const className = classNames('column-link', { 'column-link--transparent': transparent });
|
||||||
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
||||||
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
|
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
|
||||||
|
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
|
||||||
|
const active = match?.isExact;
|
||||||
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<a href={href} className={className} data-method={method} title={text} {...other}>
|
<a href={href} className={className} data-method={method} title={text} {...other}>
|
||||||
{iconElement}
|
{active ? activeIconElement : iconElement}
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
{badgeElement}
|
{badgeElement}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
} else if (to) {
|
} else if (to) {
|
||||||
return (
|
return (
|
||||||
<NavLink to={to} className={className} title={text} {...other}>
|
<NavLink to={to} className={className} title={text} exact {...other}>
|
||||||
{iconElement}
|
{active ? activeIconElement : iconElement}
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
{badgeElement}
|
{badgeElement}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
@ -46,6 +49,8 @@ const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badg
|
||||||
ColumnLink.propTypes = {
|
ColumnLink.propTypes = {
|
||||||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||||
iconComponent: PropTypes.func,
|
iconComponent: PropTypes.func,
|
||||||
|
activeIcon: PropTypes.node,
|
||||||
|
activeIconComponent: PropTypes.func,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
to: PropTypes.string,
|
to: PropTypes.string,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Component } from 'react';
|
|
||||||
|
|
||||||
import { injectIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
|
||||||
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
|
||||||
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
|
||||||
import ColumnLink from 'flavours/glitch/features/ui/components/column_link';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
text: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
|
|
||||||
});
|
|
||||||
|
|
||||||
class FollowRequestsColumnLink extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
count: PropTypes.number.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
|
|
||||||
dispatch(fetchFollowRequests());
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { count, intl } = this.props;
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ColumnLink
|
|
||||||
transparent
|
|
||||||
to='/follow_requests'
|
|
||||||
icon={<IconWithBadge className='column-link__icon' id='user-plus' icon={PersonAddIcon} count={count} />}
|
|
||||||
text={intl.formatMessage(messages.text)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps)(FollowRequestsColumnLink));
|
|
|
@ -1,10 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
|
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||||
|
|
||||||
|
@ -18,40 +17,25 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
export const ListPanel = () => {
|
||||||
lists: getOrderedLists(state),
|
const dispatch = useDispatch();
|
||||||
});
|
const lists = useSelector(state => getOrderedLists(state));
|
||||||
|
|
||||||
class ListPanel extends ImmutablePureComponent {
|
useEffect(() => {
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
dispatch: PropTypes.func.isRequired,
|
|
||||||
lists: ImmutablePropTypes.list,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(fetchLists());
|
dispatch(fetchLists());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!lists || lists.isEmpty()) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
return (
|
||||||
const { lists } = this.props;
|
<div className='list-panel'>
|
||||||
|
<hr />
|
||||||
|
|
||||||
if (!lists || lists.isEmpty()) {
|
{lists.map(list => (
|
||||||
return null;
|
<ColumnLink icon='list-ul' key={list.get('id')} iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
|
||||||
}
|
))}
|
||||||
|
</div>
|
||||||
return (
|
);
|
||||||
<div className='list-panel'>
|
};
|
||||||
<hr />
|
|
||||||
|
|
||||||
{lists.map(list => (
|
|
||||||
<ColumnLink icon='list-ul' iconComponent={ListAltIcon} key={list.get('id')} strict text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(ListPanel);
|
|
||||||
|
|
|
@ -1,19 +1,33 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Component } from 'react';
|
import { Component, useEffect } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
|
import { useSelector, useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
|
||||||
|
import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
|
||||||
|
import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react';
|
||||||
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||||
|
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
|
||||||
|
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import MailActiveIcon from '@/material-icons/400-24px/mail-fill.svg?react';
|
||||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||||
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
|
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
|
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||||
|
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
|
||||||
|
import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
|
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
|
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||||
|
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||||
|
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
|
||||||
|
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
||||||
import { NavigationPortal } from 'flavours/glitch/components/navigation_portal';
|
import { NavigationPortal } from 'flavours/glitch/components/navigation_portal';
|
||||||
import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
|
import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
|
||||||
import { transientSingleColumn } from 'flavours/glitch/is_mobile';
|
import { transientSingleColumn } from 'flavours/glitch/is_mobile';
|
||||||
|
@ -21,9 +35,7 @@ import { preferencesLink } from 'flavours/glitch/utils/backend_links';
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
import ColumnLink from './column_link';
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
import DisabledAccountBanner from './disabled_account_banner';
|
||||||
import FollowRequestsColumnLink from './follow_requests_column_link';
|
import { ListPanel } from './list_panel';
|
||||||
import ListPanel from './list_panel';
|
|
||||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
|
||||||
import SignInBanner from './sign_in_banner';
|
import SignInBanner from './sign_in_banner';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -42,8 +54,48 @@ const messages = defineMessages({
|
||||||
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
||||||
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
|
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
|
||||||
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
|
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const NotificationsLink = () => {
|
||||||
|
const count = useSelector(state => state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0);
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/notifications'
|
||||||
|
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />}
|
||||||
|
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
|
||||||
|
text={intl.formatMessage(messages.notifications)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FollowRequestsLink = () => {
|
||||||
|
const count = useSelector(state => state.getIn(['user_lists', 'follow_requests', 'items'])?.size ?? 0);
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchFollowRequests());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/follow_requests'
|
||||||
|
icon={<IconWithBadge id='user-plus' icon={PersonAddIcon} count={count} className='column-link__icon' />}
|
||||||
|
activeIcon={<IconWithBadge id='user-plus' icon={PersonAddActiveIcon} count={count} className='column-link__icon' />}
|
||||||
|
text={intl.formatMessage(messages.followRequests)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
class NavigationPanel extends Component {
|
class NavigationPanel extends Component {
|
||||||
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
|
@ -84,9 +136,9 @@ class NavigationPanel extends Component {
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<>
|
||||||
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} text={intl.formatMessage(messages.home)} />
|
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
|
||||||
<ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
|
<NotificationsLink />
|
||||||
<FollowRequestsColumnLink />
|
<FollowRequestsLink />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -109,10 +161,10 @@ class NavigationPanel extends Component {
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<>
|
||||||
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} text={intl.formatMessage(messages.direct)} />
|
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} activeIconComponent={MailActiveIcon} text={intl.formatMessage(messages.direct)} />
|
||||||
<ColumnLink transparent to='/bookmarks' icon='bookmark' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} />
|
<ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
|
||||||
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} />
|
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
|
||||||
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} />
|
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
|
||||||
|
|
||||||
<ListPanel />
|
<ListPanel />
|
||||||
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
|
||||||
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
|
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
count: state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0,
|
|
||||||
id: 'bell',
|
|
||||||
icon: NotificationsIcon,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(IconWithBadge);
|
|
|
@ -50,6 +50,8 @@ import {
|
||||||
DirectTimeline,
|
DirectTimeline,
|
||||||
HashtagTimeline,
|
HashtagTimeline,
|
||||||
Notifications,
|
Notifications,
|
||||||
|
NotificationRequests,
|
||||||
|
NotificationRequest,
|
||||||
FollowRequests,
|
FollowRequests,
|
||||||
FavouritedStatuses,
|
FavouritedStatuses,
|
||||||
BookmarkedStatuses,
|
BookmarkedStatuses,
|
||||||
|
@ -83,7 +85,6 @@ const mapStateToProps = state => ({
|
||||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
isWide: state.getIn(['local_settings', 'stretch']),
|
||||||
dropdownMenuIsOpen: state.dropdownMenu.openId !== null,
|
|
||||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||||
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
||||||
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
||||||
|
@ -213,7 +214,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
|
||||||
|
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
|
||||||
|
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
|
||||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
|
||||||
|
@ -274,7 +277,6 @@ class UI extends PureComponent {
|
||||||
hasMediaAttachments: PropTypes.bool,
|
hasMediaAttachments: PropTypes.bool,
|
||||||
canUploadMore: PropTypes.bool,
|
canUploadMore: PropTypes.bool,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
dropdownMenuIsOpen: PropTypes.bool,
|
|
||||||
unreadNotifications: PropTypes.number,
|
unreadNotifications: PropTypes.number,
|
||||||
showFaviconBadge: PropTypes.bool,
|
showFaviconBadge: PropTypes.bool,
|
||||||
hicolorPrivacyIcons: PropTypes.bool,
|
hicolorPrivacyIcons: PropTypes.bool,
|
||||||
|
@ -600,7 +602,7 @@ class UI extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { draggingOver } = this.state;
|
const { draggingOver } = this.state;
|
||||||
const { children, isWide, location, dropdownMenuIsOpen, layout, moved } = this.props;
|
const { children, isWide, location, layout, moved } = this.props;
|
||||||
|
|
||||||
const className = classNames('ui', {
|
const className = classNames('ui', {
|
||||||
'wide': isWide,
|
'wide': isWide,
|
||||||
|
@ -632,7 +634,7 @@ class UI extends PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
|
||||||
<div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
<div className={className} ref={this.setRef}>
|
||||||
{moved && (<div className='flash-message alert'>
|
{moved && (<div className='flash-message alert'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='moved_to_warning'
|
id='moved_to_warning'
|
||||||
|
|
|
@ -201,3 +201,11 @@ export function About () {
|
||||||
export function PrivacyPolicy () {
|
export function PrivacyPolicy () {
|
||||||
return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'../../privacy_policy');
|
return import(/*webpackChunkName: "features/glitch/async/privacy_policy" */'../../privacy_policy');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function NotificationRequests () {
|
||||||
|
return import(/*webpackChunkName: "features/glitch/notifications/requests" */'../../notifications/requests');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationRequest () {
|
||||||
|
return import(/*webpackChunkName: "features/glitch/notifications/request" */'../../notifications/request');
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createContext, useContext, useMemo } from 'react';
|
||||||
|
|
||||||
|
export const SensitiveMediaContext = createContext<{
|
||||||
|
hideMediaByDefault: boolean;
|
||||||
|
}>({
|
||||||
|
hideMediaByDefault: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useSensitiveMediaContext() {
|
||||||
|
return useContext(SensitiveMediaContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextValue = React.ContextType<typeof SensitiveMediaContext>;
|
||||||
|
|
||||||
|
export const SensitiveMediaContextProvider: React.FC<
|
||||||
|
React.PropsWithChildren<{ hideMediaByDefault: boolean }>
|
||||||
|
> = ({ hideMediaByDefault, children }) => {
|
||||||
|
const contextValue = useMemo<ContextValue>(
|
||||||
|
() => ({ hideMediaByDefault }),
|
||||||
|
[hideMediaByDefault],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SensitiveMediaContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SensitiveMediaContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -66,6 +66,9 @@
|
||||||
"notification_purge.btn_invert": "Invert\nselection",
|
"notification_purge.btn_invert": "Invert\nselection",
|
||||||
"notification_purge.btn_none": "Select\nnone",
|
"notification_purge.btn_none": "Select\nnone",
|
||||||
"notification_purge.start": "Enter notification cleaning mode",
|
"notification_purge.start": "Enter notification cleaning mode",
|
||||||
|
"notifications.column_settings.filter_bar.advanced": "Display all categories",
|
||||||
|
"notifications.column_settings.filter_bar.category": "Quick filter bar",
|
||||||
|
"notifications.column_settings.filter_bar.show_bar": "Show filter bar",
|
||||||
"notifications.column_settings.reaction": "Reactions:",
|
"notifications.column_settings.reaction": "Reactions:",
|
||||||
"notifications.filter.reactions": "Reactions",
|
"notifications.filter.reactions": "Reactions",
|
||||||
"notifications.marked_clear": "Clear selected notifications",
|
"notifications.marked_clear": "Clear selected notifications",
|
||||||
|
|
|
@ -55,7 +55,7 @@ import {
|
||||||
import { REDRAFT } from '../actions/statuses';
|
import { REDRAFT } from '../actions/statuses';
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
import { TIMELINE_DELETE } from '../actions/timelines';
|
||||||
import { me, defaultContentType, pollLimits } from '../initial_state';
|
import { me, defaultContentType } from '../initial_state';
|
||||||
import { recoverHashtags } from '../utils/hashtag';
|
import { recoverHashtags } from '../utils/hashtag';
|
||||||
import { unescapeHTML } from '../utils/html';
|
import { unescapeHTML } from '../utils/html';
|
||||||
import { overwrite } from '../utils/js_helpers';
|
import { overwrite } from '../utils/js_helpers';
|
||||||
|
@ -358,12 +358,12 @@ const updateSuggestionTags = (state, token) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePoll = (state, index, value) => state.updateIn(['poll', 'options'], options => {
|
const updatePoll = (state, index, value, maxOptions) => state.updateIn(['poll', 'options'], options => {
|
||||||
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
||||||
|
|
||||||
if (tmp.size === 0) {
|
if (tmp.size === 0) {
|
||||||
return tmp.push('').push('');
|
return tmp.push('').push('');
|
||||||
} else if (tmp.size < pollLimits.max_options) {
|
} else if (tmp.size < maxOptions) {
|
||||||
return tmp.push('');
|
return tmp.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -653,7 +653,7 @@ export default function compose(state = initialState, action) {
|
||||||
case COMPOSE_POLL_REMOVE:
|
case COMPOSE_POLL_REMOVE:
|
||||||
return state.set('poll', null);
|
return state.set('poll', null);
|
||||||
case COMPOSE_POLL_OPTION_CHANGE:
|
case COMPOSE_POLL_OPTION_CHANGE:
|
||||||
return updatePoll(state, action.index, action.title);
|
return updatePoll(state, action.index, action.title, action.maxOptions);
|
||||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||||
case COMPOSE_LANGUAGE_CHANGE:
|
case COMPOSE_LANGUAGE_CHANGE:
|
||||||
|
|
|
@ -28,6 +28,8 @@ import media_attachments from './media_attachments';
|
||||||
import meta from './meta';
|
import meta from './meta';
|
||||||
import { modalReducer } from './modal';
|
import { modalReducer } from './modal';
|
||||||
import mutes from './mutes';
|
import mutes from './mutes';
|
||||||
|
import { notificationPolicyReducer } from './notification_policy';
|
||||||
|
import { notificationRequestsReducer } from './notification_requests';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
import picture_in_picture from './picture_in_picture';
|
import picture_in_picture from './picture_in_picture';
|
||||||
import pinnedAccountsEditor from './pinned_accounts_editor';
|
import pinnedAccountsEditor from './pinned_accounts_editor';
|
||||||
|
@ -88,6 +90,8 @@ const reducers = {
|
||||||
history,
|
history,
|
||||||
tags,
|
tags,
|
||||||
followed_tags,
|
followed_tags,
|
||||||
|
notificationPolicy: notificationPolicyReducer,
|
||||||
|
notificationRequests: notificationRequestsReducer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
// We want the root state to be an ImmutableRecord, which is an object with a defined list of keys,
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import { NOTIFICATION_POLICY_FETCH_SUCCESS } from 'flavours/glitch/actions/notifications';
|
||||||
|
|
||||||
|
export const notificationPolicyReducer = (state = null, action) => {
|
||||||
|
switch(action.type) {
|
||||||
|
case NOTIFICATION_POLICY_FETCH_SUCCESS:
|
||||||
|
return fromJS(action.policy);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NOTIFICATION_REQUESTS_EXPAND_REQUEST,
|
||||||
|
NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
|
||||||
|
NOTIFICATION_REQUESTS_EXPAND_FAIL,
|
||||||
|
NOTIFICATION_REQUESTS_FETCH_REQUEST,
|
||||||
|
NOTIFICATION_REQUESTS_FETCH_SUCCESS,
|
||||||
|
NOTIFICATION_REQUESTS_FETCH_FAIL,
|
||||||
|
NOTIFICATION_REQUEST_FETCH_REQUEST,
|
||||||
|
NOTIFICATION_REQUEST_FETCH_SUCCESS,
|
||||||
|
NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||||
|
NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||||
|
NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
|
||||||
|
NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
|
||||||
|
} from 'flavours/glitch/actions/notifications';
|
||||||
|
|
||||||
|
import { notificationToMap } from './notifications';
|
||||||
|
|
||||||
|
const initialState = ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
next: null,
|
||||||
|
current: ImmutableMap({
|
||||||
|
isLoading: false,
|
||||||
|
item: null,
|
||||||
|
removed: false,
|
||||||
|
notifications: ImmutableMap({
|
||||||
|
items: ImmutableList(),
|
||||||
|
isLoading: false,
|
||||||
|
next: null,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizeRequest = request => fromJS({
|
||||||
|
...request,
|
||||||
|
account: request.account.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeRequest = (state, id) => {
|
||||||
|
if (state.getIn(['current', 'item', 'id']) === id) {
|
||||||
|
state = state.setIn(['current', 'removed'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.update('items', list => list.filterNot(item => item.get('id') === id));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const notificationRequestsReducer = (state = initialState, action) => {
|
||||||
|
switch(action.type) {
|
||||||
|
case NOTIFICATION_REQUESTS_FETCH_SUCCESS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.update('items', list => ImmutableList(action.requests.map(normalizeRequest)).concat(list));
|
||||||
|
map.set('isLoading', false);
|
||||||
|
map.update('next', next => next ?? action.next);
|
||||||
|
});
|
||||||
|
case NOTIFICATION_REQUESTS_EXPAND_SUCCESS:
|
||||||
|
return state.withMutations(map => {
|
||||||
|
map.update('items', list => list.concat(ImmutableList(action.requests.map(normalizeRequest))));
|
||||||
|
map.set('isLoading', false);
|
||||||
|
map.set('next', action.next);
|
||||||
|
});
|
||||||
|
case NOTIFICATION_REQUESTS_EXPAND_REQUEST:
|
||||||
|
case NOTIFICATION_REQUESTS_FETCH_REQUEST:
|
||||||
|
return state.set('isLoading', true);
|
||||||
|
case NOTIFICATION_REQUESTS_EXPAND_FAIL:
|
||||||
|
case NOTIFICATION_REQUESTS_FETCH_FAIL:
|
||||||
|
return state.set('isLoading', false);
|
||||||
|
case NOTIFICATION_REQUEST_ACCEPT_REQUEST:
|
||||||
|
case NOTIFICATION_REQUEST_DISMISS_REQUEST:
|
||||||
|
return removeRequest(state, action.id);
|
||||||
|
case NOTIFICATION_REQUEST_FETCH_REQUEST:
|
||||||
|
return state.set('current', initialState.get('current').set('isLoading', true));
|
||||||
|
case NOTIFICATION_REQUEST_FETCH_SUCCESS:
|
||||||
|
return state.update('current', map => map.set('isLoading', false).set('item', normalizeRequest(action.request)));
|
||||||
|
case NOTIFICATION_REQUEST_FETCH_FAIL:
|
||||||
|
return state.update('current', map => map.set('isLoading', false));
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST:
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST:
|
||||||
|
return state.setIn(['current', 'notifications', 'isLoading'], true);
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS:
|
||||||
|
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => ImmutableList(action.notifications.map(notificationToMap)).concat(list)).update('next', next => next ?? action.next));
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS:
|
||||||
|
return state.updateIn(['current', 'notifications'], map => map.set('isLoading', false).update('items', list => list.concat(ImmutableList(action.notifications.map(notificationToMap)))).set('next', action.next));
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL:
|
||||||
|
case NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL:
|
||||||
|
return state.setIn(['current', 'notifications', 'isLoading'], false);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
|
@ -54,7 +54,7 @@ const initialState = ImmutableMap({
|
||||||
markNewForDelete: false,
|
markNewForDelete: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = (notification, markForDelete) => ImmutableMap({
|
export const notificationToMap = (notification, markForDelete = false) => ImmutableMap({
|
||||||
id: notification.id,
|
id: notification.id,
|
||||||
type: notification.type,
|
type: notification.type,
|
||||||
account: notification.account.id,
|
account: notification.account.id,
|
||||||
|
|
|
@ -26,7 +26,7 @@ $fluid-breakpoint: $maximum-width + 20px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
padding: 1em 1.75em;
|
padding: 1em 1.75em;
|
||||||
padding-inline-start: 3em;
|
padding-inline-start: 3em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -53,4 +53,10 @@ $fluid-breakpoint: $maximum-width + 20px;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1399,8 +1399,8 @@ a.sparkline {
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
background: $ui-base-color;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
|
||||||
&__permalink {
|
&__permalink {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: $font-sans-serif, sans-serif;
|
font-family: $font-sans-serif, sans-serif;
|
||||||
background: darken($ui-base-color, 8%);
|
background: var(--background-color);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1315,6 +1315,12 @@ code {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
.recommended {
|
.recommended {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
|
|
|
@ -21,25 +21,6 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Change default background colors of columns
|
// Change default background colors of columns
|
||||||
.column > .scrollable,
|
|
||||||
.getting-started,
|
|
||||||
.column-inline-form,
|
|
||||||
.regeneration-indicator {
|
|
||||||
background: $white;
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-column {
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.column > .scrollable.about {
|
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.about__meta,
|
|
||||||
.about__section__title,
|
|
||||||
.interaction-modal {
|
.interaction-modal {
|
||||||
background: $white;
|
background: $white;
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
border: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -53,37 +34,10 @@ html {
|
||||||
background: lighten($ui-base-color, 12%);
|
background: lighten($ui-base-color, 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-form {
|
|
||||||
background: $white;
|
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-back-button,
|
|
||||||
.column-header {
|
|
||||||
background: $white;
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
|
|
||||||
@media screen and (max-width: $no-gap-breakpoint) {
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--slim-button {
|
|
||||||
top: -50px;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-header__back-button,
|
|
||||||
.column-header__button,
|
|
||||||
.column-header__button.active,
|
|
||||||
.account__header {
|
.account__header {
|
||||||
background: $white;
|
background: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-header {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-header__button.active {
|
.column-header__button.active {
|
||||||
color: $ui-highlight-color;
|
color: $ui-highlight-color;
|
||||||
|
|
||||||
|
@ -91,7 +45,6 @@ html {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $ui-highlight-color;
|
color: $ui-highlight-color;
|
||||||
background: $white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,25 +70,6 @@ html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.column-subheading {
|
|
||||||
background: darken($ui-base-color, 4%);
|
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.getting-started,
|
|
||||||
.scrollable {
|
|
||||||
.column-link {
|
|
||||||
background: $white;
|
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
background: $ui-base-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.getting-started .navigation-bar {
|
.getting-started .navigation-bar {
|
||||||
border-top: 1px solid lighten($ui-base-color, 8%);
|
border-top: 1px solid lighten($ui-base-color, 8%);
|
||||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||||
|
@ -168,23 +102,6 @@ html {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification__filter-bar {
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
border-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__header,
|
|
||||||
.drawer__inner {
|
|
||||||
background: $white;
|
|
||||||
border: 1px solid lighten($ui-base-color, 8%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.drawer__inner__mastodon {
|
|
||||||
background: $white
|
|
||||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>')
|
|
||||||
no-repeat bottom / 100% auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-progress__backdrop {
|
.upload-progress__backdrop {
|
||||||
background: $ui-base-color;
|
background: $ui-base-color;
|
||||||
}
|
}
|
||||||
|
@ -194,11 +111,6 @@ html {
|
||||||
background: lighten($white, 4%);
|
background: lighten($white, 4%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.detailed-status,
|
|
||||||
.detailed-status__action-bar {
|
|
||||||
background: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the background colors of status__content__spoiler-link
|
// Change the background colors of status__content__spoiler-link
|
||||||
.reply-indicator__content .status__content__spoiler-link,
|
.reply-indicator__content .status__content__spoiler-link,
|
||||||
.status__content .status__content__spoiler-link {
|
.status__content .status__content__spoiler-link {
|
||||||
|
@ -351,11 +263,11 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-track {
|
.react-toggle-track {
|
||||||
background: $ui-secondary-color;
|
background: $ui-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
|
.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track {
|
||||||
background: darken($ui-secondary-color, 10%);
|
background: lighten($ui-primary-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled)
|
.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled)
|
||||||
|
|
|
@ -59,4 +59,8 @@ $emojis-requiring-inversion: 'chains';
|
||||||
.theme-mastodon-light {
|
.theme-mastodon-light {
|
||||||
--dropdown-border-color: #d9e1e8;
|
--dropdown-border-color: #d9e1e8;
|
||||||
--dropdown-background-color: #fff;
|
--dropdown-background-color: #fff;
|
||||||
|
--background-border-color: #d9e1e8;
|
||||||
|
--background-color: #fff;
|
||||||
|
--background-color-tint: rgba(255, 255, 255, 90%);
|
||||||
|
--background-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,10 +100,15 @@ $ui-avatar-border-size: 8%;
|
||||||
$dismiss-overlay-width: 4rem;
|
$dismiss-overlay-width: 4rem;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--dropdown-border-color: #{lighten($ui-base-color, 12%)};
|
--dropdown-border-color: #{lighten($ui-base-color, 4%)};
|
||||||
--dropdown-background-color: #{lighten($ui-base-color, 4%)};
|
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||||
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
|
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
|
||||||
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
|
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
|
||||||
--modal-background-color: #{darken($ui-base-color, 4%)};
|
--modal-background-color: #{darken($ui-base-color, 4%)};
|
||||||
--modal-border-color: #{lighten($ui-base-color, 4%)};
|
--modal-border-color: #{lighten($ui-base-color, 4%)};
|
||||||
|
--background-border-color: #{lighten($ui-base-color, 4%)};
|
||||||
|
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||||
|
--background-color: #{darken($ui-base-color, 8%)};
|
||||||
|
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||||
|
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import Rails from '@rails/ujs';
|
|
||||||
|
|
||||||
import { signOutLink } from 'flavours/glitch/utils/backend_links';
|
import { signOutLink } from 'flavours/glitch/utils/backend_links';
|
||||||
|
|
||||||
export const logOut = () => {
|
export const logOut = () => {
|
||||||
|
@ -11,13 +9,18 @@ export const logOut = () => {
|
||||||
methodInput.setAttribute('type', 'hidden');
|
methodInput.setAttribute('type', 'hidden');
|
||||||
form.appendChild(methodInput);
|
form.appendChild(methodInput);
|
||||||
|
|
||||||
const csrfToken = Rails.csrfToken();
|
const csrfToken = document.querySelector<HTMLMetaElement>(
|
||||||
const csrfParam = Rails.csrfParam();
|
'meta[name=csrf-token]',
|
||||||
|
);
|
||||||
|
|
||||||
|
const csrfParam = document.querySelector<HTMLMetaElement>(
|
||||||
|
'meta[name=csrf-param]',
|
||||||
|
);
|
||||||
|
|
||||||
if (csrfParam && csrfToken) {
|
if (csrfParam && csrfToken) {
|
||||||
const csrfInput = document.createElement('input');
|
const csrfInput = document.createElement('input');
|
||||||
csrfInput.setAttribute('name', csrfParam);
|
csrfInput.setAttribute('name', csrfParam.content);
|
||||||
csrfInput.setAttribute('value', csrfToken);
|
csrfInput.setAttribute('value', csrfToken.content);
|
||||||
csrfInput.setAttribute('type', 'hidden');
|
csrfInput.setAttribute('type', 'hidden');
|
||||||
form.appendChild(csrfInput);
|
form.appendChild(csrfInput);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,3 +69,11 @@ export function pluralReady(
|
||||||
export function roundTo10(num: number): number {
|
export function roundTo10(num: number): number {
|
||||||
return Math.round(num * 0.1) / 0.1;
|
return Math.round(num * 0.1) / 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toCappedNumber(num: string): string {
|
||||||
|
if (parseInt(num) > 99) {
|
||||||
|
return '99+';
|
||||||
|
} else {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -788,11 +788,12 @@ export function addPollOption(title) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changePollOption(index, title) {
|
export function changePollOption(index, title, maxOptions) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||||
index,
|
index,
|
||||||
title,
|
title,
|
||||||
|
maxOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
|
||||||
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
|
||||||
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
|
||||||
|
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
|
||||||
|
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
|
||||||
|
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
|
||||||
|
|
||||||
defineMessages({
|
defineMessages({
|
||||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||||
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
|
||||||
|
@ -313,3 +345,270 @@ export function setBrowserPermission (value) {
|
||||||
value,
|
value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const fetchNotificationPolicy = () => (dispatch, getState) => {
|
||||||
|
dispatch(fetchNotificationPolicyRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationPolicySuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationPolicyFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationPolicyRequest = () => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationPolicySuccess = policy => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
|
||||||
|
policy,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationPolicyFail = error => ({
|
||||||
|
type: NOTIFICATION_POLICY_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateNotificationsPolicy = params => (dispatch, getState) => {
|
||||||
|
dispatch(fetchNotificationPolicyRequest());
|
||||||
|
|
||||||
|
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationPolicySuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationPolicyFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequests = () => (dispatch, getState) => {
|
||||||
|
const params = {};
|
||||||
|
|
||||||
|
if (getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
|
||||||
|
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationRequestsRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
|
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationRequestsFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsRequest = () => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsSuccess = (requests, next) => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
|
||||||
|
requests,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestsFail = error => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequests = () => (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['notificationRequests', 'next']);
|
||||||
|
|
||||||
|
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandNotificationRequestsRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||||
|
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(expandNotificationRequestsFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandNotificationRequestsRequest = () => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequestsSuccess = (requests, next) => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
|
||||||
|
requests,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationRequestsFail = error => ({
|
||||||
|
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
const current = getState().getIn(['notificationRequests', 'current']);
|
||||||
|
|
||||||
|
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
|
||||||
|
dispatch(fetchNotificationRequestSuccess(data));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestSuccess = request => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_FETCH_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
dispatch(acceptNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
|
||||||
|
dispatch(acceptNotificationRequestSuccess(id));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(acceptNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const acceptNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequestSuccess = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequest = id => (dispatch, getState) => {
|
||||||
|
dispatch(dismissNotificationRequestRequest(id));
|
||||||
|
|
||||||
|
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
|
||||||
|
dispatch(dismissNotificationRequestSuccess(id));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(dismissNotificationRequestFail(id, err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dismissNotificationRequestRequest = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequestSuccess = id => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dismissNotificationRequestFail = (id, error) => ({
|
||||||
|
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
|
||||||
|
const current = getState().getIn(['notificationRequests', 'current']);
|
||||||
|
const params = { account_id: accountId };
|
||||||
|
|
||||||
|
if (current.getIn(['item', 'account']) === accountId) {
|
||||||
|
if (current.getIn(['notifications', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.getIn(['notifications', 'items'])?.size > 0) {
|
||||||
|
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(fetchNotificationsForRequestRequest());
|
||||||
|
|
||||||
|
api(getState).get('/api/v1/notifications', { params }).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
|
dispatch(fetchNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(fetchNotificationsForRequestFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestRequest = () => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestSuccess = (notifications, next) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS,
|
||||||
|
notifications,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fetchNotificationsForRequestFail = (error) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequest = () => (dispatch, getState) => {
|
||||||
|
const url = getState().getIn(['notificationRequests', 'current', 'notifications', 'next']);
|
||||||
|
|
||||||
|
if (!url || getState().getIn(['notificationRequests', 'current', 'notifications', 'isLoading'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(expandNotificationsForRequestRequest());
|
||||||
|
|
||||||
|
api(getState).get(url).then(response => {
|
||||||
|
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||||
|
dispatch(importFetchedAccounts(response.data.map(item => item.account)));
|
||||||
|
dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||||
|
dispatch(importFetchedAccounts(response.data.filter(item => item.report).map(item => item.report.target_account)));
|
||||||
|
|
||||||
|
dispatch(expandNotificationsForRequestSuccess(response.data, next?.uri));
|
||||||
|
}).catch(err => {
|
||||||
|
dispatch(expandNotificationsForRequestFail(err));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestRequest = () => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestSuccess = (notifications, next) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS,
|
||||||
|
notifications,
|
||||||
|
next,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const expandNotificationsForRequestFail = (error) => ({
|
||||||
|
type: NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent, useCallback } from 'react';
|
import { PureComponent, useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
@ -11,7 +11,7 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
|
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { ButtonInTabsBar, useColumnsContext } from 'mastodon/features/ui/util/columns_context';
|
import { ButtonInTabsBar, useColumnsContext } from 'mastodon/features/ui/util/columns_context';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
@ -23,10 +23,12 @@ const messages = defineMessages({
|
||||||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||||
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
|
||||||
|
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const BackButton = ({ pinned, show }) => {
|
const BackButton = ({ pinned, show, onlyIcon }) => {
|
||||||
const history = useAppHistory();
|
const history = useAppHistory();
|
||||||
|
const intl = useIntl();
|
||||||
const { multiColumn } = useColumnsContext();
|
const { multiColumn } = useColumnsContext();
|
||||||
|
|
||||||
const handleBackClick = useCallback(() => {
|
const handleBackClick = useCallback(() => {
|
||||||
|
@ -39,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
|
||||||
|
|
||||||
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
||||||
|
|
||||||
if(!showButton) return null;
|
if (!showButton) return null;
|
||||||
|
|
||||||
return (<button onClick={handleBackClick} className='column-header__back-button'>
|
|
||||||
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
|
||||||
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
|
|
||||||
</button>);
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
|
||||||
|
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
|
||||||
|
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
BackButton.propTypes = {
|
BackButton.propTypes = {
|
||||||
pinned: PropTypes.bool,
|
pinned: PropTypes.bool,
|
||||||
show: PropTypes.bool,
|
show: PropTypes.bool,
|
||||||
|
onlyIcon: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
class ColumnHeader extends PureComponent {
|
class ColumnHeader extends PureComponent {
|
||||||
|
@ -145,27 +149,31 @@ class ColumnHeader extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multiColumn && pinned) {
|
if (multiColumn && pinned) {
|
||||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
|
||||||
|
|
||||||
moveButtons = (
|
moveButtons = (
|
||||||
<div key='move-buttons' className='column-header__setting-arrows'>
|
<div className='column-header__setting-arrows'>
|
||||||
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
|
||||||
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (multiColumn && this.props.onPin) {
|
} else if (multiColumn && this.props.onPin) {
|
||||||
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
backButton = <BackButton pinned={pinned} show={showBackButton} />;
|
backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
|
||||||
|
|
||||||
const collapsedContent = [
|
const collapsedContent = [
|
||||||
extraContent,
|
extraContent,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (multiColumn) {
|
if (multiColumn) {
|
||||||
collapsedContent.push(pinButton);
|
collapsedContent.push(
|
||||||
collapsedContent.push(moveButtons);
|
<div key='buttons' className='column-header__advanced-buttons'>
|
||||||
|
{pinButton}
|
||||||
|
{moveButtons}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||||
|
@ -177,7 +185,7 @@ class ColumnHeader extends PureComponent {
|
||||||
onClick={this.handleToggleClick}
|
onClick={this.handleToggleClick}
|
||||||
>
|
>
|
||||||
<i className='icon-with-badge'>
|
<i className='icon-with-badge'>
|
||||||
<Icon id='sliders' icon={TuneIcon} />
|
<Icon id='sliders' icon={SettingsIcon} />
|
||||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||||
</i>
|
</i>
|
||||||
</button>
|
</button>
|
||||||
|
@ -190,16 +198,19 @@ class ColumnHeader extends PureComponent {
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName}>
|
||||||
<h1 className={buttonClassName}>
|
<h1 className={buttonClassName}>
|
||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<button onClick={this.handleTitleClick}>
|
<>
|
||||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
{showBackButton && backButton}
|
||||||
{title}
|
|
||||||
</button>
|
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||||
|
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasTitle && backButton}
|
{!hasTitle && showBackButton && backButton}
|
||||||
|
|
||||||
<div className='column-header__buttons'>
|
<div className='column-header__buttons'>
|
||||||
{hasTitle && backButton}
|
|
||||||
{extraButton}
|
{extraButton}
|
||||||
{collapseButton}
|
{collapseButton}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import Overlay from 'react-overlays/Overlay';
|
import Overlay from 'react-overlays/Overlay';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import { CircularProgress } from 'mastodon/components/circular_progress';
|
import { CircularProgress } from 'mastodon/components/circular_progress';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
|
@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
|
||||||
}) : (
|
}) : (
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={!open ? icon : 'close'}
|
icon={!open ? icon : 'close'}
|
||||||
iconComponent={!open ? iconComponent : CloseIcon}
|
iconComponent={iconComponent}
|
||||||
title={title}
|
title={title}
|
||||||
active={open}
|
active={open}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import InlineAccount from 'mastodon/components/inline_account';
|
import InlineAccount from 'mastodon/components/inline_account';
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ class EditedTimestamp extends PureComponent {
|
||||||
return (
|
return (
|
||||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||||
<button className='dropdown-menu__text-button'>
|
<button className='dropdown-menu__text-button'>
|
||||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) }} /> <Icon id='caret-down' icon={ArrowDropDownIcon} />
|
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
|
||||||
</button>
|
</button>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -22,6 +22,7 @@ import Card from '../features/status/components/card';
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
|
||||||
|
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
|
||||||
import { displayMedia } from '../initial_state';
|
import { displayMedia } from '../initial_state';
|
||||||
|
|
||||||
import { Avatar } from './avatar';
|
import { Avatar } from './avatar';
|
||||||
|
@ -78,6 +79,8 @@ const messages = defineMessages({
|
||||||
|
|
||||||
class Status extends ImmutablePureComponent {
|
class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static contextType = SensitiveMediaContext;
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
account: ImmutablePropTypes.record,
|
account: ImmutablePropTypes.record,
|
||||||
|
@ -133,19 +136,21 @@ class Status extends ImmutablePureComponent {
|
||||||
];
|
];
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status),
|
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
|
||||||
statusId: undefined,
|
|
||||||
forceFilter: undefined,
|
forceFilter: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
static getDerivedStateFromProps(nextProps, prevState) {
|
componentDidUpdate (prevProps) {
|
||||||
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
|
// This will potentially cause a wasteful redraw, but in most cases `Status` components are used
|
||||||
return {
|
// with a `key` directly depending on their `id`, preventing re-use of the component across
|
||||||
showMedia: defaultMediaVisibility(nextProps.status),
|
// different IDs.
|
||||||
statusId: nextProps.status.get('id'),
|
// But just in case this does change, reset the state on status change.
|
||||||
};
|
|
||||||
} else {
|
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
|
||||||
return null;
|
this.setState({
|
||||||
|
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
|
||||||
|
forceFilter: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -170,7 +170,8 @@ class About extends PureComponent {
|
||||||
<ol className='rules-list'>
|
<ol className='rules-list'>
|
||||||
{server.get('rules').map(rule => (
|
{server.get('rules').map(rule => (
|
||||||
<li key={rule.get('id')}>
|
<li key={rule.get('id')}>
|
||||||
<span className='rules-list__text'>{rule.get('text')}</span>
|
<div className='rules-list__text'>{rule.get('text')}</div>
|
||||||
|
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -188,18 +189,20 @@ class About extends PureComponent {
|
||||||
<>
|
<>
|
||||||
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
|
||||||
|
|
||||||
<div className='about__domain-blocks'>
|
{domainBlocks.get('items').size > 0 && (
|
||||||
{domainBlocks.get('items').map(block => (
|
<div className='about__domain-blocks'>
|
||||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
{domainBlocks.get('items').map(block => (
|
||||||
<div className='about__domain-blocks__domain__header'>
|
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||||
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
<div className='about__domain-blocks__domain__header'>
|
||||||
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||||
</div>
|
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { CopyIconButton } from 'mastodon/components/copy_icon_button';
|
||||||
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
|
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||||
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
|
import { autoPlayGif, me, domain } from 'mastodon/initial_state';
|
||||||
|
@ -289,7 +290,7 @@ class Header extends ImmutablePureComponent {
|
||||||
|
|
||||||
if (me !== account.get('id')) {
|
if (me !== account.get('id')) {
|
||||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||||
actionBtn = '';
|
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
||||||
} else if (account.getIn(['relationship', 'requested'])) {
|
} else if (account.getIn(['relationship', 'requested'])) {
|
||||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||||
|
@ -426,15 +427,10 @@ class Header extends ImmutablePureComponent {
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className='account__header__tabs__buttons'>
|
<div className='account__header__tabs__buttons'>
|
||||||
{!hidden && (
|
{!hidden && bellBtn}
|
||||||
<>
|
{!hidden && shareBtn}
|
||||||
{actionBtn}
|
|
||||||
{bellBtn}
|
|
||||||
{shareBtn}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||||
|
{!hidden && actionBtn}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange } = this.props;
|
const { settings, onChange } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<div className='column-settings__row'>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,10 +58,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||||
|
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
|
||||||
|
|
||||||
const handleChange = useCallback(({ target: { value } }) => {
|
const handleChange = useCallback(({ target: { value } }) => {
|
||||||
dispatch(changePollOption(index, value));
|
dispatch(changePollOption(index, value, maxOptions));
|
||||||
}, [dispatch, index]);
|
}, [dispatch, index, maxOptions]);
|
||||||
|
|
||||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||||
dispatch(fetchComposeSuggestions(token));
|
dispatch(fetchComposeSuggestions(token));
|
||||||
|
|
|
@ -7,7 +7,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { LoadMore } from 'mastodon/components/load_more';
|
import { LoadMore } from 'mastodon/components/load_more';
|
||||||
|
@ -76,11 +75,6 @@ class SearchResults extends ImmutablePureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='search-results'>
|
<div className='search-results'>
|
||||||
<div className='search-results__header'>
|
|
||||||
<Icon id='search' icon={SearchIcon} />
|
|
||||||
<FormattedMessage id='explore.search_results' defaultMessage='Search results' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{accounts}
|
{accounts}
|
||||||
{hashtags}
|
{hashtags}
|
||||||
{statuses}
|
{statuses}
|
||||||
|
|
|
@ -42,15 +42,17 @@ const ColumnSettings = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle
|
<div className='column-settings__row'>
|
||||||
settings={settings}
|
<SettingToggle
|
||||||
settingPath={['onlyMedia']}
|
settings={settings}
|
||||||
onChange={onChange}
|
settingPath={['onlyMedia']}
|
||||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
onChange={onChange}
|
||||||
/>
|
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -107,28 +107,28 @@ class ColumnSettings extends PureComponent {
|
||||||
const { settings, onChange } = this.props;
|
const { settings, onChange } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<div className='setting-toggle'>
|
<div className='column-settings__row'>
|
||||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||||
|
|
||||||
<span className='setting-toggle__label'>
|
<div className='setting-toggle'>
|
||||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||||
</span>
|
|
||||||
|
<span className='setting-toggle__label'>
|
||||||
|
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.state.open && (
|
{this.state.open && (
|
||||||
<div className='column-settings__hashtags'>
|
<div className='column-settings__hashtags'>
|
||||||
{this.modeSelect('any')}
|
{this.modeSelect('any')}
|
||||||
{this.modeSelect('all')}
|
{this.modeSelect('all')}
|
||||||
{this.modeSelect('none')}
|
{this.modeSelect('none')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
<div className='column-settings__row'>
|
|
||||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,43 +24,36 @@ export const ColumnSettings: React.FC = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column-settings'>
|
||||||
<span className='column-settings__section'>
|
<section>
|
||||||
<FormattedMessage
|
<div className='column-settings__row'>
|
||||||
id='home.column_settings.basic'
|
<SettingToggle
|
||||||
defaultMessage='Basic'
|
prefix='home_timeline'
|
||||||
/>
|
settings={settings}
|
||||||
</span>
|
settingPath={['shows', 'reblog']}
|
||||||
|
onChange={onChange}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='home.column_settings.show_reblogs'
|
||||||
|
defaultMessage='Show boosts'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
<SettingToggle
|
||||||
<SettingToggle
|
prefix='home_timeline'
|
||||||
prefix='home_timeline'
|
settings={settings}
|
||||||
settings={settings}
|
settingPath={['shows', 'reply']}
|
||||||
settingPath={['shows', 'reblog']}
|
onChange={onChange}
|
||||||
onChange={onChange}
|
label={
|
||||||
label={
|
<FormattedMessage
|
||||||
<FormattedMessage
|
id='home.column_settings.show_replies'
|
||||||
id='home.column_settings.show_reblogs'
|
defaultMessage='Show replies'
|
||||||
defaultMessage='Show boosts'
|
/>
|
||||||
/>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
<SettingToggle
|
|
||||||
prefix='home_timeline'
|
|
||||||
settings={settings}
|
|
||||||
settingPath={['shows', 'reply']}
|
|
||||||
onChange={onChange}
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id='home.column_settings.show_replies'
|
|
||||||
defaultMessage='Show replies'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -193,35 +193,38 @@ class ListTimeline extends PureComponent {
|
||||||
pinned={pinned}
|
pinned={pinned}
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
>
|
>
|
||||||
<div className='column-settings__row column-header__links'>
|
<div className='column-settings'>
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
<section className='column-header__links'>
|
||||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||||
</button>
|
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||||
|
</button>
|
||||||
|
|
||||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div className='setting-toggle'>
|
<section>
|
||||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
<div className='setting-toggle'>
|
||||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||||
</label>
|
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||||
</div>
|
</label>
|
||||||
|
|
||||||
{ replies_policy !== undefined && (
|
|
||||||
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
|
|
||||||
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
|
|
||||||
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
|
|
||||||
</span>
|
|
||||||
<div className='column-settings__row'>
|
|
||||||
{ ['none', 'list', 'followed'].map(policy => (
|
|
||||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
)}
|
|
||||||
|
{replies_policy !== undefined && (
|
||||||
|
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||||
|
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||||
|
|
||||||
|
<div className='column-settings__row'>
|
||||||
|
{ ['none', 'list', 'followed'].map(policy => (
|
||||||
|
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
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,
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue