mirror of
https://git.bsd.gay/fef/nyastodon.git
synced 2024-12-26 16:03:42 +01:00
Merge branch 'glitch-soc' into develop
# Conflicts: # README.md # app/javascript/flavours/glitch/components/status.jsx # app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx # app/javascript/flavours/glitch/locales/en.json # app/javascript/flavours/glitch/reducers/compose.js # app/javascript/flavours/glitch/styles/components.scss # app/javascript/mastodon/reducers/compose.js # app/models/notification.rb
This commit is contained in:
commit
caccc2e0f4
613 changed files with 10843 additions and 5190 deletions
|
@ -123,7 +123,7 @@ module.exports = defineConfig({
|
|||
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
|
||||
'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/click-events-have-key-events': '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': [
|
||||
'error',
|
||||
'always',
|
||||
|
|
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
|
@ -53,7 +53,7 @@ jobs:
|
|||
|
||||
# Create or update the pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v6.0.0
|
||||
uses: peter-evans/create-pull-request@v6.0.1
|
||||
with:
|
||||
commit-message: 'New Crowdin translations'
|
||||
title: 'New Crowdin Translations (automated)'
|
||||
|
|
|
@ -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
|
||||
# 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=""
|
||||
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
|
||||
ARG MASTODON_VERSION_METADATA=""
|
||||
|
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
|
|||
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
|
||||
ARG RAILS_SERVE_STATIC_FILES="true"
|
||||
# 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"
|
||||
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
|
||||
ARG TZ="Etc/UTC"
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -112,7 +112,7 @@ group :test do
|
|||
# RSpec helpers for email specs
|
||||
gem 'email_spec'
|
||||
|
||||
# Extra RSpec extenion methods and helpers for sidekiq
|
||||
# Extra RSpec extension methods and helpers for sidekiq
|
||||
gem 'rspec-sidekiq', '~> 4.0'
|
||||
|
||||
# Browser integration testing
|
||||
|
|
17
Gemfile.lock
17
Gemfile.lock
|
@ -357,7 +357,7 @@ GEM
|
|||
jmespath (1.6.2)
|
||||
json (2.7.1)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.15.3)
|
||||
json-jwt (1.15.3.1)
|
||||
activesupport (>= 4.2)
|
||||
aes_key_wrap
|
||||
bindata
|
||||
|
@ -465,11 +465,11 @@ GEM
|
|||
statsd-ruby (~> 1.4, >= 1.4.0)
|
||||
oj (3.16.3)
|
||||
bigdecimal (>= 3.0)
|
||||
omniauth (2.1.1)
|
||||
omniauth (2.1.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 2.2.3)
|
||||
rack-protection
|
||||
omniauth-cas (3.0.0.beta.1)
|
||||
omniauth-cas (3.0.0)
|
||||
addressable (~> 2.8)
|
||||
nokogiri (~> 1.12)
|
||||
omniauth (~> 2.1)
|
||||
|
@ -505,7 +505,7 @@ GEM
|
|||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.5)
|
||||
pg (1.5.6)
|
||||
pghero (3.4.1)
|
||||
activerecord (>= 6)
|
||||
posix-spawn (0.3.15)
|
||||
|
@ -535,7 +535,7 @@ GEM
|
|||
rack (2.2.8.1)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.1)
|
||||
rack-cors (2.0.2)
|
||||
rack (>= 2.0.0)
|
||||
rack-oauth2 (1.21.3)
|
||||
activesupport
|
||||
|
@ -543,8 +543,9 @@ GEM
|
|||
httpclient
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (3.0.5)
|
||||
rack
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
rack-session (1.0.2)
|
||||
|
@ -743,7 +744,7 @@ GEM
|
|||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (1.0.1)
|
||||
climate_control
|
||||
test-prof (1.3.1)
|
||||
test-prof (1.3.2)
|
||||
thor (1.3.1)
|
||||
tilt (2.3.0)
|
||||
timeout (0.4.1)
|
||||
|
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
|||
|
||||
config.vm.post_up_message = <<MESSAGE
|
||||
To start server
|
||||
$ vagrant ssh -c "cd /vagrant && foreman start"
|
||||
$ vagrant ssh -c "cd /vagrant && bin/dev"
|
||||
MESSAGE
|
||||
|
||||
end
|
||||
|
|
|
@ -53,7 +53,7 @@ module Admin
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:rule).permit(:text, :priority)
|
||||
params.require(:rule).permit(:text, :hint, :priority)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -73,6 +73,14 @@ class Api::BaseController < ApplicationController
|
|||
|
||||
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)
|
||||
links = []
|
||||
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)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
def pagination_collection
|
||||
@statuses
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@accounts.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@accounts.first.id
|
||||
def pagination_collection
|
||||
@accounts
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@canonical_email_blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@canonical_email_blocks.first.id
|
||||
def pagination_collection
|
||||
@canonical_email_blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@domain_allows.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@domain_allows.first.id
|
||||
def pagination_collection
|
||||
@domain_allows
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@domain_blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@domain_blocks.first.id
|
||||
def pagination_collection
|
||||
@domain_blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@email_domain_blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@email_domain_blocks.first.id
|
||||
def pagination_collection
|
||||
@email_domain_blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@ip_blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@ip_blocks.first.id
|
||||
def pagination_collection
|
||||
@ip_blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@reports.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@reports.first.id
|
||||
def pagination_collection
|
||||
@reports
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@tags.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@tags.first.id
|
||||
def pagination_collection
|
||||
@tags
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@providers.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@providers.first.id
|
||||
def pagination_collection
|
||||
@providers
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
paginated_blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
paginated_blocks.first.id
|
||||
def pagination_collection
|
||||
paginated_blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
results.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
results.first.id
|
||||
def pagination_collection
|
||||
results
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@encrypted_messages.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@encrypted_messages.first.id
|
||||
def pagination_collection
|
||||
@encrypted_messages
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@blocks.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@blocks.first.id
|
||||
def pagination_collection
|
||||
@blocks
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@accounts.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@accounts.first.id
|
||||
def pagination_collection
|
||||
@accounts
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
results.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
results.first.id
|
||||
def pagination_collection
|
||||
results
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@results.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@results.first.id
|
||||
def pagination_collection
|
||||
@results
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@accounts.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@accounts.first.id
|
||||
def pagination_collection
|
||||
@accounts
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
paginated_mutes.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
paginated_mutes.first.id
|
||||
def pagination_collection
|
||||
paginated_mutes
|
||||
end
|
||||
|
||||
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(
|
||||
types: Array(browserable_params[: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
|
||||
|
||||
|
@ -78,19 +79,15 @@ class Api::V1::NotificationsController < Api::BaseController
|
|||
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@notifications.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@notifications.first.id
|
||||
def pagination_collection
|
||||
@notifications
|
||||
end
|
||||
|
||||
def browserable_params
|
||||
params.permit(:account_id, types: [], exclude_types: [])
|
||||
params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -63,11 +63,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
|
|||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
def pagination_collection
|
||||
@statuses
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,12 +9,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
|
|||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
def pagination_collection
|
||||
@statuses
|
||||
end
|
||||
|
||||
def next_path_params
|
||||
|
|
|
@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def use_seamless_external_login?
|
||||
|
|
|
@ -214,7 +214,7 @@ module ApplicationHelper
|
|||
state_params[:moved_to_account] = current_account.moved_to_account
|
||||
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
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
|
|
|
@ -109,6 +109,7 @@ module LanguagesHelper
|
|||
mn: ['Mongolian', 'Монгол хэл'].freeze,
|
||||
mr: ['Marathi', 'मराठी'].freeze,
|
||||
ms: ['Malay', 'Bahasa Melayu'].freeze,
|
||||
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
|
||||
mt: ['Maltese', 'Malti'].freeze,
|
||||
my: ['Burmese', 'ဗမာစာ'].freeze,
|
||||
na: ['Nauru', 'Ekakairũ Naoero'].freeze,
|
||||
|
@ -127,7 +128,7 @@ module LanguagesHelper
|
|||
om: ['Oromo', 'Afaan Oromoo'].freeze,
|
||||
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
|
||||
os: ['Ossetian', 'ирон æвзаг'].freeze,
|
||||
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze,
|
||||
pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze,
|
||||
pi: ['Pāli', 'पाऴि'].freeze,
|
||||
pl: ['Polish', 'Polski'].freeze,
|
||||
ps: ['Pashto', 'پښتو'].freeze,
|
||||
|
@ -191,15 +192,20 @@ module LanguagesHelper
|
|||
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
|
||||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||
csb: ['Kashubian', 'Kaszëbsczi'].freeze,
|
||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||
ldn: ['Láadan', 'Láadan'].freeze,
|
||||
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
|
||||
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
|
||||
nds: ['Low German', 'Plattdüütsch'].freeze,
|
||||
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
|
||||
sco: ['Scots', 'Scots'].freeze,
|
||||
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
|
||||
smj: ['Lule Sami', 'Julevsámegiella'].freeze,
|
||||
szl: ['Silesian', 'ślůnsko godka'].freeze,
|
||||
tok: ['Toki Pona', 'toki pona'].freeze,
|
||||
vai: ['Vai', 'ꕙꔤ'].freeze,
|
||||
xal: ['Kalmyk', 'Хальмг келн'].freeze,
|
||||
zba: ['Balaibalan', 'باليبلن'].freeze,
|
||||
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
|
||||
|
|
|
@ -66,11 +66,9 @@ export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS';
|
|||
export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
|
||||
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST';
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS';
|
||||
export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
|
||||
|
||||
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
|
||||
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
||||
|
||||
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
|
||||
|
@ -93,11 +91,6 @@ export * from './accounts_typed';
|
|||
export function fetchAccount(id) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(fetchRelationships([id]));
|
||||
|
||||
if (getState().getIn(['accounts', id], null) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchAccountRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||
|
|
|
@ -266,12 +266,14 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
|
|||
insertIfOnline('direct');
|
||||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
}));
|
||||
if (getState().getIn(['local_settings', 'show_published_toast'])) {
|
||||
dispatch(showAlert({
|
||||
message: statusId === null ? messages.published : messages.saved,
|
||||
action: messages.open,
|
||||
dismissAfter: 10000,
|
||||
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
|
||||
}));
|
||||
}
|
||||
}).catch(function (error) {
|
||||
dispatch(submitComposeFail(error));
|
||||
});
|
||||
|
@ -820,11 +822,12 @@ export function addPollOption(title) {
|
|||
};
|
||||
}
|
||||
|
||||
export function changePollOption(index, title) {
|
||||
export function changePollOption(index, title, maxOptions) {
|
||||
return {
|
||||
type: COMPOSE_POLL_OPTION_CHANGE,
|
||||
index,
|
||||
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_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({
|
||||
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
|
||||
});
|
||||
|
@ -402,3 +434,264 @@ export function setBrowserPermission (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(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
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(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status)));
|
||||
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 api from '../api';
|
||||
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST';
|
||||
|
@ -138,10 +138,10 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
|||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||
dispatch(deleteStatusSuccess(id));
|
||||
dispatch(deleteFromTimelines(id));
|
||||
dispatch(importFetchedAccount(response.data.account));
|
||||
|
||||
if (withRedraft) {
|
||||
dispatch(redraft(status, response.data.text, response.data.content_type));
|
||||
|
||||
ensureComposeIsVisible(getState, routerHistory);
|
||||
}
|
||||
}).catch(error => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import LinkIcon from '@/material-icons/400-24px/link.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
|
||||
|
||||
export default class AttachmentList extends ImmutablePureComponent {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
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 { withRouter } from 'react-router-dom';
|
||||
|
@ -11,12 +11,11 @@ import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.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 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 { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
||||
import { useAppHistory } from './router';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -24,10 +23,12 @@ const messages = defineMessages({
|
|||
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
|
||||
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 intl = useIntl();
|
||||
const { multiColumn } = useColumnsContext();
|
||||
|
||||
const handleBackClick = useCallback(() => {
|
||||
|
@ -40,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
|
|||
|
||||
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
|
||||
|
||||
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>);
|
||||
if (!showButton) return null;
|
||||
|
||||
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 = {
|
||||
pinned: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
onlyIcon: PropTypes.bool,
|
||||
};
|
||||
|
||||
class ColumnHeader extends PureComponent {
|
||||
|
@ -146,27 +149,31 @@ class ColumnHeader extends PureComponent {
|
|||
}
|
||||
|
||||
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 = (
|
||||
<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.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
|
||||
</div>
|
||||
);
|
||||
} 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 = [
|
||||
extraContent,
|
||||
];
|
||||
|
||||
if (multiColumn) {
|
||||
collapsedContent.push(pinButton);
|
||||
collapsedContent.push(moveButtons);
|
||||
collapsedContent.push(
|
||||
<div key='buttons' className='column-header__advanced-buttons'>
|
||||
{pinButton}
|
||||
{moveButtons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
|
||||
|
@ -178,7 +185,7 @@ class ColumnHeader extends PureComponent {
|
|||
onClick={this.handleToggleClick}
|
||||
>
|
||||
<i className='icon-with-badge'>
|
||||
<Icon id='sliders' icon={TuneIcon} />
|
||||
<Icon id='sliders' icon={SettingsIcon} />
|
||||
{collapseIssues && <i className='icon-with-badge__issue-badge' />}
|
||||
</i>
|
||||
</button>
|
||||
|
@ -191,16 +198,19 @@ class ColumnHeader extends PureComponent {
|
|||
<div className={wrapperClassName}>
|
||||
<h1 className={buttonClassName}>
|
||||
{hasTitle && (
|
||||
<button onClick={this.handleTitleClick}>
|
||||
<Icon id={icon} icon={iconComponent} className='column-header__icon' />
|
||||
{title}
|
||||
</button>
|
||||
<>
|
||||
{backButton}
|
||||
|
||||
<button onClick={this.handleTitleClick} className='column-header__title'>
|
||||
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
|
||||
{title}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hasTitle && backButton}
|
||||
|
||||
<div className='column-header__buttons'>
|
||||
{hasTitle && backButton}
|
||||
{extraButton}
|
||||
{collapseButton}
|
||||
</div>
|
||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
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 { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
|
|||
}) : (
|
||||
<IconButton
|
||||
icon={!open ? icon : 'close'}
|
||||
iconComponent={!open ? iconComponent : CloseIcon}
|
||||
iconComponent={iconComponent}
|
||||
title={title}
|
||||
active={open}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
|||
|
||||
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 { Icon } from 'flavours/glitch/components/icon';
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import logo from '@/images/logo.svg';
|
||||
|
||||
export const WordmarkLogo = () => (
|
||||
export const WordmarkLogo: React.FC = () => (
|
||||
<svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
|
||||
<title>Mastodon</title>
|
||||
<use xlinkHref='#logo-symbol-wordmark' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const SymbolLogo = () => (
|
||||
export const SymbolLogo: React.FC = () => (
|
||||
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
||||
);
|
||||
|
||||
export default WordmarkLogo;
|
|
@ -1,6 +1,6 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import illustration from 'flavours/glitch/images/elephant_ui_working.svg';
|
||||
import illustration from '@/images/elephant_ui_working.svg';
|
||||
|
||||
const RegenerationIndicator = () => (
|
||||
<div className='regeneration-indicator'>
|
||||
|
|
|
@ -20,6 +20,7 @@ import Card from '../features/status/components/card';
|
|||
// to use the progress bar to show download progress
|
||||
import Bundle from '../features/ui/components/bundle';
|
||||
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 AttachmentList from './attachment_list';
|
||||
|
@ -73,9 +74,7 @@ export const defaultMediaVisibility = (status, settings) => {
|
|||
|
||||
class Status extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
identity: PropTypes.object,
|
||||
};
|
||||
static contextType = SensitiveMediaContext;
|
||||
|
||||
static propTypes = {
|
||||
containerId: PropTypes.string,
|
||||
|
@ -132,8 +131,7 @@ class Status extends ImmutablePureComponent {
|
|||
isCollapsed: false,
|
||||
autoCollapsed: false,
|
||||
isExpanded: undefined,
|
||||
showMedia: undefined,
|
||||
statusId: undefined,
|
||||
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
|
||||
revealBehindCW: undefined,
|
||||
showCard: false,
|
||||
forceFilter: undefined,
|
||||
|
@ -218,12 +216,6 @@ class Status extends ImmutablePureComponent {
|
|||
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) {
|
||||
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
|
||||
if (update.revealBehindCW) {
|
||||
|
@ -319,6 +311,18 @@ class Status extends ImmutablePureComponent {
|
|||
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < 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() {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { List as ImmutableList } from 'immutable';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
|
||||
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
|
||||
|
@ -171,7 +170,8 @@ class About extends PureComponent {
|
|||
<ol className='rules-list'>
|
||||
{server.get('rules').map(rule => (
|
||||
<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>
|
||||
))}
|
||||
</ol>
|
||||
|
@ -189,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>
|
||||
|
||||
<div className='about__domain-blocks'>
|
||||
{domainBlocks.get('items').map(block => (
|
||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||
<div className='about__domain-blocks__domain__header'>
|
||||
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
|
||||
</div>
|
||||
{domainBlocks.get('items').size > 0 && (
|
||||
<div className='about__domain-blocks'>
|
||||
{domainBlocks.get('items').map(block => (
|
||||
<div className='about__domain-blocks__domain' key={block.get('domain')}>
|
||||
<div className='about__domain-blocks__domain__header'>
|
||||
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<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 { Icon } from 'flavours/glitch/components/icon';
|
||||
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 { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
|
||||
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 (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
actionBtn = <Button disabled><LoadingIndicator /></Button>;
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
|
@ -347,15 +348,10 @@ class Header extends ImmutablePureComponent {
|
|||
</a>
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{!hidden && (
|
||||
<>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
{shareBtn}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!hidden && bellBtn}
|
||||
{!hidden && shareBtn}
|
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
|
||||
{!hidden && actionBtn}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import { Blurhash } from 'flavours/glitch/components/blurhash';
|
|||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
|
||||
|
||||
|
||||
export default class MediaItem extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -18,7 +18,6 @@ import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
|
|||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
|
||||
|
||||
|
||||
import { Blurhash } from '../../components/blurhash';
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings'>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
|
|
|
@ -59,10 +59,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
|||
const dispatch = useDispatch();
|
||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||
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 } }) => {
|
||||
dispatch(changePollOption(index, value));
|
||||
}, [dispatch, index]);
|
||||
dispatch(changePollOption(index, value, maxOptions));
|
||||
}, [dispatch, index, maxOptions]);
|
||||
|
||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
|
|
|
@ -4,14 +4,24 @@ import { uploadCompose } from '../../../actions/compose';
|
|||
import { openModal } from '../../../actions/modal';
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
const mapStateToProps = state => {
|
||||
const isPoll = state.getIn(['compose', 'poll']) !== null;
|
||||
const isUploading = state.getIn(['compose', 'is_uploading']);
|
||||
const readyAttachmentsSize = state.getIn(['compose', 'media_attachments']).size ?? 0;
|
||||
const pendingAttachmentsSize = state.getIn(['compose', 'pending_media_attachments']).size ?? 0;
|
||||
const attachmentsSize = readyAttachmentsSize + pendingAttachmentsSize;
|
||||
const isOverLimit = attachmentsSize > 3;
|
||||
const hasVideoOrAudio = state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')));
|
||||
|
||||
return {
|
||||
disabled: isPoll || isUploading || isOverLimit || hasVideoOrAudio,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onSelectFile (files) {
|
||||
onSelectFile(files) {
|
||||
dispatch(uploadCompose(files));
|
||||
},
|
||||
|
||||
|
|
|
@ -26,18 +26,20 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span>
|
||||
<div className='column-settings'>
|
||||
<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'>
|
||||
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
|
||||
</div>
|
||||
<section>
|
||||
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
|
||||
|
||||
<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>
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
|
||||
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
|
||||
|
|
|
@ -8,7 +8,6 @@ import { NavLink, Switch, Route } from 'react-router-dom';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
|
|
|
@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||
|
|
|
@ -180,7 +180,7 @@ class SelectFilter extends PureComponent {
|
|||
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus />
|
||||
<button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
</div>
|
||||
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|||
import { Helmet } from 'react-helmet';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { addColumn } from 'flavours/glitch/actions/columns';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
|
@ -46,28 +45,37 @@ const ColumnSettings = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['onlyMedia']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||
/>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['allowLocalOnly']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
|
||||
/>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
<SettingText
|
||||
settings={settings}
|
||||
settingPath={['regex', 'body']}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(messages.filter_regex)}
|
||||
/>
|
||||
</div>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['onlyMedia']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
|
||||
/>
|
||||
|
||||
<SettingToggle
|
||||
settings={settings}
|
||||
settingPath={['allowLocalOnly']}
|
||||
onChange={onChange}
|
||||
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText
|
||||
settings={settings}
|
||||
settingPath={['regex', 'body']}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(messages.filter_regex)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -109,28 +109,28 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings__row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
|
||||
<span className='setting-toggle__label'>
|
||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||
</span>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||
|
||||
<span className='setting-toggle__label'>
|
||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.open && (
|
||||
<div className='column-settings__hashtags'>
|
||||
{this.modeSelect('any')}
|
||||
{this.modeSelect('all')}
|
||||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
</div>
|
||||
{this.state.open && (
|
||||
<div className='column-settings__hashtags'>
|
||||
{this.modeSelect('any')}
|
||||
{this.modeSelect('all')}
|
||||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,75 +35,68 @@ export const ColumnSettings: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className='column-settings__section'>
|
||||
<FormattedMessage
|
||||
id='home.column_settings.basic'
|
||||
defaultMessage='Basic'
|
||||
/>
|
||||
</span>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_reblogs'
|
||||
defaultMessage='Show boosts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reblog']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_reblogs'
|
||||
defaultMessage='Show boosts'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'reply']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_replies'
|
||||
defaultMessage='Show replies'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<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>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'direct']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_direct'
|
||||
defaultMessage='Show private mentions'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['shows', 'direct']}
|
||||
onChange={onChange}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='home.column_settings.show_direct'
|
||||
defaultMessage='Show private mentions'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<section aria-labelledby='home-column-advanced'>
|
||||
<h3 id='home-column-advanced'>
|
||||
<FormattedMessage
|
||||
id='home.column_settings.advanced'
|
||||
defaultMessage='Advanced'
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<span className='column-settings__section'>
|
||||
<FormattedMessage
|
||||
id='home.column_settings.advanced'
|
||||
defaultMessage='Advanced'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['regex', 'body']}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(messages.filter_regex)}
|
||||
/>
|
||||
</div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingText
|
||||
prefix='home_timeline'
|
||||
settings={settings}
|
||||
settingPath={['regex', 'body']}
|
||||
onChange={onChange}
|
||||
label={intl.formatMessage(messages.filter_regex)}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ import { Helmet } from 'react-helmet';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import Column from 'flavours/glitch/components/column';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
|
|
|
@ -11,7 +11,6 @@ import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
|||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -193,37 +193,38 @@ class ListTimeline extends PureComponent {
|
|||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<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}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</div>
|
||||
<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' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{ 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} />
|
||||
))}
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
{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>
|
||||
|
||||
<StatusListContainer
|
||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'flavours/glitch/actions/lists';
|
||||
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
|
||||
|
|
|
@ -224,6 +224,14 @@ class LocalSettingsPage extends PureComponent {
|
|||
>
|
||||
<FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' />
|
||||
</LocalSettingsPageItem>
|
||||
<LocalSettingsPageItem
|
||||
settings={settings}
|
||||
item={['show_published_toast']}
|
||||
id='mastodon-settings--show_published_toast'
|
||||
onChange={onChange}
|
||||
>
|
||||
<FormattedMessage id='settings.show_published_toast' defaultMessage='Display toast when publishing/saving a post' />
|
||||
</LocalSettingsPageItem>
|
||||
<LocalSettingsPageItem
|
||||
settings={settings}
|
||||
item={['side_arm']}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
|
|||
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
export default class ClearColumnButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
|
||||
|
||||
import { CheckboxWithLabel } from './checkbox_with_label';
|
||||
import ClearColumnButton from './clear_column_button';
|
||||
import GrantPermissionButton from './grant_permission_button';
|
||||
import PillBarButton from './pill_bar_button';
|
||||
|
@ -27,14 +28,32 @@ export default class ColumnSettings extends PureComponent {
|
|||
alertsEnabled: PropTypes.bool,
|
||||
browserSupport: PropTypes.bool,
|
||||
browserPermission: PropTypes.string,
|
||||
notificationPolicy: ImmutablePropTypes.map,
|
||||
onChangePolicy: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
onPushChange = (path, checked) => {
|
||||
this.props.onChange(['push', ...path], checked);
|
||||
};
|
||||
|
||||
handleFilterNotFollowing = checked => {
|
||||
this.props.onChangePolicy('filter_not_following', checked);
|
||||
};
|
||||
|
||||
handleFilterNotFollowers = checked => {
|
||||
this.props.onChangePolicy('filter_not_followers', checked);
|
||||
};
|
||||
|
||||
handleFilterNewAccounts = checked => {
|
||||
this.props.onChangePolicy('filter_new_accounts', checked);
|
||||
};
|
||||
|
||||
handleFilterPrivateMentions = checked => {
|
||||
this.props.onChangePolicy('filter_private_mentions', checked);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = 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 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' />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='column-settings'>
|
||||
{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>
|
||||
</div>
|
||||
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
|
||||
)}
|
||||
|
||||
{alertsEnabled && browserSupport && browserPermission === 'default' && (
|
||||
<div className='column-settings__row column-settings__row--with-margin'>
|
||||
<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} />
|
||||
</span>
|
||||
</div>
|
||||
<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} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<section>
|
||||
<ClearColumnButton onClick={onClear} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<span id='notifications-unread-markers' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
|
||||
</span>
|
||||
<section>
|
||||
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
|
||||
</div>
|
||||
</div>
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_following')} onChange={this.handleFilterNotFollowing}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' />
|
||||
</span>
|
||||
<CheckboxWithLabel checked={notificationPolicy.get('filter_not_followers')} onChange={this.handleFilterNotFollowers}>
|
||||
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
|
||||
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
|
||||
</CheckboxWithLabel>
|
||||
|
||||
<CheckboxWithLabel checked={notificationPolicy.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'>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow'>
|
||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-unread-markers'>
|
||||
<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'>
|
||||
<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={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div 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>
|
||||
<section role='group' aria-labelledby='notifications-follow-request'>
|
||||
<h3 id='notifications-follow-request'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-favourite'>
|
||||
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-favourite'>
|
||||
<h3 id='notifications-favourite'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-reaction'>
|
||||
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-reaction'>
|
||||
<h3 id='notifications-reaction'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-mention'>
|
||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-mention'>
|
||||
<h3 id='notifications-mention'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'mention']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-reblog'>
|
||||
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-reblog'>
|
||||
<h3 id='notifications-reblog'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-poll'>
|
||||
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-poll'>
|
||||
<h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'poll']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-status'>
|
||||
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-status'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'status']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div role='group' aria-labelledby='notifications-update'>
|
||||
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span>
|
||||
<section role='group' aria-labelledby='notifications-update'>
|
||||
<h3 id='notifications-update'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'update']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
|
||||
<div 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>
|
||||
<section role='group' aria-labelledby='notifications-admin-sign-up'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
|
||||
<div 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>
|
||||
<section role='group' aria-labelledby='notifications-admin-report'>
|
||||
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></h3>
|
||||
|
||||
<div className='column-settings__pillbar'>
|
||||
<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={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,7 +12,6 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
|||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const tooltips = defineMessages({
|
||||
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -5,7 +5,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
|
||||
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';
|
||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { showAlert } from '../../../actions/alerts';
|
||||
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 { changeSetting } from '../../../actions/settings';
|
||||
import ColumnSettings from '../components/column_settings';
|
||||
|
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
|
|||
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
|
||||
browserSupport: state.getIn(['notifications', 'browserSupport']),
|
||||
browserPermission: state.getIn(['notifications', 'browserPermission']),
|
||||
notificationPolicy: state.get('notificationPolicy'),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
@ -73,6 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
dispatch(requestBrowserPermission());
|
||||
},
|
||||
|
||||
onChangePolicy (param, checked) {
|
||||
dispatch(updateNotificationsPolicy({
|
||||
[param]: checked,
|
||||
}));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));
|
||||
|
|
|
@ -37,6 +37,7 @@ import { LoadGap } from '../../components/load_gap';
|
|||
import ScrollableList from '../../components/scrollable_list';
|
||||
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
|
||||
|
||||
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
|
||||
import NotificationsPermissionBanner from './components/notifications_permission_banner';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import FilterBarContainer from './containers/filter_bar_container';
|
||||
|
@ -357,6 +358,9 @@ class Notifications extends PureComponent {
|
|||
</ColumnHeader>
|
||||
|
||||
{filterBarContainer}
|
||||
|
||||
<FilteredNotificationsBanner />
|
||||
|
||||
{scrollContainer}
|
||||
|
||||
<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') });
|
||||
|
||||
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;
|
|
@ -6,7 +6,6 @@ import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?rea
|
|||
import CheckIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
|
||||
const content = (
|
||||
<>
|
||||
|
|
|
@ -8,7 +8,6 @@ import { Link, Switch, Route, useHistory } from 'react-router-dom';
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
|
||||
|
||||
import illustration from '@/images/elephant_ui_conversation.svg';
|
||||
import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
|
||||
|
|
|
@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
|
|
|
@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
|
|
|
@ -11,7 +11,6 @@ import { connect } from 'react-redux';
|
|||
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
|
||||
import { getStatusList } from 'flavours/glitch/selectors';
|
||||
|
||||
|
||||
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
|
||||
import StatusList from '../../components/status_list';
|
||||
import Column from '../ui/components/column';
|
||||
|
|
|
@ -25,18 +25,22 @@ class ColumnSettings extends PureComponent {
|
|||
const { settings, onChange, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<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', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote 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' />} />}
|
||||
</div>
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<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', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote 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' />} />}
|
||||
</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'>
|
||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
|
||||
import { domain } from 'flavours/glitch/initial_state';
|
||||
|
|
|
@ -14,7 +14,6 @@ import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
|||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
import { fetchReblogs, expandReblogs } from '../../actions/interactions';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||
|
|
|
@ -6,7 +6,6 @@ import classNames from 'classnames';
|
|||
import CheckIcon from '@/material-icons/400-24px/done.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
export default class Option extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -19,6 +19,7 @@ class StatusCheckBox extends PureComponent {
|
|||
status: ImmutablePropTypes.map.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleStatusesToggle = (value, checked) => {
|
||||
|
|
|
@ -18,8 +18,8 @@ import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
|||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
|
||||
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg';
|
||||
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg';
|
||||
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
|
||||
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
|
||||
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
|
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
|
||||
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
|
|
@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { followAccount } from 'flavours/glitch/actions/accounts';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { useRouteMatch, NavLink } from 'react-router-dom';
|
||||
|
||||
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 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 activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
|
||||
const active = match?.isExact;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} className={className} data-method={method} title={text} {...other}>
|
||||
{iconElement}
|
||||
{active ? activeIconElement : iconElement}
|
||||
<span>{text}</span>
|
||||
{badgeElement}
|
||||
</a>
|
||||
);
|
||||
} else if (to) {
|
||||
return (
|
||||
<NavLink to={to} className={className} title={text} {...other}>
|
||||
{iconElement}
|
||||
<NavLink to={to} className={className} title={text} exact {...other}>
|
||||
{active ? activeIconElement : iconElement}
|
||||
<span>{text}</span>
|
||||
{badgeElement}
|
||||
</NavLink>
|
||||
|
@ -46,6 +49,8 @@ const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badg
|
|||
ColumnLink.propTypes = {
|
||||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
iconComponent: PropTypes.func,
|
||||
activeIcon: PropTypes.node,
|
||||
activeIconComponent: PropTypes.func,
|
||||
text: PropTypes.string.isRequired,
|
||||
to: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
|
|
|
@ -58,6 +58,7 @@ const TabsBarPortal = () => {
|
|||
export default class ColumnsArea extends ImmutablePureComponent {
|
||||
static propTypes = {
|
||||
columns: ImmutablePropTypes.list.isRequired,
|
||||
isModalOpen: PropTypes.bool.isRequired,
|
||||
singleColumn: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
openSettings: PropTypes.func,
|
||||
|
@ -145,7 +146,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { columns, children, singleColumn, openSettings } = this.props;
|
||||
const { columns, children, singleColumn, isModalOpen, openSettings } = this.props;
|
||||
const { renderComposePanel } = this.state;
|
||||
|
||||
if (singleColumn) {
|
||||
|
@ -172,7 +173,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='columns-area' ref={this.setRef}>
|
||||
<div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
|
||||
{columns.map(column => {
|
||||
const params = column.get('params', null) === null ? null : column.get('params').toJS();
|
||||
const other = params && params.other ? params.other : {};
|
||||
|
|
|
@ -4,7 +4,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
|||
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import api from 'flavours/glitch/api';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
|
|
@ -5,7 +5,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { fetchFilters, createFilter, createFilterStatus } from 'flavours/glitch/actions/filters';
|
||||
import { fetchStatus } from 'flavours/glitch/actions/statuses';
|
||||
|
|
|
@ -181,14 +181,14 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onChangeDescription(e.target.value);
|
||||
this.handleSubmit();
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
|
||||
};
|
||||
|
||||
|
@ -318,7 +318,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='report-modal__container'>
|
||||
<div className='report-modal__comment'>
|
||||
<form className='report-modal__comment' onSubmit={this.handleSubmit} >
|
||||
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}
|
||||
|
||||
{thumbnailable && (
|
||||
|
@ -367,12 +367,23 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
</div>
|
||||
|
||||
<div className='setting-text__toolbar'>
|
||||
<button disabled={detecting || media.get('type') !== 'image' || is_changing_upload} className='link-button' onClick={this.handleTextDetection}><FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' /></button>
|
||||
<button
|
||||
type='button'
|
||||
disabled={detecting || media.get('type') !== 'image' || is_changing_upload}
|
||||
className='link-button'
|
||||
onClick={this.handleTextDetection}
|
||||
>
|
||||
<FormattedMessage id='upload_modal.detect_text' defaultMessage='Detect text from picture' />
|
||||
</button>
|
||||
<CharacterCounter max={1500} text={detecting ? '' : description} />
|
||||
</div>
|
||||
|
||||
<Button disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload} text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)} onClick={this.handleSubmit} />
|
||||
</div>
|
||||
<Button
|
||||
type='submit'
|
||||
disabled={!dirty || detecting || isUploadingThumbnail || length(description) > 1500 || is_changing_upload}
|
||||
text={intl.formatMessage(is_changing_upload ? messages.applying : messages.apply)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<div className='focal-point-modal__content'>
|
||||
{focals && (
|
||||
|
|
|
@ -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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch, useSelector } 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 { 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);
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
});
|
||||
export const ListPanel = () => {
|
||||
const dispatch = useDispatch();
|
||||
const lists = useSelector(state => getOrderedLists(state));
|
||||
|
||||
class ListPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
useEffect(() => {
|
||||
dispatch(fetchLists());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!lists || lists.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { lists } = this.props;
|
||||
return (
|
||||
<div className='list-panel'>
|
||||
<hr />
|
||||
|
||||
if (!lists || lists.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
{lists.map(list => (
|
||||
<ColumnLink icon='list-ul' key={list.get('id')} iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,33 @@
|
|||
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 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 MailActiveIcon from '@/material-icons/400-24px/mail-fill.svg?react';
|
||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.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 SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||
import SettingsIcon from '@/material-icons/400-24px/settings.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 { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
|
||||
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 DisabledAccountBanner from './disabled_account_banner';
|
||||
import FollowRequestsColumnLink from './follow_requests_column_link';
|
||||
import ListPanel from './list_panel';
|
||||
import NotificationsCounterIcon from './notifications_counter_icon';
|
||||
import { ListPanel } from './list_panel';
|
||||
import SignInBanner from './sign_in_banner';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -42,8 +54,48 @@ const messages = defineMessages({
|
|||
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.' },
|
||||
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 {
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -84,9 +136,9 @@ class NavigationPanel extends Component {
|
|||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} text={intl.formatMessage(messages.home)} />
|
||||
<ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} />
|
||||
<FollowRequestsColumnLink />
|
||||
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
|
||||
<NotificationsLink />
|
||||
<FollowRequestsLink />
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -109,10 +161,10 @@ class NavigationPanel extends Component {
|
|||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} text={intl.formatMessage(messages.direct)} />
|
||||
<ColumnLink transparent to='/bookmarks' icon='bookmark' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} />
|
||||
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} />
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} />
|
||||
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} activeIconComponent={MailActiveIcon} text={intl.formatMessage(messages.direct)} />
|
||||
<ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
|
||||
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
|
||||
|
||||
<ListPanel />
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue