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:
Jeremy Kescher 2024-03-14 20:00:37 +01:00
commit caccc2e0f4
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
613 changed files with 10843 additions and 5190 deletions

View file

@ -123,7 +123,7 @@ module.exports = defineConfig({
'react/react-in-jsx-scope': 'off', // not needed with new JSX transform 'react/react-in-jsx-scope': 'off', // not needed with new JSX transform
'react/self-closing-comp': 'error', 'react/self-closing-comp': 'error',
// recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/src/index.js // recommended values found in https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/v6.8.0/src/index.js#L46
'jsx-a11y/accessible-emoji': 'warn', 'jsx-a11y/accessible-emoji': 'warn',
'jsx-a11y/click-events-have-key-events': 'off', 'jsx-a11y/click-events-have-key-events': 'off',
'jsx-a11y/label-has-associated-control': 'off', 'jsx-a11y/label-has-associated-control': 'off',
@ -176,7 +176,7 @@ module.exports = defineConfig({
}, },
], ],
// See https://github.com/import-js/eslint-plugin-import/blob/main/config/recommended.js // See https://github.com/import-js/eslint-plugin-import/blob/v2.29.1/config/recommended.js
'import/extensions': [ 'import/extensions': [
'error', 'error',
'always', 'always',

View file

@ -53,7 +53,7 @@ jobs:
# Create or update the pull request # Create or update the pull request
- name: Create 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: with:
commit-message: 'New Crowdin translations' commit-message: 'New Crowdin translations'
title: 'New Crowdin Translations (automated)' title: 'New Crowdin Translations (automated)'

View file

@ -20,7 +20,7 @@ FROM docker.io/ruby:${RUBY_VERSION}-slim-${DEBIAN_VERSION} as ruby
# Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA # Resulting version string is vX.X.X-MASTODON_VERSION_PRERELEASE+MASTODON_VERSION_METADATA
# Example: v4.2.0-nightly.2023.11.09+something # Example: v4.2.0-nightly.2023.11.09+something
# Overwrite existance of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"] # Overwrite existence of 'alpha.0' in version.rb [--build-arg MASTODON_VERSION_PRERELEASE="nightly.2023.11.09"]
ARG MASTODON_VERSION_PRERELEASE="" ARG MASTODON_VERSION_PRERELEASE=""
# Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"] # Append build metadata or fork information to version.rb [--build-arg MASTODON_VERSION_METADATA="something"]
ARG MASTODON_VERSION_METADATA="" ARG MASTODON_VERSION_METADATA=""
@ -29,7 +29,7 @@ ARG MASTODON_VERSION_METADATA=""
# See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files # See: https://docs.joinmastodon.org/admin/config/#rails_serve_static_files
ARG RAILS_SERVE_STATIC_FILES="true" ARG RAILS_SERVE_STATIC_FILES="true"
# Allow to use YJIT compiler # Allow to use YJIT compiler
# See: https://github.com/ruby/ruby/blob/master/doc/yjit/yjit.md # See: https://github.com/ruby/ruby/blob/v3_2_3/doc/yjit/yjit.md
ARG RUBY_YJIT_ENABLE="1" ARG RUBY_YJIT_ENABLE="1"
# Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin] # Timezone used by the Docker container and runtime, change with [--build-arg TZ=Europe/Berlin]
ARG TZ="Etc/UTC" ARG TZ="Etc/UTC"

View file

@ -112,7 +112,7 @@ group :test do
# RSpec helpers for email specs # RSpec helpers for email specs
gem 'email_spec' gem 'email_spec'
# Extra RSpec extenion methods and helpers for sidekiq # Extra RSpec extension methods and helpers for sidekiq
gem 'rspec-sidekiq', '~> 4.0' gem 'rspec-sidekiq', '~> 4.0'
# Browser integration testing # Browser integration testing

View file

@ -357,7 +357,7 @@ GEM
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.1) json (2.7.1)
json-canonicalization (1.0.0) json-canonicalization (1.0.0)
json-jwt (1.15.3) json-jwt (1.15.3.1)
activesupport (>= 4.2) activesupport (>= 4.2)
aes_key_wrap aes_key_wrap
bindata bindata
@ -465,11 +465,11 @@ GEM
statsd-ruby (~> 1.4, >= 1.4.0) statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
omniauth (2.1.1) omniauth (2.1.2)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
omniauth-cas (3.0.0.beta.1) omniauth-cas (3.0.0)
addressable (~> 2.8) addressable (~> 2.8)
nokogiri (~> 1.12) nokogiri (~> 1.12)
omniauth (~> 2.1) omniauth (~> 2.1)
@ -505,7 +505,7 @@ GEM
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.5) pg (1.5.6)
pghero (3.4.1) pghero (3.4.1)
activerecord (>= 6) activerecord (>= 6)
posix-spawn (0.3.15) posix-spawn (0.3.15)
@ -535,7 +535,7 @@ GEM
rack (2.2.8.1) rack (2.2.8.1)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (2.0.1) rack-cors (2.0.2)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-oauth2 (1.21.3) rack-oauth2 (1.21.3)
activesupport activesupport
@ -543,8 +543,9 @@ GEM
httpclient httpclient
json-jwt (>= 1.11.0) json-jwt (>= 1.11.0)
rack (>= 2.1.0) rack (>= 2.1.0)
rack-protection (3.0.5) rack-protection (3.2.0)
rack base64 (>= 0.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-session (1.0.2) rack-session (1.0.2)
@ -743,7 +744,7 @@ GEM
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (1.0.1) terrapin (1.0.1)
climate_control climate_control
test-prof (1.3.1) test-prof (1.3.2)
thor (1.3.1) thor (1.3.1)
tilt (2.3.0) tilt (2.3.0)
timeout (0.4.1) timeout (0.4.1)

2
Vagrantfile vendored
View file

@ -188,7 +188,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.post_up_message = <<MESSAGE config.vm.post_up_message = <<MESSAGE
To start server To start server
$ vagrant ssh -c "cd /vagrant && foreman start" $ vagrant ssh -c "cd /vagrant && bin/dev"
MESSAGE MESSAGE
end end

View file

@ -53,7 +53,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:rule).permit(:text, :priority) params.require(:rule).permit(:text, :hint, :priority)
end end
end end
end end

View file

@ -73,6 +73,14 @@ class Api::BaseController < ApplicationController
protected protected
def pagination_max_id
pagination_collection.last.id
end
def pagination_since_id
pagination_collection.first.id
end
def set_pagination_headers(next_path = nil, prev_path = nil) def set_pagination_headers(next_path = nil, prev_path = nil)
links = [] links = []
links << [next_path, [%w(rel next)]] if next_path links << [next_path, [%w(rel next)]] if next_path

View file

@ -51,11 +51,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end end
def pagination_max_id def pagination_collection
@statuses.last.id @statuses
end
def pagination_since_id
@statuses.first.id
end end
end end

View file

@ -137,12 +137,8 @@ class Api::V1::Admin::AccountsController < Api::BaseController
api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty?
end end
def pagination_max_id def pagination_collection
@accounts.last.id @accounts
end
def pagination_since_id
@accounts.first.id
end end
def records_continue? def records_continue?

View file

@ -77,12 +77,8 @@ class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty? api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end end
def pagination_max_id def pagination_collection
@canonical_email_blocks.last.id @canonical_email_blocks
end
def pagination_since_id
@canonical_email_blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -73,12 +73,8 @@ class Api::V1::Admin::DomainAllowsController < Api::BaseController
api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty? api_v1_admin_domain_allows_url(pagination_params(min_id: pagination_since_id)) unless @domain_allows.empty?
end end
def pagination_max_id def pagination_collection
@domain_allows.last.id @domain_allows
end
def pagination_since_id
@domain_allows.first.id
end end
def records_continue? def records_continue?

View file

@ -84,12 +84,8 @@ class Api::V1::Admin::DomainBlocksController < Api::BaseController
api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty? api_v1_admin_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @domain_blocks.empty?
end end
def pagination_max_id def pagination_collection
@domain_blocks.last.id @domain_blocks
end
def pagination_since_id
@domain_blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -70,12 +70,8 @@ class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty? api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end end
def pagination_max_id def pagination_collection
@email_domain_blocks.last.id @email_domain_blocks
end
def pagination_since_id
@email_domain_blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -75,12 +75,8 @@ class Api::V1::Admin::IpBlocksController < Api::BaseController
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty? api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end end
def pagination_max_id def pagination_collection
@ip_blocks.last.id @ip_blocks
end
def pagination_since_id
@ip_blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -101,12 +101,8 @@ class Api::V1::Admin::ReportsController < Api::BaseController
api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty?
end end
def pagination_max_id def pagination_collection
@reports.last.id @reports
end
def pagination_since_id
@reports.first.id
end end
def records_continue? def records_continue?

View file

@ -56,12 +56,8 @@ class Api::V1::Admin::TagsController < Api::BaseController
api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty? api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty?
end end
def pagination_max_id def pagination_collection
@tags.last.id @tags
end
def pagination_since_id
@tags.first.id
end end
def records_continue? def records_continue?

View file

@ -54,12 +54,8 @@ class Api::V1::Admin::Trends::Links::PreviewCardProvidersController < Api::BaseC
api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty? api_v1_admin_trends_links_preview_card_providers_url(pagination_params(min_id: pagination_since_id)) unless @providers.empty?
end end
def pagination_max_id def pagination_collection
@providers.last.id @providers
end
def pagination_since_id
@providers.first.id
end end
def records_continue? def records_continue?

View file

@ -40,12 +40,8 @@ class Api::V1::BlocksController < Api::BaseController
api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty? api_v1_blocks_url pagination_params(since_id: pagination_since_id) unless paginated_blocks.empty?
end end
def pagination_max_id def pagination_collection
paginated_blocks.last.id paginated_blocks
end
def pagination_since_id
paginated_blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -43,12 +43,8 @@ class Api::V1::BookmarksController < Api::BaseController
api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty? api_v1_bookmarks_url pagination_params(min_id: pagination_since_id) unless results.empty?
end end
def pagination_max_id def pagination_collection
results.last.id results
end
def pagination_since_id
results.first.id
end end
def records_continue? def records_continue?

View file

@ -41,12 +41,8 @@ class Api::V1::Crypto::EncryptedMessagesController < Api::BaseController
api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty? api_v1_crypto_encrypted_messages_url pagination_params(min_id: pagination_since_id) unless @encrypted_messages.empty?
end end
def pagination_max_id def pagination_collection
@encrypted_messages.last.id @encrypted_messages
end
def pagination_since_id
@encrypted_messages.first.id
end end
def records_continue? def records_continue?

View file

@ -50,12 +50,8 @@ class Api::V1::DomainBlocksController < Api::BaseController
api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty? api_v1_domain_blocks_url pagination_params(since_id: pagination_since_id) unless @blocks.empty?
end end
def pagination_max_id def pagination_collection
@blocks.last.id @blocks
end
def pagination_since_id
@blocks.first.id
end end
def records_continue? def records_continue?

View file

@ -44,12 +44,8 @@ class Api::V1::EndorsementsController < Api::BaseController
api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? api_v1_endorsements_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end end
def pagination_max_id def pagination_collection
@accounts.last.id @accounts
end
def pagination_since_id
@accounts.first.id
end end
def records_continue? def records_continue?

View file

@ -43,12 +43,8 @@ class Api::V1::FavouritesController < Api::BaseController
api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty? api_v1_favourites_url pagination_params(min_id: pagination_since_id) unless results.empty?
end end
def pagination_max_id def pagination_collection
results.last.id results
end
def pagination_since_id
results.first.id
end end
def records_continue? def records_continue?

View file

@ -34,12 +34,8 @@ class Api::V1::FollowedTagsController < Api::BaseController
api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty? api_v1_followed_tags_url pagination_params(since_id: pagination_since_id) unless @results.empty?
end end
def pagination_max_id def pagination_collection
@results.last.id @results
end
def pagination_since_id
@results.first.id
end end
def records_continue? def records_continue?

View file

@ -71,12 +71,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty? api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) unless @accounts.empty?
end end
def pagination_max_id def pagination_collection
@accounts.last.id @accounts
end
def pagination_since_id
@accounts.first.id
end end
def records_continue? def records_continue?

View file

@ -40,12 +40,8 @@ class Api::V1::MutesController < Api::BaseController
api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty? api_v1_mutes_url pagination_params(since_id: pagination_since_id) unless paginated_mutes.empty?
end end
def pagination_max_id def pagination_collection
paginated_mutes.last.id paginated_mutes
end
def pagination_since_id
paginated_mutes.first.id
end end
def records_continue? def records_continue?

View 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

View 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

View file

@ -58,7 +58,8 @@ class Api::V1::NotificationsController < Api::BaseController
current_account.notifications.without_suspended.browserable( current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]), types: Array(browserable_params[:types]),
exclude_types: Array(browserable_params[:exclude_types]), exclude_types: Array(browserable_params[:exclude_types]),
from_account_id: browserable_params[:account_id] from_account_id: browserable_params[:account_id],
include_filtered: truthy_param?(:include_filtered)
) )
end end
@ -78,19 +79,15 @@ class Api::V1::NotificationsController < Api::BaseController
api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty? api_v1_notifications_url pagination_params(min_id: pagination_since_id) unless @notifications.empty?
end end
def pagination_max_id def pagination_collection
@notifications.last.id @notifications
end
def pagination_since_id
@notifications.first.id
end end
def browserable_params def browserable_params
params.permit(:account_id, types: [], exclude_types: []) params.permit(:account_id, :include_filtered, types: [], exclude_types: [])
end end
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit, :account_id, :types, :exclude_types).permit(:limit, :account_id, types: [], exclude_types: []).merge(core_params) params.slice(:limit, :account_id, :types, :exclude_types, :include_filtered).permit(:limit, :account_id, :include_filtered, types: [], exclude_types: []).merge(core_params)
end end
end end

View file

@ -63,11 +63,7 @@ class Api::V1::ScheduledStatusesController < Api::BaseController
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
end end
def pagination_max_id def pagination_collection
@statuses.last.id @statuses
end
def pagination_since_id
@statuses.first.id
end end
end end

View file

@ -9,12 +9,8 @@ class Api::V1::Timelines::BaseController < Api::BaseController
set_pagination_headers(next_path, prev_path) set_pagination_headers(next_path, prev_path)
end end
def pagination_max_id def pagination_collection
@statuses.last.id @statuses
end
def pagination_since_id
@statuses.first.id
end end
def next_path_params def next_path_params

View file

@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
end end
def single_user_mode? def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
end end
def use_seamless_external_login? def use_seamless_external_login?

View file

@ -214,7 +214,7 @@ module ApplicationHelper
state_params[:moved_to_account] = current_account.moved_to_account state_params[:moved_to_account] = current_account.moved_to_account
end end
state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety # rubocop:disable Rails/OutputSafety

View file

@ -109,6 +109,7 @@ module LanguagesHelper
mn: ['Mongolian', 'Монгол хэл'].freeze, mn: ['Mongolian', 'Монгол хэл'].freeze,
mr: ['Marathi', 'मराठी'].freeze, mr: ['Marathi', 'मराठी'].freeze,
ms: ['Malay', 'Bahasa Melayu'].freeze, ms: ['Malay', 'Bahasa Melayu'].freeze,
'ms-Arab': ['Jawi Malay', 'بهاس ملايو'].freeze,
mt: ['Maltese', 'Malti'].freeze, mt: ['Maltese', 'Malti'].freeze,
my: ['Burmese', 'ဗမာစာ'].freeze, my: ['Burmese', 'ဗမာစာ'].freeze,
na: ['Nauru', 'Ekakairũ Naoero'].freeze, na: ['Nauru', 'Ekakairũ Naoero'].freeze,
@ -127,7 +128,7 @@ module LanguagesHelper
om: ['Oromo', 'Afaan Oromoo'].freeze, om: ['Oromo', 'Afaan Oromoo'].freeze,
or: ['Oriya', 'ଓଡ଼ିଆ'].freeze, or: ['Oriya', 'ଓଡ଼ିଆ'].freeze,
os: ['Ossetian', 'ирон æвзаг'].freeze, os: ['Ossetian', 'ирон æвзаг'].freeze,
pa: ['Panjabi', 'ਪੰਜਾਬੀ'].freeze, pa: ['Punjabi', 'ਪੰਜਾਬੀ'].freeze,
pi: ['Pāli', 'पाऴि'].freeze, pi: ['Pāli', 'पाऴि'].freeze,
pl: ['Polish', 'Polski'].freeze, pl: ['Polish', 'Polski'].freeze,
ps: ['Pashto', 'پښتو'].freeze, ps: ['Pashto', 'پښتو'].freeze,
@ -191,15 +192,20 @@ module LanguagesHelper
chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze, chr: ['Cherokee', 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ'].freeze,
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze, cnr: ['Montenegrin', 'crnogorski'].freeze,
csb: ['Kashubian', 'Kaszëbsczi'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze, jbo: ['Lojban', 'la .lojban.'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze, ldn: ['Láadan', 'Láadan'].freeze,
lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze, lfn: ['Lingua Franca Nova', 'lingua franca nova'].freeze,
moh: ['Mohawk', 'Kanienʼkéha'].freeze,
nds: ['Low German', 'Plattdüütsch'].freeze,
pdc: ['Pennsylvania Dutch', 'Pennsilfaani-Deitsch'].freeze,
sco: ['Scots', 'Scots'].freeze, sco: ['Scots', 'Scots'].freeze,
sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze, sma: ['Southern Sami', 'Åarjelsaemien Gïele'].freeze,
smj: ['Lule Sami', 'Julevsámegiella'].freeze, smj: ['Lule Sami', 'Julevsámegiella'].freeze,
szl: ['Silesian', 'ślůnsko godka'].freeze, szl: ['Silesian', 'ślůnsko godka'].freeze,
tok: ['Toki Pona', 'toki pona'].freeze, tok: ['Toki Pona', 'toki pona'].freeze,
vai: ['Vai', 'ꕙꔤ'].freeze,
xal: ['Kalmyk', 'Хальмг келн'].freeze, xal: ['Kalmyk', 'Хальмг келн'].freeze,
zba: ['Balaibalan', 'باليبلن'].freeze, zba: ['Balaibalan', 'باليبلن'].freeze,
zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze, zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,

View file

@ -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_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL';
export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; 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_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL';
export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; 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 FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST';
@ -93,11 +91,6 @@ export * from './accounts_typed';
export function fetchAccount(id) { export function fetchAccount(id) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(fetchRelationships([id])); dispatch(fetchRelationships([id]));
if (getState().getIn(['accounts', id], null) !== null) {
return;
}
dispatch(fetchAccountRequest(id)); dispatch(fetchAccountRequest(id));
api(getState).get(`/api/v1/accounts/${id}`).then(response => { api(getState).get(`/api/v1/accounts/${id}`).then(response => {

View file

@ -266,12 +266,14 @@ export function submitCompose(routerHistory, overridePrivacy = null) {
insertIfOnline('direct'); insertIfOnline('direct');
} }
dispatch(showAlert({ if (getState().getIn(['local_settings', 'show_published_toast'])) {
message: statusId === null ? messages.published : messages.saved, dispatch(showAlert({
action: messages.open, message: statusId === null ? messages.published : messages.saved,
dismissAfter: 10000, action: messages.open,
onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`), dismissAfter: 10000,
})); onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
}));
}
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
}); });
@ -820,11 +822,12 @@ export function addPollOption(title) {
}; };
} }
export function changePollOption(index, title) { export function changePollOption(index, title, maxOptions) {
return { return {
type: COMPOSE_POLL_OPTION_CHANGE, type: COMPOSE_POLL_OPTION_CHANGE,
index, index,
title, title,
maxOptions,
}; };
} }

View file

@ -57,6 +57,38 @@ export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ';
export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT';
export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION';
export const NOTIFICATION_POLICY_FETCH_REQUEST = 'NOTIFICATION_POLICY_FETCH_REQUEST';
export const NOTIFICATION_POLICY_FETCH_SUCCESS = 'NOTIFICATION_POLICY_FETCH_SUCCESS';
export const NOTIFICATION_POLICY_FETCH_FAIL = 'NOTIFICATION_POLICY_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_FETCH_REQUEST = 'NOTIFICATION_REQUESTS_FETCH_REQUEST';
export const NOTIFICATION_REQUESTS_FETCH_SUCCESS = 'NOTIFICATION_REQUESTS_FETCH_SUCCESS';
export const NOTIFICATION_REQUESTS_FETCH_FAIL = 'NOTIFICATION_REQUESTS_FETCH_FAIL';
export const NOTIFICATION_REQUESTS_EXPAND_REQUEST = 'NOTIFICATION_REQUESTS_EXPAND_REQUEST';
export const NOTIFICATION_REQUESTS_EXPAND_SUCCESS = 'NOTIFICATION_REQUESTS_EXPAND_SUCCESS';
export const NOTIFICATION_REQUESTS_EXPAND_FAIL = 'NOTIFICATION_REQUESTS_EXPAND_FAIL';
export const NOTIFICATION_REQUEST_FETCH_REQUEST = 'NOTIFICATION_REQUEST_FETCH_REQUEST';
export const NOTIFICATION_REQUEST_FETCH_SUCCESS = 'NOTIFICATION_REQUEST_FETCH_SUCCESS';
export const NOTIFICATION_REQUEST_FETCH_FAIL = 'NOTIFICATION_REQUEST_FETCH_FAIL';
export const NOTIFICATION_REQUEST_ACCEPT_REQUEST = 'NOTIFICATION_REQUEST_ACCEPT_REQUEST';
export const NOTIFICATION_REQUEST_ACCEPT_SUCCESS = 'NOTIFICATION_REQUEST_ACCEPT_SUCCESS';
export const NOTIFICATION_REQUEST_ACCEPT_FAIL = 'NOTIFICATION_REQUEST_ACCEPT_FAIL';
export const NOTIFICATION_REQUEST_DISMISS_REQUEST = 'NOTIFICATION_REQUEST_DISMISS_REQUEST';
export const NOTIFICATION_REQUEST_DISMISS_SUCCESS = 'NOTIFICATION_REQUEST_DISMISS_SUCCESS';
export const NOTIFICATION_REQUEST_DISMISS_FAIL = 'NOTIFICATION_REQUEST_DISMISS_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_FETCH_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_FETCH_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL = 'NOTIFICATIONS_FOR_REQUEST_FETCH_FAIL';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_REQUEST';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_SUCCESS';
export const NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL = 'NOTIFICATIONS_FOR_REQUEST_EXPAND_FAIL';
defineMessages({ defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
}); });
@ -402,3 +434,264 @@ export function setBrowserPermission (value) {
value, value,
}; };
} }
export const fetchNotificationPolicy = () => (dispatch, getState) => {
dispatch(fetchNotificationPolicyRequest());
api(getState).get('/api/v1/notifications/policy').then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationPolicyRequest = () => ({
type: NOTIFICATION_POLICY_FETCH_REQUEST,
});
export const fetchNotificationPolicySuccess = policy => ({
type: NOTIFICATION_POLICY_FETCH_SUCCESS,
policy,
});
export const fetchNotificationPolicyFail = error => ({
type: NOTIFICATION_POLICY_FETCH_FAIL,
error,
});
export const updateNotificationsPolicy = params => (dispatch, getState) => {
dispatch(fetchNotificationPolicyRequest());
api(getState).put('/api/v1/notifications/policy', params).then(({ data }) => {
dispatch(fetchNotificationPolicySuccess(data));
}).catch(err => {
dispatch(fetchNotificationPolicyFail(err));
});
};
export const fetchNotificationRequests = () => (dispatch, getState) => {
const params = {};
if (getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
if (getState().getIn(['notificationRequests', 'items'])?.size > 0) {
params.since_id = getState().getIn(['notificationRequests', 'items', 0, 'id']);
}
dispatch(fetchNotificationRequestsRequest());
api(getState).get('/api/v1/notifications/requests', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(fetchNotificationRequestsSuccess(response.data, next ? next.uri : null));
}).catch(err => {
dispatch(fetchNotificationRequestsFail(err));
});
};
export const fetchNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_FETCH_REQUEST,
});
export const fetchNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_FETCH_SUCCESS,
requests,
next,
});
export const fetchNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_FETCH_FAIL,
error,
});
export const expandNotificationRequests = () => (dispatch, getState) => {
const url = getState().getIn(['notificationRequests', 'next']);
if (!url || getState().getIn(['notificationRequests', 'isLoading'])) {
return;
}
dispatch(expandNotificationRequestsRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
dispatch(expandNotificationRequestsSuccess(response.data, next?.uri));
}).catch(err => {
dispatch(expandNotificationRequestsFail(err));
});
};
export const expandNotificationRequestsRequest = () => ({
type: NOTIFICATION_REQUESTS_EXPAND_REQUEST,
});
export const expandNotificationRequestsSuccess = (requests, next) => ({
type: NOTIFICATION_REQUESTS_EXPAND_SUCCESS,
requests,
next,
});
export const expandNotificationRequestsFail = error => ({
type: NOTIFICATION_REQUESTS_EXPAND_FAIL,
error,
});
export const fetchNotificationRequest = id => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
if (current.getIn(['item', 'id']) === id || current.get('isLoading')) {
return;
}
dispatch(fetchNotificationRequestRequest(id));
api(getState).get(`/api/v1/notifications/requests/${id}`).then(({ data }) => {
dispatch(fetchNotificationRequestSuccess(data));
}).catch(err => {
dispatch(fetchNotificationRequestFail(id, err));
});
};
export const fetchNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_FETCH_REQUEST,
id,
});
export const fetchNotificationRequestSuccess = request => ({
type: NOTIFICATION_REQUEST_FETCH_SUCCESS,
request,
});
export const fetchNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_FETCH_FAIL,
id,
error,
});
export const acceptNotificationRequest = id => (dispatch, getState) => {
dispatch(acceptNotificationRequestRequest(id));
api(getState).post(`/api/v1/notifications/requests/${id}/accept`).then(() => {
dispatch(acceptNotificationRequestSuccess(id));
}).catch(err => {
dispatch(acceptNotificationRequestFail(id, err));
});
};
export const acceptNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_REQUEST,
id,
});
export const acceptNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_ACCEPT_SUCCESS,
id,
});
export const acceptNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_ACCEPT_FAIL,
id,
error,
});
export const dismissNotificationRequest = id => (dispatch, getState) => {
dispatch(dismissNotificationRequestRequest(id));
api(getState).post(`/api/v1/notifications/requests/${id}/dismiss`).then(() =>{
dispatch(dismissNotificationRequestSuccess(id));
}).catch(err => {
dispatch(dismissNotificationRequestFail(id, err));
});
};
export const dismissNotificationRequestRequest = id => ({
type: NOTIFICATION_REQUEST_DISMISS_REQUEST,
id,
});
export const dismissNotificationRequestSuccess = id => ({
type: NOTIFICATION_REQUEST_DISMISS_SUCCESS,
id,
});
export const dismissNotificationRequestFail = (id, error) => ({
type: NOTIFICATION_REQUEST_DISMISS_FAIL,
id,
error,
});
export const fetchNotificationsForRequest = accountId => (dispatch, getState) => {
const current = getState().getIn(['notificationRequests', 'current']);
const params = { account_id: accountId };
if (current.getIn(['item', 'account']) === accountId) {
if (current.getIn(['notifications', 'isLoading'])) {
return;
}
if (current.getIn(['notifications', 'items'])?.size > 0) {
params.since_id = current.getIn(['notifications', 'items', 0, 'id']);
}
}
dispatch(fetchNotificationsForRequestRequest());
api(getState).get('/api/v1/notifications', { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(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,
});

View file

@ -1,7 +1,7 @@
import api from '../api'; import api from '../api';
import { ensureComposeIsVisible, setComposeToStatus } from './compose'; import { ensureComposeIsVisible, setComposeToStatus } from './compose';
import { importFetchedStatus, importFetchedStatuses } from './importer'; import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; 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 => { api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
dispatch(deleteStatusSuccess(id)); dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id)); dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));
if (withRedraft) { if (withRedraft) {
dispatch(redraft(status, response.data.text, response.data.content_type)); dispatch(redraft(status, response.data.text, response.data.content_type));
ensureComposeIsVisible(getState, routerHistory); ensureComposeIsVisible(getState, routerHistory);
} }
}).catch(error => { }).catch(error => {

View file

@ -10,7 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import LinkIcon from '@/material-icons/400-24px/link.svg?react'; import LinkIcon from '@/material-icons/400-24px/link.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; const filename = url => url.split('/').pop().split('#')[0].split('?')[0];
export default class AttachmentList extends ImmutablePureComponent { export default class AttachmentList extends ImmutablePureComponent {

View file

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent, useCallback } from 'react'; import { PureComponent, useCallback } from 'react';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { FormattedMessage, injectIntl, defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
@ -11,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 ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import TuneIcon from '@/material-icons/400-24px/tune.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context'; import { ButtonInTabsBar, useColumnsContext } from 'flavours/glitch/features/ui/util/columns_context';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
import { useAppHistory } from './router'; import { useAppHistory } from './router';
const messages = defineMessages({ const messages = defineMessages({
@ -24,10 +23,12 @@ const messages = defineMessages({
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' }, moveLeft: { id: 'column_header.moveLeft_settings', defaultMessage: 'Move column to the left' },
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' }, moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
}); });
const BackButton = ({ pinned, show }) => { const BackButton = ({ pinned, show, onlyIcon }) => {
const history = useAppHistory(); const history = useAppHistory();
const intl = useIntl();
const { multiColumn } = useColumnsContext(); const { multiColumn } = useColumnsContext();
const handleBackClick = useCallback(() => { const handleBackClick = useCallback(() => {
@ -40,18 +41,20 @@ const BackButton = ({ pinned, show }) => {
const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show); const showButton = history && !pinned && ((multiColumn && history.location?.state?.fromMastodon) || show);
if(!showButton) return null; if (!showButton) return null;
return (<button onClick={handleBackClick} className='column-header__back-button'>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</button>);
return (
<button onClick={handleBackClick} className={classNames('column-header__back-button', { 'compact': onlyIcon })} aria-label={intl.formatMessage(messages.back)}>
<Icon id='chevron-left' icon={ArrowBackIcon} className='column-back-button__icon' />
{!onlyIcon && <FormattedMessage id='column_back_button.label' defaultMessage='Back' />}
</button>
);
}; };
BackButton.propTypes = { BackButton.propTypes = {
pinned: PropTypes.bool, pinned: PropTypes.bool,
show: PropTypes.bool, show: PropTypes.bool,
onlyIcon: PropTypes.bool,
}; };
class ColumnHeader extends PureComponent { class ColumnHeader extends PureComponent {
@ -146,27 +149,31 @@ class ColumnHeader extends PureComponent {
} }
if (multiColumn && pinned) { if (multiColumn && pinned) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>; pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='column_header.unpin' defaultMessage='Unpin' /></button>;
moveButtons = ( moveButtons = (
<div key='move-buttons' className='column-header__setting-arrows'> <div className='column-header__setting-arrows'>
<button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button> <button title={formatMessage(messages.moveLeft)} aria-label={formatMessage(messages.moveLeft)} className='icon-button column-header__setting-btn' onClick={this.handleMoveLeft}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>
<button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button> <button title={formatMessage(messages.moveRight)} aria-label={formatMessage(messages.moveRight)} className='icon-button column-header__setting-btn' onClick={this.handleMoveRight}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>
</div> </div>
); );
} else if (multiColumn && this.props.onPin) { } else if (multiColumn && this.props.onPin) {
pinButton = <button key='pin-button' className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>; pinButton = <button className='text-btn column-header__setting-btn' onClick={this.handlePin}><Icon id='plus' icon={AddIcon} /> <FormattedMessage id='column_header.pin' defaultMessage='Pin' /></button>;
} }
backButton = <BackButton pinned={pinned} show={showBackButton} />; backButton = <BackButton pinned={pinned} show={showBackButton} onlyIcon={!!title} />;
const collapsedContent = [ const collapsedContent = [
extraContent, extraContent,
]; ];
if (multiColumn) { if (multiColumn) {
collapsedContent.push(pinButton); collapsedContent.push(
collapsedContent.push(moveButtons); <div key='buttons' className='column-header__advanced-buttons'>
{pinButton}
{moveButtons}
</div>
);
} }
if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) { if (this.context.identity.signedIn && (children || (multiColumn && this.props.onPin))) {
@ -178,7 +185,7 @@ class ColumnHeader extends PureComponent {
onClick={this.handleToggleClick} onClick={this.handleToggleClick}
> >
<i className='icon-with-badge'> <i className='icon-with-badge'>
<Icon id='sliders' icon={TuneIcon} /> <Icon id='sliders' icon={SettingsIcon} />
{collapseIssues && <i className='icon-with-badge__issue-badge' />} {collapseIssues && <i className='icon-with-badge__issue-badge' />}
</i> </i>
</button> </button>
@ -191,16 +198,19 @@ class ColumnHeader extends PureComponent {
<div className={wrapperClassName}> <div className={wrapperClassName}>
<h1 className={buttonClassName}> <h1 className={buttonClassName}>
{hasTitle && ( {hasTitle && (
<button onClick={this.handleTitleClick}> <>
<Icon id={icon} icon={iconComponent} className='column-header__icon' /> {backButton}
{title}
</button> <button onClick={this.handleTitleClick} className='column-header__title'>
{!showBackButton && <Icon id={icon} icon={iconComponent} className='column-header__icon' />}
{title}
</button>
</>
)} )}
{!hasTitle && backButton} {!hasTitle && backButton}
<div className='column-header__buttons'> <div className='column-header__buttons'>
{hasTitle && backButton}
{extraButton} {extraButton}
{collapseButton} {collapseButton}
</div> </div>

View file

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { CircularProgress } from 'flavours/glitch/components/circular_progress'; import { CircularProgress } from 'flavours/glitch/components/circular_progress';
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router'; import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
@ -298,7 +297,7 @@ class Dropdown extends PureComponent {
}) : ( }) : (
<IconButton <IconButton
icon={!open ? icon : 'close'} icon={!open ? icon : 'close'}
iconComponent={!open ? iconComponent : CloseIcon} iconComponent={iconComponent}
title={title} title={title}
active={open} active={open}
disabled={disabled} disabled={disabled}

View file

@ -5,7 +5,6 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react'; import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';

View file

@ -1,14 +1,12 @@
import logo from '@/images/logo.svg'; 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'> <svg viewBox='0 0 261 66' className='logo logo--wordmark' role='img'>
<title>Mastodon</title> <title>Mastodon</title>
<use xlinkHref='#logo-symbol-wordmark' /> <use xlinkHref='#logo-symbol-wordmark' />
</svg> </svg>
); );
export const SymbolLogo = () => ( export const SymbolLogo: React.FC = () => (
<img src={logo} alt='Mastodon' className='logo logo--icon' /> <img src={logo} alt='Mastodon' className='logo logo--icon' />
); );
export default WordmarkLogo;

View file

@ -1,6 +1,6 @@
import { FormattedMessage } from 'react-intl'; 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 = () => ( const RegenerationIndicator = () => (
<div className='regeneration-indicator'> <div className='regeneration-indicator'>

View file

@ -20,6 +20,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
import { displayMedia, visibleReactions } from '../initial_state'; import { displayMedia, visibleReactions } from '../initial_state';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
@ -73,9 +74,7 @@ export const defaultMediaVisibility = (status, settings) => {
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextTypes = { static contextType = SensitiveMediaContext;
identity: PropTypes.object,
};
static propTypes = { static propTypes = {
containerId: PropTypes.string, containerId: PropTypes.string,
@ -132,8 +131,7 @@ class Status extends ImmutablePureComponent {
isCollapsed: false, isCollapsed: false,
autoCollapsed: false, autoCollapsed: false,
isExpanded: undefined, isExpanded: undefined,
showMedia: undefined, showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
statusId: undefined,
revealBehindCW: undefined, revealBehindCW: undefined,
showCard: false, showCard: false,
forceFilter: undefined, forceFilter: undefined,
@ -218,12 +216,6 @@ class Status extends ImmutablePureComponent {
updated = true; updated = true;
} }
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
update.showMedia = defaultMediaVisibility(nextProps.status, nextProps.settings);
update.statusId = nextProps.status.get('id');
updated = true;
}
if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) { if (nextProps.settings.getIn(['media', 'reveal_behind_cw']) !== prevState.revealBehindCW) {
update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']); update.revealBehindCW = nextProps.settings.getIn(['media', 'reveal_behind_cw']);
if (update.revealBehindCW) { if (update.revealBehindCW) {
@ -319,6 +311,18 @@ class Status extends ImmutablePureComponent {
if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) { if (snapshot !== null && this.props.updateScrollBottom && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top); this.props.updateScrollBottom(snapshot.height - snapshot.top);
} }
// This will potentially cause a wasteful redraw, but in most cases `Status` components are used
// with a `key` directly depending on their `id`, preventing re-use of the component across
// different IDs.
// But just in case this does change, reset the state on status change.
if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
this.setState({
showMedia: defaultMediaVisibility(this.props.status, this.props.settings) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
});
}
} }
componentWillUnmount() { componentWillUnmount() {

View file

@ -10,7 +10,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react'; import ExpandMoreIcon from '@/material-icons/400-24px/expand_more.svg?react';
import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server'; import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'flavours/glitch/actions/server';
@ -171,7 +170,8 @@ class About extends PureComponent {
<ol className='rules-list'> <ol className='rules-list'>
{server.get('rules').map(rule => ( {server.get('rules').map(rule => (
<li key={rule.get('id')}> <li key={rule.get('id')}>
<span className='rules-list__text'>{rule.get('text')}</span> <div className='rules-list__text'>{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
</li> </li>
))} ))}
</ol> </ol>
@ -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> <p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
<div className='about__domain-blocks'> {domainBlocks.get('items').size > 0 && (
{domainBlocks.get('items').map(block => ( <div className='about__domain-blocks'>
<div className='about__domain-blocks__domain' key={block.get('domain')}> {domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain__header'> <div className='about__domain-blocks__domain' key={block.get('domain')}>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6> <div className='about__domain-blocks__domain__header'>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span> <h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
</div> <span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p> <p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div> </div>
))} ))}
</div> </div>
)}
</> </>
) : ( ) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p> <p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>

View file

@ -21,6 +21,7 @@ import { Button } from 'flavours/glitch/components/button';
import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button'; import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
@ -206,7 +207,7 @@ class Header extends ImmutablePureComponent {
if (me !== account.get('id')) { if (me !== account.get('id')) {
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = ''; actionBtn = <Button disabled><LoadingIndicator /></Button>;
} else if (account.getIn(['relationship', 'requested'])) { } else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; actionBtn = <Button text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) { } else if (!account.getIn(['relationship', 'blocking'])) {
@ -347,15 +348,10 @@ class Header extends ImmutablePureComponent {
</a> </a>
<div className='account__header__tabs__buttons'> <div className='account__header__tabs__buttons'>
{!hidden && ( {!hidden && bellBtn}
<> {!hidden && shareBtn}
{actionBtn}
{bellBtn}
{shareBtn}
</>
)}
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' />
{!hidden && actionBtn}
</div> </div>
</div> </div>

View file

@ -12,7 +12,6 @@ import { Blurhash } from 'flavours/glitch/components/blurhash';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state'; import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/initial_state';
export default class MediaItem extends ImmutablePureComponent { export default class MediaItem extends ImmutablePureComponent {
static propTypes = { static propTypes = {

View file

@ -18,7 +18,6 @@ import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video'; import { formatTime, getPointerPosition, fileNameFromURL } from 'flavours/glitch/features/video';
import { Blurhash } from '../../components/blurhash'; import { Blurhash } from '../../components/blurhash';
import { displayMedia, useBlurhash } from '../../initial_state'; import { displayMedia, useBlurhash } from '../../initial_state';

View file

@ -26,7 +26,7 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props; const { settings, onChange, intl } = this.props;
return ( return (
<div> <div className='column-settings'>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
</div> </div>

View file

@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';

View file

@ -59,10 +59,11 @@ const Option = ({ multipleChoice, index, title, autoFocus }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions'])); const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
const lang = useSelector(state => state.getIn(['compose', 'language'])); const lang = useSelector(state => state.getIn(['compose', 'language']));
const maxOptions = useSelector(state => state.getIn(['server', 'server', 'configuration', 'polls', 'max_options']));
const handleChange = useCallback(({ target: { value } }) => { const handleChange = useCallback(({ target: { value } }) => {
dispatch(changePollOption(index, value)); dispatch(changePollOption(index, value, maxOptions));
}, [dispatch, index]); }, [dispatch, index, maxOptions]);
const handleSuggestionsFetchRequested = useCallback(token => { const handleSuggestionsFetchRequested = useCallback(token => {
dispatch(fetchComposeSuggestions(token)); dispatch(fetchComposeSuggestions(token));

View file

@ -4,14 +4,24 @@ import { uploadCompose } from '../../../actions/compose';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import UploadButton from '../components/upload_button'; import UploadButton from '../components/upload_button';
const mapStateToProps = state => ({ 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')))), const isPoll = state.getIn(['compose', 'poll']) !== null;
resetFileKey: state.getIn(['compose', 'resetFileKey']), 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 => ({ const mapDispatchToProps = dispatch => ({
onSelectFile (files) { onSelectFile(files) {
dispatch(uploadCompose(files)); dispatch(uploadCompose(files));
}, },

View file

@ -26,18 +26,20 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props; const { settings, onChange, intl } = this.props;
return ( return (
<div> <div className='column-settings'>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.basic' defaultMessage='Basic' /></span> <section>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} />
</div>
</section>
<div className='column-settings__row'> <section>
<SettingToggle settings={settings} settingPath={['conversations']} onChange={onChange} label={<FormattedMessage id='direct.group_by_conversations' defaultMessage='Group by conversation' />} /> <h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
</div>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> <div className='column-settings__row'>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
<div className='column-settings__row'> </div>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> </section>
</div>
</div> </div>
); );
} }

View file

@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns'; import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory'; import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';

View file

@ -8,7 +8,6 @@ import { NavLink, Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';

View file

@ -9,7 +9,6 @@ import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react'; import FindInPageIcon from '@/material-icons/400-24px/find_in_page.svg?react';
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
import TagIcon from '@/material-icons/400-24px/tag.svg?react'; import TagIcon from '@/material-icons/400-24px/tag.svg?react';

View file

@ -180,7 +180,7 @@ class SelectFilter extends PureComponent {
<div className='emoji-mart-search'> <div className='emoji-mart-search'>
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} autoFocus /> <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>
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>

View file

@ -6,7 +6,6 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { addColumn } from 'flavours/glitch/actions/columns'; import { addColumn } from 'flavours/glitch/actions/columns';
import { changeSetting } from 'flavours/glitch/actions/settings'; import { changeSetting } from 'flavours/glitch/actions/settings';
@ -46,28 +45,37 @@ const ColumnSettings = () => {
); );
return ( return (
<div> <div className='column-settings'>
<div className='column-settings__row'> <section>
<SettingToggle <div className='column-settings__row'>
settings={settings} <SettingToggle
settingPath={['onlyMedia']} settings={settings}
onChange={onChange} settingPath={['onlyMedia']}
label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} onChange={onChange}
/> label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />}
<SettingToggle />
settings={settings}
settingPath={['allowLocalOnly']} <SettingToggle
onChange={onChange} settings={settings}
label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />} settingPath={['allowLocalOnly']}
/> onChange={onChange}
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> label={<FormattedMessage id='firehose.column_settings.allow_local_only' defaultMessage='Show local-only posts in "All"' />}
<SettingText />
settings={settings} </div>
settingPath={['regex', 'body']} </section>
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)} <section>
/> <h3><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></h3>
</div>
<div className='column-settings__row'>
<SettingText
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
</section>
</div> </div>
); );
}; };

View file

@ -109,28 +109,28 @@ class ColumnSettings extends PureComponent {
const { settings, onChange } = this.props; const { settings, onChange } = this.props;
return ( return (
<div> <div className='column-settings'>
<div className='column-settings__row'> <section>
<div className='setting-toggle'> <div className='column-settings__row'>
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} /> <SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
<span className='setting-toggle__label'> <div className='setting-toggle'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' /> <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
</span>
<span className='setting-toggle__label'>
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
</span>
</div>
</div> </div>
</div>
{this.state.open && ( {this.state.open && (
<div className='column-settings__hashtags'> <div className='column-settings__hashtags'>
{this.modeSelect('any')} {this.modeSelect('any')}
{this.modeSelect('all')} {this.modeSelect('all')}
{this.modeSelect('none')} {this.modeSelect('none')}
</div> </div>
)} )}
</section>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
</div>
</div> </div>
); );
} }

View file

@ -35,75 +35,68 @@ export const ColumnSettings: React.FC = () => {
); );
return ( return (
<div> <div className='column-settings'>
<span className='column-settings__section'> <section>
<FormattedMessage <div className='column-settings__row'>
id='home.column_settings.basic' <SettingToggle
defaultMessage='Basic' prefix='home_timeline'
/> settings={settings}
</span> settingPath={['shows', 'reblog']}
onChange={onChange}
label={
<FormattedMessage
id='home.column_settings.show_reblogs'
defaultMessage='Show boosts'
/>
}
/>
<div className='column-settings__row'> <SettingToggle
<SettingToggle prefix='home_timeline'
prefix='home_timeline' settings={settings}
settings={settings} settingPath={['shows', 'reply']}
settingPath={['shows', 'reblog']} onChange={onChange}
onChange={onChange} label={
label={ <FormattedMessage
<FormattedMessage id='home.column_settings.show_replies'
id='home.column_settings.show_reblogs' defaultMessage='Show replies'
defaultMessage='Show boosts' />
/> }
} />
/>
</div>
<div className='column-settings__row'> <SettingToggle
<SettingToggle prefix='home_timeline'
prefix='home_timeline' settings={settings}
settings={settings} settingPath={['shows', 'direct']}
settingPath={['shows', 'reply']} onChange={onChange}
onChange={onChange} label={
label={ <FormattedMessage
<FormattedMessage id='home.column_settings.show_direct'
id='home.column_settings.show_replies' defaultMessage='Show private mentions'
defaultMessage='Show replies' />
/> }
} />
/> </div>
</div> </section>
<div className='column-settings__row'> <section aria-labelledby='home-column-advanced'>
<SettingToggle <h3 id='home-column-advanced'>
prefix='home_timeline' <FormattedMessage
settings={settings} id='home.column_settings.advanced'
settingPath={['shows', 'direct']} defaultMessage='Advanced'
onChange={onChange} />
label={ </h3>
<FormattedMessage
id='home.column_settings.show_direct'
defaultMessage='Show private mentions'
/>
}
/>
</div>
<span className='column-settings__section'> <div className='column-settings__row'>
<FormattedMessage <SettingText
id='home.column_settings.advanced' prefix='home_timeline'
defaultMessage='Advanced' settings={settings}
/> settingPath={['regex', 'body']}
</span> onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
<div className='column-settings__row'> />
<SettingText </div>
prefix='home_timeline' </section>
settings={settings}
settingPath={['regex', 'body']}
onChange={onChange}
label={intl.formatMessage(messages.filter_regex)}
/>
</div>
</div> </div>
); );
}; };

View file

@ -8,7 +8,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react'; import CampaignIcon from '@/material-icons/400-24px/campaign.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements'; import { fetchAnnouncements, toggleShowAnnouncements } from 'flavours/glitch/actions/announcements';

View file

@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import Column from 'flavours/glitch/components/column'; import Column from 'flavours/glitch/components/column';
import ColumnHeader from 'flavours/glitch/components/column_header'; import ColumnHeader from 'flavours/glitch/components/column_header';

View file

@ -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 ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
import { IconButton } from '../../../components/icon_button'; import { IconButton } from '../../../components/icon_button';

View file

@ -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 SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
const messages = defineMessages({ const messages = defineMessages({

View file

@ -193,37 +193,38 @@ class ListTimeline extends PureComponent {
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
> >
<div className='column-settings__row column-header__links'> <div className='column-settings'>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}> <section className='column-header__links'>
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' /> <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
</button> <Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</button>
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}> <button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' /> <Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button> </button>
</div> </section>
<div className='setting-toggle'> <section>
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} /> <div className='setting-toggle'>
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'> <Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
<FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' /> <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
</label> <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
</div> </label>
{ replies_policy !== undefined && (
<div role='group' aria-labelledby={`list-${id}-replies-policy`}>
<span id={`list-${id}-replies-policy`} className='column-settings__section'>
<FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' />
</span>
<div className='column-settings__row'>
{ ['none', 'list', 'followed'].map(policy => (
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
))}
</div> </div>
</div> </section>
)}
<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> </ColumnHeader>
<StatusListContainer <StatusListContainer

View file

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';
import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator'; import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';

View file

@ -224,6 +224,14 @@ class LocalSettingsPage extends PureComponent {
> >
<FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' /> <FormattedMessage id='settings.show_content_type_choice' defaultMessage='Show content-type choice when authoring toots' />
</LocalSettingsPageItem> </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 <LocalSettingsPageItem
settings={settings} settings={settings}
item={['side_arm']} item={['side_arm']}

View file

@ -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,
};

View file

@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react'; import DeleteForeverIcon from '@/material-icons/400-24px/delete_forever.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
export default class ClearColumnButton extends PureComponent { export default class ClearColumnButton extends PureComponent {
static propTypes = { static propTypes = {

View file

@ -7,6 +7,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';
import { CheckboxWithLabel } from './checkbox_with_label';
import ClearColumnButton from './clear_column_button'; import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button'; import GrantPermissionButton from './grant_permission_button';
import PillBarButton from './pill_bar_button'; import PillBarButton from './pill_bar_button';
@ -27,14 +28,32 @@ export default class ColumnSettings extends PureComponent {
alertsEnabled: PropTypes.bool, alertsEnabled: PropTypes.bool,
browserSupport: PropTypes.bool, browserSupport: PropTypes.bool,
browserPermission: PropTypes.string, browserPermission: PropTypes.string,
notificationPolicy: ImmutablePropTypes.map,
onChangePolicy: PropTypes.func.isRequired,
}; };
onPushChange = (path, checked) => { onPushChange = (path, checked) => {
this.props.onChange(['push', ...path], checked); this.props.onChange(['push', ...path], checked);
}; };
handleFilterNotFollowing = checked => {
this.props.onChangePolicy('filter_not_following', checked);
};
handleFilterNotFollowers = checked => {
this.props.onChangePolicy('filter_not_followers', checked);
};
handleFilterNewAccounts = checked => {
this.props.onChangePolicy('filter_new_accounts', checked);
};
handleFilterPrivateMentions = checked => {
this.props.onChangePolicy('filter_private_mentions', checked);
};
render () { render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props; const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />; const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />; const filterBarShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show_bar' defaultMessage='Show filter bar' />;
@ -47,48 +66,68 @@ export default class ColumnSettings extends PureComponent {
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
return ( return (
<div> <div className='column-settings'>
{alertsEnabled && browserSupport && browserPermission === 'denied' && ( {alertsEnabled && browserSupport && browserPermission === 'denied' && (
<div className='column-settings__row column-settings__row--with-margin'> <span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
<span className='warning-hint'><FormattedMessage id='notifications.permission_denied' defaultMessage='Desktop notifications are unavailable due to previously denied browser permissions request' /></span>
</div>
)} )}
{alertsEnabled && browserSupport && browserPermission === 'default' && ( {alertsEnabled && browserSupport && browserPermission === 'default' && (
<div className='column-settings__row column-settings__row--with-margin'> <span className='warning-hint'>
<span className='warning-hint'> <FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} />
<FormattedMessage id='notifications.permission_required' defaultMessage='Desktop notifications are unavailable because the required permission has not been granted.' /> <GrantPermissionButton onClick={onRequestNotificationPermission} /> </span>
</span>
</div>
)} )}
<div className='column-settings__row'> <section>
<ClearColumnButton onClick={onClear} /> <ClearColumnButton onClick={onClear} />
</div> </section>
<div role='group' aria-labelledby='notifications-unread-markers'> <section>
<span id='notifications-unread-markers' className='column-settings__section'> <h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
</span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} /> <CheckboxWithLabel checked={notificationPolicy.get('filter_not_following')} onChange={this.handleFilterNotFollowing}>
</div> <strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
</div> <span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
</CheckboxWithLabel>
<div role='group' aria-labelledby='notifications-filter-bar'> <CheckboxWithLabel checked={notificationPolicy.get('filter_not_followers')} onChange={this.handleFilterNotFollowers}>
<span id='notifications-filter-bar' className='column-settings__section'> <strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
<FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /> <span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
</span> </CheckboxWithLabel>
<CheckboxWithLabel checked={notificationPolicy.get('filter_new_accounts')} onChange={this.handleFilterNewAccounts}>
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
</CheckboxWithLabel>
<CheckboxWithLabel checked={notificationPolicy.get('filter_private_mentions')} onChange={this.handleFilterPrivateMentions}>
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
</CheckboxWithLabel>
</div>
</section>
<section role='group' aria-labelledby='notifications-filter-bar'>
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} /> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} /> <SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-follow'> <section role='group' aria-labelledby='notifications-unread-markers'>
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span> <h3 id='notifications-unread-markers'>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />
</h3>
<div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['showUnread']} onChange={onChange} label={unreadMarkersShowStr} />
</div>
</section>
<section role='group' aria-labelledby='notifications-follow'>
<h3 id='notifications-follow'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow']} onChange={onChange} label={alertStr} />
@ -96,10 +135,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-follow-request'> <section role='group' aria-labelledby='notifications-follow-request'>
<span id='notifications-follow-request' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></span> <h3 id='notifications-follow-request'><FormattedMessage id='notifications.column_settings.follow_request' defaultMessage='New follow requests:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'follow_request']} onChange={onChange} label={alertStr} />
@ -107,10 +146,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow_request']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow_request']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-favourite'> <section role='group' aria-labelledby='notifications-favourite'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></span> <h3 id='notifications-favourite'><FormattedMessage id='notifications.column_settings.favourite' defaultMessage='Favorites:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'favourite']} onChange={onChange} label={alertStr} />
@ -118,10 +157,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'favourite']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'favourite']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-reaction'> <section role='group' aria-labelledby='notifications-reaction'>
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span> <h3 id='notifications-reaction'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
@ -129,10 +168,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-mention'> <section role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span> <h3 id='notifications-mention'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'mention']} onChange={onChange} label={alertStr} />
@ -140,10 +179,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'mention']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'mention']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-reblog'> <section role='group' aria-labelledby='notifications-reblog'>
<span id='notifications-reblog' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></span> <h3 id='notifications-reblog'><FormattedMessage id='notifications.column_settings.reblog' defaultMessage='Boosts:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reblog']} onChange={onChange} label={alertStr} />
@ -151,10 +190,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reblog']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reblog']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-poll'> <section role='group' aria-labelledby='notifications-poll'>
<span id='notifications-poll' className='column-settings__section'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></span> <h3 id='notifications-poll'><FormattedMessage id='notifications.column_settings.poll' defaultMessage='Poll results:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'poll']} onChange={onChange} label={alertStr} />
@ -162,10 +201,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'poll']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'poll']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-status'> <section role='group' aria-labelledby='notifications-status'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></span> <h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.status' defaultMessage='New posts:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'status']} onChange={onChange} label={alertStr} />
@ -173,10 +212,10 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'status']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'status']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
<div role='group' aria-labelledby='notifications-update'> <section role='group' aria-labelledby='notifications-update'>
<span id='notifications-update' className='column-settings__section'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></span> <h3 id='notifications-update'><FormattedMessage id='notifications.column_settings.update' defaultMessage='Edits:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'update']} onChange={onChange} label={alertStr} />
@ -184,11 +223,11 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'update']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'update']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
{((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && ( {((this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
<div role='group' aria-labelledby='notifications-admin-sign-up'> <section role='group' aria-labelledby='notifications-admin-sign-up'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></span> <h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.sign_up']} onChange={onChange} label={alertStr} />
@ -196,12 +235,12 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.sign_up']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.sign_up']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
)} )}
{((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && ( {((this.context.identity.permissions & PERMISSION_MANAGE_REPORTS) === PERMISSION_MANAGE_REPORTS) && (
<div role='group' aria-labelledby='notifications-admin-report'> <section role='group' aria-labelledby='notifications-admin-report'>
<span id='notifications-status' className='column-settings__section'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></span> <h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.report' defaultMessage='New reports:' /></h3>
<div className='column-settings__pillbar'> <div className='column-settings__pillbar'>
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} /> <PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'admin.report']} onChange={onChange} label={alertStr} />
@ -209,7 +248,7 @@ export default class ColumnSettings extends PureComponent {
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'admin.report']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'admin.report']} onChange={onChange} label={soundStr} />
</div> </div>
</div> </section>
)} )}
</div> </div>
); );

View file

@ -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 StarIcon from '@/material-icons/400-24px/star.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
const tooltips = defineMessages({ const tooltips = defineMessages({
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' }, mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' }, favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favorites' },

View file

@ -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>
);
};

View file

@ -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,
};

View file

@ -5,7 +5,6 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import TuneIcon from '@/material-icons/400-24px/tune.svg?react'; import TuneIcon from '@/material-icons/400-24px/tune.svg?react';
import { requestBrowserPermission } from 'flavours/glitch/actions/notifications'; import { requestBrowserPermission } from 'flavours/glitch/actions/notifications';

View file

@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { showAlert } from '../../../actions/alerts'; import { showAlert } from '../../../actions/alerts';
import { openModal } from '../../../actions/modal'; import { openModal } from '../../../actions/modal';
import { setFilter, clearNotifications, requestBrowserPermission } from '../../../actions/notifications'; import { setFilter, clearNotifications, requestBrowserPermission, updateNotificationsPolicy } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { changeSetting } from '../../../actions/settings'; import { changeSetting } from '../../../actions/settings';
import ColumnSettings from '../components/column_settings'; import ColumnSettings from '../components/column_settings';
@ -21,6 +21,7 @@ const mapStateToProps = state => ({
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true), alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
browserSupport: state.getIn(['notifications', 'browserSupport']), browserSupport: state.getIn(['notifications', 'browserSupport']),
browserPermission: state.getIn(['notifications', 'browserPermission']), browserPermission: state.getIn(['notifications', 'browserPermission']),
notificationPolicy: state.get('notificationPolicy'),
}); });
const mapDispatchToProps = (dispatch, { intl }) => ({ const mapDispatchToProps = (dispatch, { intl }) => ({
@ -73,6 +74,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(requestBrowserPermission()); dispatch(requestBrowserPermission());
}, },
onChangePolicy (param, checked) {
dispatch(updateNotificationsPolicy({
[param]: checked,
}));
},
}); });
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));

View file

@ -37,6 +37,7 @@ import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list'; import ScrollableList from '../../components/scrollable_list';
import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container'; import NotificationPurgeButtonsContainer from '../../containers/notification_purge_buttons_container';
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
import NotificationsPermissionBanner from './components/notifications_permission_banner'; import NotificationsPermissionBanner from './components/notifications_permission_banner';
import ColumnSettingsContainer from './containers/column_settings_container'; import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container'; import FilterBarContainer from './containers/filter_bar_container';
@ -357,6 +358,9 @@ class Notifications extends PureComponent {
</ColumnHeader> </ColumnHeader>
{filterBarContainer} {filterBarContainer}
<FilteredNotificationsBanner />
{scrollContainer} {scrollContainer}
<Helmet> <Helmet>

View file

@ -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;

View file

@ -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;

View file

@ -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 CheckIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => { export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
const content = ( const content = (
<> <>

View file

@ -8,7 +8,6 @@ import { Link, Switch, Route, useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import illustration from '@/images/elephant_ui_conversation.svg'; import illustration from '@/images/elephant_ui_conversation.svg';
import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react'; import AccountCircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react'; import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';

View file

@ -9,7 +9,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';

View file

@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { Avatar } from 'flavours/glitch/components/avatar'; import { Avatar } from 'flavours/glitch/components/avatar';
import { DisplayName } from 'flavours/glitch/components/display_name'; import { DisplayName } from 'flavours/glitch/components/display_name';

View file

@ -11,7 +11,6 @@ import { connect } from 'react-redux';
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react'; import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
import { getStatusList } from 'flavours/glitch/selectors'; import { getStatusList } from 'flavours/glitch/selectors';
import { fetchPinnedStatuses } from '../../actions/pin_statuses'; import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import Column from '../ui/components/column'; import Column from '../ui/components/column';

View file

@ -25,18 +25,22 @@ class ColumnSettings extends PureComponent {
const { settings, onChange, intl } = this.props; const { settings, onChange, intl } = this.props;
return ( return (
<div> <div className='column-settings'>
<div className='column-settings__row'> <section>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> <div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} /> <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
{!settings.getIn(['other', 'onlyRemote']) && <SettingToggle settings={settings} settingPath={['other', 'allowLocalOnly']} onChange={onChange} label={<FormattedMessage id='community.column_settings.allow_local_only' defaultMessage='Show local-only toots' />} />} <SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
</div> {!settings.getIn(['other', 'onlyRemote']) && <SettingToggle settings={settings} settingPath={['other', 'allowLocalOnly']} onChange={onChange} label={<FormattedMessage id='community.column_settings.allow_local_only' defaultMessage='Show local-only toots' />} />}
</div>
</section>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> <section>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> <SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div> </div>
</section>
</div> </div>
); );
} }

View file

@ -7,7 +7,6 @@ import { Helmet } from 'react-helmet';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner'; import { DismissableBanner } from 'flavours/glitch/components/dismissable_banner';
import { domain } from 'flavours/glitch/initial_state'; import { domain } from 'flavours/glitch/initial_state';

View file

@ -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 RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import { fetchReblogs, expandReblogs } from '../../actions/interactions'; import { fetchReblogs, expandReblogs } from '../../actions/interactions';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { LoadingIndicator } from '../../components/loading_indicator'; import { LoadingIndicator } from '../../components/loading_indicator';

View file

@ -6,7 +6,6 @@ import classNames from 'classnames';
import CheckIcon from '@/material-icons/400-24px/done.svg?react'; import CheckIcon from '@/material-icons/400-24px/done.svg?react';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
export default class Option extends PureComponent { export default class Option extends PureComponent {
static propTypes = { static propTypes = {

View file

@ -19,6 +19,7 @@ class StatusCheckBox extends PureComponent {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
checked: PropTypes.bool, checked: PropTypes.bool,
onToggle: PropTypes.func.isRequired, onToggle: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}; };
handleStatusesToggle = (value, checked) => { handleStatusesToggle = (value, checked) => {

View file

@ -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 StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react'; import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg'; import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg'; import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react'; import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions';
import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links';

View file

@ -8,7 +8,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { followAccount } from 'flavours/glitch/actions/accounts'; import { followAccount } from 'flavours/glitch/actions/accounts';
import { Button } from 'flavours/glitch/components/button'; import { Button } from 'flavours/glitch/components/button';

View file

@ -1,27 +1,30 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { NavLink } from 'react-router-dom'; import { useRouteMatch, NavLink } from 'react-router-dom';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => { const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, onClick, href, method, badge, transparent, ...other }) => {
const match = useRouteMatch(to);
const className = classNames('column-link', { 'column-link--transparent': transparent }); const className = classNames('column-link', { 'column-link--transparent': transparent });
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon; const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon;
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement);
const active = match?.isExact;
if (href) { if (href) {
return ( return (
<a href={href} className={className} data-method={method} title={text} {...other}> <a href={href} className={className} data-method={method} title={text} {...other}>
{iconElement} {active ? activeIconElement : iconElement}
<span>{text}</span> <span>{text}</span>
{badgeElement} {badgeElement}
</a> </a>
); );
} else if (to) { } else if (to) {
return ( return (
<NavLink to={to} className={className} title={text} {...other}> <NavLink to={to} className={className} title={text} exact {...other}>
{iconElement} {active ? activeIconElement : iconElement}
<span>{text}</span> <span>{text}</span>
{badgeElement} {badgeElement}
</NavLink> </NavLink>
@ -46,6 +49,8 @@ const ColumnLink = ({ icon, iconComponent, text, to, onClick, href, method, badg
ColumnLink.propTypes = { ColumnLink.propTypes = {
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
iconComponent: PropTypes.func, iconComponent: PropTypes.func,
activeIcon: PropTypes.node,
activeIconComponent: PropTypes.func,
text: PropTypes.string.isRequired, text: PropTypes.string.isRequired,
to: PropTypes.string, to: PropTypes.string,
onClick: PropTypes.func, onClick: PropTypes.func,

View file

@ -58,6 +58,7 @@ const TabsBarPortal = () => {
export default class ColumnsArea extends ImmutablePureComponent { export default class ColumnsArea extends ImmutablePureComponent {
static propTypes = { static propTypes = {
columns: ImmutablePropTypes.list.isRequired, columns: ImmutablePropTypes.list.isRequired,
isModalOpen: PropTypes.bool.isRequired,
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
children: PropTypes.node, children: PropTypes.node,
openSettings: PropTypes.func, openSettings: PropTypes.func,
@ -145,7 +146,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
}; };
render () { render () {
const { columns, children, singleColumn, openSettings } = this.props; const { columns, children, singleColumn, isModalOpen, openSettings } = this.props;
const { renderComposePanel } = this.state; const { renderComposePanel } = this.state;
if (singleColumn) { if (singleColumn) {
@ -172,7 +173,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
} }
return ( return (
<div className='columns-area' ref={this.setRef}> <div className={`columns-area ${ isModalOpen ? 'unscrollable' : '' }`} ref={this.setRef}>
{columns.map(column => { {columns.map(column => {
const params = column.get('params', null) === null ? null : column.get('params').toJS(); const params = column.get('params', null) === null ? null : column.get('params').toJS();
const other = params && params.other ? params.other : {}; const other = params && params.other ? params.other : {};

View file

@ -4,7 +4,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import api from 'flavours/glitch/api'; import api from 'flavours/glitch/api';
import { IconButton } from 'flavours/glitch/components/icon_button'; import { IconButton } from 'flavours/glitch/components/icon_button';

View file

@ -5,7 +5,6 @@ import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { fetchFilters, createFilter, createFilterStatus } from 'flavours/glitch/actions/filters'; import { fetchFilters, createFilter, createFilterStatus } from 'flavours/glitch/actions/filters';
import { fetchStatus } from 'flavours/glitch/actions/statuses'; import { fetchStatus } from 'flavours/glitch/actions/statuses';

View file

@ -181,14 +181,14 @@ class FocalPointModal extends ImmutablePureComponent {
handleKeyDown = (e) => { handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
e.stopPropagation();
this.props.onChangeDescription(e.target.value); 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); this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
}; };
@ -318,7 +318,7 @@ class FocalPointModal extends ImmutablePureComponent {
</div> </div>
<div className='report-modal__container'> <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>} {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 && ( {thumbnailable && (
@ -367,12 +367,23 @@ class FocalPointModal extends ImmutablePureComponent {
</div> </div>
<div className='setting-text__toolbar'> <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} /> <CharacterCounter max={1500} text={detecting ? '' : description} />
</div> </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} /> <Button
</div> 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'> <div className='focal-point-modal__content'>
{focals && ( {focals && (

View file

@ -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));

View file

@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import { useEffect } from 'react';
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import ImmutablePropTypes from 'react-immutable-proptypes'; import { useDispatch, useSelector } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchLists } from 'flavours/glitch/actions/lists'; import { fetchLists } from 'flavours/glitch/actions/lists';
@ -18,40 +17,25 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4); return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4);
}); });
const mapStateToProps = state => ({ export const ListPanel = () => {
lists: getOrderedLists(state), const dispatch = useDispatch();
}); const lists = useSelector(state => getOrderedLists(state));
class ListPanel extends ImmutablePureComponent { useEffect(() => {
static propTypes = {
dispatch: PropTypes.func.isRequired,
lists: ImmutablePropTypes.list,
};
componentDidMount () {
const { dispatch } = this.props;
dispatch(fetchLists()); dispatch(fetchLists());
}, [dispatch]);
if (!lists || lists.isEmpty()) {
return null;
} }
render () { return (
const { lists } = this.props; <div className='list-panel'>
<hr />
if (!lists || lists.isEmpty()) { {lists.map(list => (
return null; <ColumnLink icon='list-ul' key={list.get('id')} iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
} ))}
</div>
return ( );
<div className='list-panel'> };
<hr />
{lists.map(list => (
<ColumnLink icon='list-ul' iconComponent={ListAltIcon} key={list.get('id')} strict text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
))}
</div>
);
}
}
export default connect(mapStateToProps)(ListPanel);

View file

@ -1,19 +1,33 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Component } from 'react'; import { Component, useEffect } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl, useIntl } from 'react-intl';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react'; import { useSelector, useDispatch } from 'react-redux';
import BookmarksActiveIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react';
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import HomeActiveIcon from '@/material-icons/400-24px/home-fill.svg?react';
import HomeIcon from '@/material-icons/400-24px/home.svg?react';
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react'; import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import MailActiveIcon from '@/material-icons/400-24px/mail-fill.svg?react';
import MailIcon from '@/material-icons/400-24px/mail.svg?react'; import MailIcon from '@/material-icons/400-24px/mail.svg?react';
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react'; import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react';
import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarIcon from '@/material-icons/400-24px/star.svg?react';
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
import { IconWithBadge } from 'flavours/glitch/components/icon_with_badge';
import { NavigationPortal } from 'flavours/glitch/components/navigation_portal'; import { NavigationPortal } from 'flavours/glitch/components/navigation_portal';
import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state'; import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
import { transientSingleColumn } from 'flavours/glitch/is_mobile'; import { transientSingleColumn } from 'flavours/glitch/is_mobile';
@ -21,9 +35,7 @@ import { preferencesLink } from 'flavours/glitch/utils/backend_links';
import ColumnLink from './column_link'; import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner'; import DisabledAccountBanner from './disabled_account_banner';
import FollowRequestsColumnLink from './follow_requests_column_link'; import { ListPanel } from './list_panel';
import ListPanel from './list_panel';
import NotificationsCounterIcon from './notifications_counter_icon';
import SignInBanner from './sign_in_banner'; import SignInBanner from './sign_in_banner';
const messages = defineMessages({ const messages = defineMessages({
@ -42,8 +54,48 @@ const messages = defineMessages({
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' }, advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' }, openedInClassicInterface: { id: 'navigation_bar.opened_in_classic_interface', defaultMessage: 'Posts, accounts, and other specific pages are opened by default in the classic web interface.' },
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' }, app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
}); });
const NotificationsLink = () => {
const count = useSelector(state => state.getIn(['local_settings', 'notifications', 'tab_badge']) ? state.getIn(['notifications', 'unread']) : 0);
const intl = useIntl();
return (
<ColumnLink
transparent
to='/notifications'
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
text={intl.formatMessage(messages.notifications)}
/>
);
};
const FollowRequestsLink = () => {
const count = useSelector(state => state.getIn(['user_lists', 'follow_requests', 'items'])?.size ?? 0);
const intl = useIntl();
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchFollowRequests());
}, [dispatch]);
if (count === 0) {
return null;
}
return (
<ColumnLink
transparent
to='/follow_requests'
icon={<IconWithBadge id='user-plus' icon={PersonAddIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='user-plus' icon={PersonAddActiveIcon} count={count} className='column-link__icon' />}
text={intl.formatMessage(messages.followRequests)}
/>
);
};
class NavigationPanel extends Component { class NavigationPanel extends Component {
static contextTypes = { static contextTypes = {
@ -84,9 +136,9 @@ class NavigationPanel extends Component {
{signedIn && ( {signedIn && (
<> <>
<ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} text={intl.formatMessage(messages.home)} /> <ColumnLink transparent to='/home' icon='home' iconComponent={HomeIcon} activeIconComponent={HomeActiveIcon} text={intl.formatMessage(messages.home)} />
<ColumnLink transparent to='/notifications' icon={<NotificationsCounterIcon className='column-link__icon' />} text={intl.formatMessage(messages.notifications)} /> <NotificationsLink />
<FollowRequestsColumnLink /> <FollowRequestsLink />
</> </>
)} )}
@ -109,10 +161,10 @@ class NavigationPanel extends Component {
{signedIn && ( {signedIn && (
<> <>
<ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} text={intl.formatMessage(messages.direct)} /> <ColumnLink transparent to='/conversations' icon='at' iconComponent={MailIcon} activeIconComponent={MailActiveIcon} text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} /> <ColumnLink transparent to='/bookmarks' icon='bookmarks' iconComponent={BookmarksIcon} activeIconComponent={BookmarksActiveIcon} text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} /> <ColumnLink transparent to='/favourites' icon='star' iconComponent={StarIcon} activeIconComponent={StarActiveIcon} text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} /> <ColumnLink transparent to='/lists' icon='list-ul' iconComponent={ListAltIcon} activeIconComponent={ListAltActiveIcon} text={intl.formatMessage(messages.lists)} />
<ListPanel /> <ListPanel />

Some files were not shown because too many files have changed in this diff Show more