Merge pull request #912 from kmycode/upstream-20241126
Upstream 20241126
This commit is contained in:
commit
8d94a8dfac
387 changed files with 9794 additions and 9803 deletions
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
|
@ -39,4 +39,4 @@ jobs:
|
|||
bundler-cache: true
|
||||
|
||||
- name: Run bundler-audit
|
||||
run: bundle exec bundler-audit check --update
|
||||
run: bin/bundler-audit check --update
|
||||
|
|
10
.github/workflows/check-i18n.yml
vendored
10
.github/workflows/check-i18n.yml
vendored
|
@ -41,18 +41,18 @@ jobs:
|
|||
git diff --exit-code
|
||||
|
||||
- name: Check locale file normalization
|
||||
run: bundle exec i18n-tasks check-normalized
|
||||
run: bin/i18n-tasks check-normalized
|
||||
|
||||
- name: Check for unused strings
|
||||
run: bundle exec i18n-tasks unused
|
||||
run: bin/i18n-tasks unused
|
||||
|
||||
- name: Check for missing strings in English YML
|
||||
run: |
|
||||
bundle exec i18n-tasks add-missing -l en
|
||||
bin/i18n-tasks add-missing -l en
|
||||
git diff --exit-code
|
||||
|
||||
- name: Check for wrong string interpolations
|
||||
run: bundle exec i18n-tasks check-consistent-interpolations
|
||||
run: bin/i18n-tasks check-consistent-interpolations
|
||||
|
||||
- name: Check that all required locale files exist
|
||||
run: bundle exec rake repo:check_locales_files
|
||||
run: bin/rake repo:check_locales_files
|
||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
uses: ./.github/actions/setup-ruby
|
||||
|
||||
- name: Run i18n normalize task
|
||||
run: bundle exec i18n-tasks normalize
|
||||
run: bin/i18n-tasks normalize
|
||||
|
||||
# Create or update the pull request
|
||||
- name: Create Pull Request
|
||||
|
|
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
|
@ -46,4 +46,4 @@ jobs:
|
|||
- name: Run haml-lint
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
|
||||
bundle exec haml-lint --reporter github
|
||||
bin/haml-lint --reporter github
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# syntax=docker/dockerfile:1.10
|
||||
# syntax=docker/dockerfile:1.11
|
||||
|
||||
# This file is designed for production server deployment, not local development work
|
||||
# For a containerized local dev environment, see: https://github.com/mastodon/mastodon/blob/main/README.md#docker
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -114,7 +114,7 @@ group :opentelemetry do
|
|||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.33.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||
|
|
43
Gemfile.lock
43
Gemfile.lock
|
@ -93,18 +93,17 @@ GEM
|
|||
annotaterb (4.13.0)
|
||||
ast (2.4.2)
|
||||
attr_required (1.0.2)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1008.0)
|
||||
aws-sdk-core (3.213.0)
|
||||
aws-partitions (1.1013.0)
|
||||
aws-sdk-core (3.214.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-kms (1.96.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.171.0)
|
||||
aws-sdk-s3 (1.174.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
|
@ -200,7 +199,7 @@ GEM
|
|||
activerecord (>= 4.2, < 9.0)
|
||||
docile (1.4.1)
|
||||
domain_name (0.6.20240107)
|
||||
doorkeeper (5.7.1)
|
||||
doorkeeper (5.8.0)
|
||||
railties (>= 5)
|
||||
dotenv (3.1.4)
|
||||
drb (2.2.1)
|
||||
|
@ -346,10 +345,11 @@ GEM
|
|||
json-ld-preloaded (3.3.1)
|
||||
json-ld (~> 3.3)
|
||||
rdf (~> 3.3)
|
||||
json-schema (5.0.1)
|
||||
json-schema (5.1.0)
|
||||
addressable (~> 2.8)
|
||||
jsonapi-renderer (0.2.2)
|
||||
jwt (2.7.1)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
|
@ -411,7 +411,7 @@ GEM
|
|||
minitest (5.25.1)
|
||||
msgpack (1.7.5)
|
||||
multi_json (1.15.0)
|
||||
mutex_m (0.2.0)
|
||||
mutex_m (0.3.0)
|
||||
net-http (0.5.0)
|
||||
uri
|
||||
net-imap (0.5.1)
|
||||
|
@ -424,7 +424,7 @@ GEM
|
|||
timeout
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.3)
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
|
@ -478,13 +478,13 @@ GEM
|
|||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-action_pack (0.9.0)
|
||||
opentelemetry-instrumentation-action_pack (0.10.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (~> 0.21)
|
||||
opentelemetry-instrumentation-action_view (0.7.2)
|
||||
opentelemetry-instrumentation-action_view (0.7.3)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-active_support (~> 0.6)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_job (0.7.8)
|
||||
opentelemetry-api (~> 1.0)
|
||||
|
@ -527,10 +527,10 @@ GEM
|
|||
opentelemetry-instrumentation-rack (0.25.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rails (0.32.0)
|
||||
opentelemetry-instrumentation-rails (0.33.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.10.0)
|
||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_record (~> 0.8.0)
|
||||
|
@ -580,7 +580,7 @@ GEM
|
|||
psych (5.2.0)
|
||||
stringio
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
puma (6.5.0)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.4.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
@ -752,7 +752,7 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securerandom (0.3.2)
|
||||
selenium-webdriver (4.26.0)
|
||||
selenium-webdriver (4.27.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
@ -844,9 +844,8 @@ GEM
|
|||
public_suffix
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
webauthn (3.1.0)
|
||||
webauthn (3.2.2)
|
||||
android_key_attestation (~> 0.3.0)
|
||||
awrence (~> 1.1)
|
||||
bindata (~> 2.4)
|
||||
cbor (~> 0.5.9)
|
||||
cose (~> 1.1)
|
||||
|
@ -966,7 +965,7 @@ DEPENDENCIES
|
|||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.33.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||
opentelemetry-sdk (~> 1.4)
|
||||
|
@ -1031,7 +1030,7 @@ DEPENDENCIES
|
|||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.5p100
|
||||
ruby 3.3.6p108
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.22
|
||||
2.5.23
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AntennasController < ApplicationController
|
||||
layout 'admin'
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_antenna, only: [:edit, :update, :destroy]
|
||||
before_action :set_body_classes
|
||||
before_action :set_cache_headers
|
||||
|
||||
def index
|
||||
@antennas = current_account.antennas.includes(:antenna_domains).includes(:antenna_tags).includes(:antenna_accounts)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
if @antenna.update(resource_params)
|
||||
redirect_to antennas_path
|
||||
else
|
||||
render action: :edit
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@antenna.destroy
|
||||
redirect_to antennas_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_antenna
|
||||
@antenna = current_account.antennas.find(params[:id])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:antenna).permit(:title, :available, :expires_in)
|
||||
end
|
||||
|
||||
def thin_resource_params
|
||||
params.require(:antenna).permit(:title)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
@body_classes = 'admin'
|
||||
end
|
||||
|
||||
def set_cache_headers
|
||||
response.cache_control.replace(private: true, no_store: true)
|
||||
end
|
||||
end
|
|
@ -15,17 +15,12 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def create
|
||||
ApplicationRecord.transaction do
|
||||
list_accounts.each do |account|
|
||||
@list.accounts << account
|
||||
end
|
||||
end
|
||||
|
||||
AddAccountsToListService.new.call(@list, Account.find(account_ids))
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
ListAccount.where(list: @list, account_id: account_ids).destroy_all
|
||||
RemoveAccountsFromListService.new.call(@list, Account.where(id: account_ids))
|
||||
render_empty
|
||||
end
|
||||
|
||||
|
@ -43,10 +38,6 @@ class Api::V1::Lists::AccountsController < Api::BaseController
|
|||
end
|
||||
end
|
||||
|
||||
def list_accounts
|
||||
Account.find(account_ids)
|
||||
end
|
||||
|
||||
def account_ids
|
||||
Array(resource_params[:account_ids])
|
||||
end
|
||||
|
|
|
@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base
|
|||
helper_method :use_seamless_external_login?
|
||||
helper_method :sso_account_settings
|
||||
helper_method :limited_federation_mode?
|
||||
helper_method :body_class_string
|
||||
helper_method :skip_csrf_meta_tags?
|
||||
|
||||
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
|
||||
|
@ -158,10 +157,6 @@ class ApplicationController < ActionController::Base
|
|||
current_user.setting_theme
|
||||
end
|
||||
|
||||
def body_class_string
|
||||
@body_classes || ''
|
||||
end
|
||||
|
||||
def respond_with_error(code)
|
||||
respond_to do |format|
|
||||
format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }
|
||||
|
|
|
@ -38,7 +38,7 @@ module CacheConcern
|
|||
return render(options)
|
||||
end
|
||||
|
||||
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
|
||||
key = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields]&.join(',')].compact.join(':')
|
||||
expires_in = options.delete(:expires_in) || 3.minutes
|
||||
body = Rails.cache.read(key, raw: true)
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def body_classes
|
||||
output = body_class_string.split
|
||||
output = []
|
||||
output << content_for(:body_classes)
|
||||
output << "theme-#{current_theme.parameterize}"
|
||||
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import api from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const ANTENNA_FETCH_REQUEST = 'ANTENNA_FETCH_REQUEST';
|
||||
export const ANTENNA_FETCH_SUCCESS = 'ANTENNA_FETCH_SUCCESS';
|
||||
export const ANTENNA_FETCH_FAIL = 'ANTENNA_FETCH_FAIL';
|
||||
|
@ -11,121 +8,10 @@ export const ANTENNAS_FETCH_REQUEST = 'ANTENNAS_FETCH_REQUEST';
|
|||
export const ANTENNAS_FETCH_SUCCESS = 'ANTENNAS_FETCH_SUCCESS';
|
||||
export const ANTENNAS_FETCH_FAIL = 'ANTENNAS_FETCH_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_TITLE_CHANGE = 'ANTENNA_EDITOR_TITLE_CHANGE';
|
||||
export const ANTENNA_EDITOR_RESET = 'ANTENNA_EDITOR_RESET';
|
||||
export const ANTENNA_EDITOR_SETUP = 'ANTENNA_EDITOR_SETUP';
|
||||
|
||||
export const ANTENNA_CREATE_REQUEST = 'ANTENNA_CREATE_REQUEST';
|
||||
export const ANTENNA_CREATE_SUCCESS = 'ANTENNA_CREATE_SUCCESS';
|
||||
export const ANTENNA_CREATE_FAIL = 'ANTENNA_CREATE_FAIL';
|
||||
|
||||
export const ANTENNA_UPDATE_REQUEST = 'ANTENNA_UPDATE_REQUEST';
|
||||
export const ANTENNA_UPDATE_SUCCESS = 'ANTENNA_UPDATE_SUCCESS';
|
||||
export const ANTENNA_UPDATE_FAIL = 'ANTENNA_UPDATE_FAIL';
|
||||
|
||||
export const ANTENNA_DELETE_REQUEST = 'ANTENNA_DELETE_REQUEST';
|
||||
export const ANTENNA_DELETE_SUCCESS = 'ANTENNA_DELETE_SUCCESS';
|
||||
export const ANTENNA_DELETE_FAIL = 'ANTENNA_DELETE_FAIL';
|
||||
|
||||
export const ANTENNA_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_ACCOUNTS_FETCH_REQUEST';
|
||||
export const ANTENNA_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const ANTENNA_ACCOUNTS_FETCH_FAIL = 'ANTENNA_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_SUGGESTIONS_CHANGE = 'ANTENNA_EDITOR_SUGGESTIONS_CHANGE';
|
||||
export const ANTENNA_EDITOR_SUGGESTIONS_READY = 'ANTENNA_EDITOR_SUGGESTIONS_READY';
|
||||
export const ANTENNA_EDITOR_SUGGESTIONS_CLEAR = 'ANTENNA_EDITOR_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_REQUEST = 'ANTENNA_EDITOR_ADD_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_SUCCESS = 'ANTENNA_EDITOR_ADD_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_FAIL = 'ANTENNA_EDITOR_ADD_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_REQUEST = 'ANTENNA_EDITOR_REMOVE_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_FAIL = 'ANTENNA_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST';
|
||||
export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL = 'ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST = 'ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST';
|
||||
export const ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS = 'ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS';
|
||||
export const ANTENNA_EDITOR_FETCH_DOMAINS_FAIL = 'ANTENNA_EDITOR_FETCH_DOMAINS_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_DOMAIN_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_DOMAIN_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDEDOMAIN_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST = 'ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST';
|
||||
export const ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS = 'ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS';
|
||||
export const ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL = 'ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_KEYWORD_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_KEYWORD_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_FETCH_TAGS_REQUEST = 'ANTENNA_EDITOR_FETCH_TAGS_REQUEST';
|
||||
export const ANTENNA_EDITOR_FETCH_TAGS_SUCCESS = 'ANTENNA_EDITOR_FETCH_TAGS_SUCCESS';
|
||||
export const ANTENNA_EDITOR_FETCH_TAGS_FAIL = 'ANTENNA_EDITOR_FETCH_TAGS_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_TAG_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_TAG_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_TAG_FAIL = 'ANTENNA_EDITOR_ADD_TAG_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_TAG_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_TAG_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_TAG_FAIL';
|
||||
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL = 'ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL';
|
||||
|
||||
export const ANTENNA_ADDER_RESET = 'ANTENNA_ADDER_RESET';
|
||||
export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP';
|
||||
|
||||
export const ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST';
|
||||
export const ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS';
|
||||
export const ANTENNA_ADDER_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_ANTENNAS_FETCH_FAIL';
|
||||
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST';
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS';
|
||||
export const ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL = 'ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL';
|
||||
|
||||
export const fetchAntenna = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['antennas', id])) {
|
||||
return;
|
||||
|
@ -176,98 +62,6 @@ export const fetchAntennasFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const submitAntennaEditor = shouldReset => (dispatch, getState) => {
|
||||
const antennaId = getState().getIn(['antennaEditor', 'antennaId']);
|
||||
const title = getState().getIn(['antennaEditor', 'title']);
|
||||
|
||||
if (antennaId === null) {
|
||||
dispatch(createAntenna(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateAntenna(antennaId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupAntennaEditor = antennaId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ANTENNA_EDITOR_SETUP,
|
||||
antenna: getState().getIn(['antennas', antennaId]),
|
||||
});
|
||||
|
||||
dispatch(fetchAntennaAccounts(antennaId));
|
||||
};
|
||||
|
||||
export const setupExcludeAntennaEditor = antennaId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ANTENNA_EDITOR_SETUP,
|
||||
antenna: getState().getIn(['antennas', antennaId]),
|
||||
});
|
||||
|
||||
dispatch(fetchAntennaExcludeAccounts(antennaId));
|
||||
};
|
||||
|
||||
export const changeAntennaEditorTitle = value => ({
|
||||
type: ANTENNA_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createAntenna = (title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(createAntennaRequest());
|
||||
|
||||
api(getState).post('/api/v1/antennas', { title }).then(({ data }) => {
|
||||
dispatch(createAntennaSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetAntennaEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createAntennaFail(err)));
|
||||
};
|
||||
|
||||
export const createAntennaRequest = () => ({
|
||||
type: ANTENNA_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createAntennaSuccess = antenna => ({
|
||||
type: ANTENNA_CREATE_SUCCESS,
|
||||
antenna,
|
||||
});
|
||||
|
||||
export const createAntennaFail = error => ({
|
||||
type: ANTENNA_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateAntenna = (id, title, shouldReset, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
|
||||
dispatch(updateAntennaRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, ltl, with_media_only, ignore_reblog, insert_feeds }).then(({ data }) => {
|
||||
dispatch(updateAntennaSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetAntennaEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateAntennaFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateAntennaRequest = id => ({
|
||||
type: ANTENNA_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateAntennaSuccess = antenna => ({
|
||||
type: ANTENNA_UPDATE_SUCCESS,
|
||||
antenna,
|
||||
});
|
||||
|
||||
export const updateAntennaFail = (id, error) => ({
|
||||
type: ANTENNA_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetAntennaEditor = () => ({
|
||||
type: ANTENNA_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteAntenna = id => (dispatch, getState) => {
|
||||
dispatch(deleteAntennaRequest(id));
|
||||
|
||||
|
@ -291,696 +85,3 @@ export const deleteAntennaFail = (id, error) => ({
|
|||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaAccounts = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaAccountsRequest(antennaId));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${antennaId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAntennaAccountsSuccess(antennaId, data));
|
||||
}).catch(err => dispatch(fetchAntennaAccountsFail(antennaId, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaAccountsRequest = id => ({
|
||||
type: ANTENNA_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaAccountsSuccess = (id, accounts, next) => ({
|
||||
type: ANTENNA_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchAntennaAccountsFail = (id, error) => ({
|
||||
type: ANTENNA_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaExcludeAccounts = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaExcludeAccountsRequest(antennaId));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAntennaExcludeAccountsSuccess(antennaId, data));
|
||||
}).catch(err => dispatch(fetchAntennaExcludeAccountsFail(antennaId, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaExcludeAccountsRequest = id => ({
|
||||
type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaExcludeAccountsSuccess = (id, accounts, next) => ({
|
||||
type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchAntennaExcludeAccountsFail = (id, error) => ({
|
||||
type: ANTENNA_EXCLUDE_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaSuggestions = q => (dispatch, getState) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchAntennaSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
export const fetchAntennaSuggestionsReady = (query, accounts) => ({
|
||||
type: ANTENNA_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearAntennaSuggestions = () => ({
|
||||
type: ANTENNA_EDITOR_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeAntennaSuggestions = value => ({
|
||||
type: ANTENNA_EDITOR_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToAntennaEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
|
||||
};
|
||||
|
||||
export const addToAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(addToAntennaRequest(antennaId, accountId));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(addToAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addToAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addExcludeToAntennaEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addExcludeToAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
|
||||
};
|
||||
|
||||
export const addExcludeToAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(addExcludeToAntennaRequest(antennaId, accountId));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/exclude_accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addExcludeToAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(addExcludeToAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addExcludeToAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addExcludeToAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addExcludeToAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromAntennaEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(removeFromAntennaRequest(antennaId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeFromAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeExcludeFromAntennaEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeExcludeFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
|
||||
};
|
||||
|
||||
export const removeExcludeFromAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(removeExcludeFromAntennaRequest(antennaId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeExcludeFromAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(removeExcludeFromAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeExcludeFromAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeExcludeFromAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeExcludeFromAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaDomains = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaDomainsRequest(antennaId));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${antennaId}/domains`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(fetchAntennaDomainsSuccess(antennaId, data));
|
||||
}).catch(err => dispatch(fetchAntennaDomainsFail(antennaId, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaDomainsRequest = id => ({
|
||||
type: ANTENNA_EDITOR_FETCH_DOMAINS_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaDomainsSuccess = (id, domains) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS,
|
||||
id,
|
||||
domains,
|
||||
});
|
||||
|
||||
export const fetchAntennaDomainsFail = (id, error) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_DOMAINS_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addDomainToAntenna = (antennaId, domain) => (dispatch, getState) => {
|
||||
dispatch(addDomainToAntennaRequest(antennaId, domain));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/domains`, { domains: [domain] })
|
||||
.then(() => dispatch(addDomainToAntennaSuccess(antennaId, domain)))
|
||||
.catch(err => dispatch(addDomainToAntennaFail(antennaId, domain, err)));
|
||||
};
|
||||
|
||||
export const addDomainToAntennaRequest = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_ADD_DOMAIN_REQUEST,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const addDomainToAntennaSuccess = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const addDomainToAntennaFail = (antennaId, domain, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_DOMAIN_FAIL,
|
||||
antennaId,
|
||||
domain,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => {
|
||||
dispatch(removeDomainFromAntennaRequest(antennaId, domain));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/domains`, { params: { domains: [domain] } })
|
||||
.then(() => dispatch(removeDomainFromAntennaSuccess(antennaId, domain)))
|
||||
.catch(err => dispatch(removeDomainFromAntennaFail(antennaId, domain, err)));
|
||||
};
|
||||
|
||||
export const removeDomainFromAntennaRequest = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL,
|
||||
antennaId,
|
||||
domain,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addExcludeDomainToAntenna = (antennaId, domain) => (dispatch, getState) => {
|
||||
dispatch(addExcludeDomainToAntennaRequest(antennaId, domain));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/exclude_domains`, { domains: [domain] })
|
||||
.then(() => dispatch(addExcludeDomainToAntennaSuccess(antennaId, domain)))
|
||||
.catch(err => dispatch(addExcludeDomainToAntennaFail(antennaId, domain, err)));
|
||||
};
|
||||
|
||||
export const addExcludeDomainToAntennaRequest = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_REQUEST,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const addExcludeDomainToAntennaSuccess = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_SUCCESS,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const addExcludeDomainToAntennaFail = (antennaId, domain, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_DOMAIN_FAIL,
|
||||
antennaId,
|
||||
domain,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeExcludeDomainFromAntenna = (antennaId, domain) => (dispatch, getState) => {
|
||||
dispatch(removeExcludeDomainFromAntennaRequest(antennaId, domain));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_domains`, { params: { domains: [domain] } })
|
||||
.then(() => dispatch(removeExcludeDomainFromAntennaSuccess(antennaId, domain)))
|
||||
.catch(err => dispatch(removeExcludeDomainFromAntennaFail(antennaId, domain, err)));
|
||||
};
|
||||
|
||||
export const removeExcludeDomainFromAntennaRequest = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_REQUEST,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const removeExcludeDomainFromAntennaSuccess = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_SUCCESS,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const removeExcludeDomainFromAntennaFail = (antennaId, domain, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_DOMAIN_FAIL,
|
||||
antennaId,
|
||||
domain,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaKeywords = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaKeywordsRequest(antennaId));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${antennaId}/keywords`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(fetchAntennaKeywordsSuccess(antennaId, data));
|
||||
}).catch(err => dispatch(fetchAntennaKeywordsFail(antennaId, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaKeywordsRequest = id => ({
|
||||
type: ANTENNA_EDITOR_FETCH_KEYWORDS_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaKeywordsSuccess = (id, keywords) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS,
|
||||
id,
|
||||
keywords,
|
||||
});
|
||||
|
||||
export const fetchAntennaKeywordsFail = (id, error) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_KEYWORDS_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => {
|
||||
dispatch(addKeywordToAntennaRequest(antennaId, keyword));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/keywords`, { keywords: [keyword] })
|
||||
.then(() => dispatch(addKeywordToAntennaSuccess(antennaId, keyword)))
|
||||
.catch(err => dispatch(addKeywordToAntennaFail(antennaId, keyword, err)));
|
||||
};
|
||||
|
||||
export const addKeywordToAntennaRequest = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_ADD_KEYWORD_REQUEST,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const addKeywordToAntennaSuccess = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const addKeywordToAntennaFail = (antennaId, keyword, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_KEYWORD_FAIL,
|
||||
antennaId,
|
||||
keyword,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => {
|
||||
dispatch(removeKeywordFromAntennaRequest(antennaId, keyword));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/keywords`, { params: { keywords: [keyword] } })
|
||||
.then(() => dispatch(removeKeywordFromAntennaSuccess(antennaId, keyword)))
|
||||
.catch(err => dispatch(removeKeywordFromAntennaFail(antennaId, keyword, err)));
|
||||
};
|
||||
|
||||
export const removeKeywordFromAntennaRequest = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_KEYWORD_REQUEST,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const removeKeywordFromAntennaSuccess = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const removeKeywordFromAntennaFail = (antennaId, keyword, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_KEYWORD_FAIL,
|
||||
antennaId,
|
||||
keyword,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addExcludeKeywordToAntenna = (antennaId, keyword) => (dispatch, getState) => {
|
||||
dispatch(addExcludeKeywordToAntennaRequest(antennaId, keyword));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/exclude_keywords`, { keywords: [keyword] })
|
||||
.then(() => dispatch(addExcludeKeywordToAntennaSuccess(antennaId, keyword)))
|
||||
.catch(err => dispatch(addExcludeKeywordToAntennaFail(antennaId, keyword, err)));
|
||||
};
|
||||
|
||||
export const addExcludeKeywordToAntennaRequest = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_REQUEST,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const addExcludeKeywordToAntennaSuccess = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_SUCCESS,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const addExcludeKeywordToAntennaFail = (antennaId, keyword, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_KEYWORD_FAIL,
|
||||
antennaId,
|
||||
keyword,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeExcludeKeywordFromAntenna = (antennaId, keyword) => (dispatch, getState) => {
|
||||
dispatch(removeExcludeKeywordFromAntennaRequest(antennaId, keyword));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_keywords`, { params: { keywords: [keyword] } })
|
||||
.then(() => dispatch(removeExcludeKeywordFromAntennaSuccess(antennaId, keyword)))
|
||||
.catch(err => dispatch(removeExcludeKeywordFromAntennaFail(antennaId, keyword, err)));
|
||||
};
|
||||
|
||||
export const removeExcludeKeywordFromAntennaRequest = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_REQUEST,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const removeExcludeKeywordFromAntennaSuccess = (antennaId, keyword) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_SUCCESS,
|
||||
antennaId,
|
||||
keyword,
|
||||
});
|
||||
|
||||
export const removeExcludeKeywordFromAntennaFail = (antennaId, keyword, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_KEYWORD_FAIL,
|
||||
antennaId,
|
||||
keyword,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaTags = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaTagsRequest(antennaId));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${antennaId}/tags`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(fetchAntennaTagsSuccess(antennaId, data));
|
||||
}).catch(err => dispatch(fetchAntennaTagsFail(antennaId, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaTagsRequest = id => ({
|
||||
type: ANTENNA_EDITOR_FETCH_TAGS_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaTagsSuccess = (id, tags) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_TAGS_SUCCESS,
|
||||
id,
|
||||
tags,
|
||||
});
|
||||
|
||||
export const fetchAntennaTagsFail = (id, error) => ({
|
||||
type: ANTENNA_EDITOR_FETCH_TAGS_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addTagToAntenna = (antennaId, tag) => (dispatch, getState) => {
|
||||
dispatch(addTagToAntennaRequest(antennaId, tag));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/tags`, { tags: [tag] })
|
||||
.then(() => dispatch(addTagToAntennaSuccess(antennaId, tag)))
|
||||
.catch(err => dispatch(addTagToAntennaFail(antennaId, tag, err)));
|
||||
};
|
||||
|
||||
export const addTagToAntennaRequest = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_ADD_TAG_REQUEST,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const addTagToAntennaSuccess = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_ADD_TAG_SUCCESS,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const addTagToAntennaFail = (antennaId, tag, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_TAG_FAIL,
|
||||
antennaId,
|
||||
tag,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => {
|
||||
dispatch(removeTagFromAntennaRequest(antennaId, tag));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/tags`, { params: { tags: [tag] } })
|
||||
.then(() => dispatch(removeTagFromAntennaSuccess(antennaId, tag)))
|
||||
.catch(err => dispatch(removeTagFromAntennaFail(antennaId, tag, err)));
|
||||
};
|
||||
|
||||
export const removeTagFromAntennaRequest = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_TAG_REQUEST,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const removeTagFromAntennaSuccess = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_TAG_SUCCESS,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const removeTagFromAntennaFail = (antennaId, tag, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_TAG_FAIL,
|
||||
antennaId,
|
||||
tag,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addExcludeTagToAntenna = (antennaId, tag) => (dispatch, getState) => {
|
||||
dispatch(addExcludeTagToAntennaRequest(antennaId, tag));
|
||||
|
||||
api(getState).post(`/api/v1/antennas/${antennaId}/exclude_tags`, { tags: [tag] })
|
||||
.then(() => dispatch(addExcludeTagToAntennaSuccess(antennaId, tag)))
|
||||
.catch(err => dispatch(addExcludeTagToAntennaFail(antennaId, tag, err)));
|
||||
};
|
||||
|
||||
export const addExcludeTagToAntennaRequest = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_REQUEST,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const addExcludeTagToAntennaSuccess = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_SUCCESS,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const addExcludeTagToAntennaFail = (antennaId, tag, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_EXCLUDE_TAG_FAIL,
|
||||
antennaId,
|
||||
tag,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeExcludeTagFromAntenna = (antennaId, tag) => (dispatch, getState) => {
|
||||
dispatch(removeExcludeTagFromAntennaRequest(antennaId, tag));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${antennaId}/exclude_tags`, { params: { tags: [tag] } })
|
||||
.then(() => dispatch(removeExcludeTagFromAntennaSuccess(antennaId, tag)))
|
||||
.catch(err => dispatch(removeExcludeTagFromAntennaFail(antennaId, tag, err)));
|
||||
};
|
||||
|
||||
export const removeExcludeTagFromAntennaRequest = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_REQUEST,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const removeExcludeTagFromAntennaSuccess = (antennaId, tag) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_SUCCESS,
|
||||
antennaId,
|
||||
tag,
|
||||
});
|
||||
|
||||
export const removeExcludeTagFromAntennaFail = (antennaId, tag, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_EXCLUDE_TAG_FAIL,
|
||||
antennaId,
|
||||
tag,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetAntennaAdder = () => ({
|
||||
type: ANTENNA_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupAntennaAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ANTENNA_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchAntennas());
|
||||
dispatch(fetchAccountAntennas(accountId));
|
||||
};
|
||||
|
||||
export const setupExcludeAntennaAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: ANTENNA_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchAntennas());
|
||||
dispatch(fetchExcludeAccountAntennas(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountAntennas = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountAntennasRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/antennas`)
|
||||
.then(({ data }) => dispatch(fetchAccountAntennasSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountAntennasFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountAntennasRequest = id => ({
|
||||
type:ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountAntennasSuccess = (id, antennas) => ({
|
||||
type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
|
||||
id,
|
||||
antennas,
|
||||
});
|
||||
|
||||
export const fetchAccountAntennasFail = (id, err) => ({
|
||||
type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennas = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchExcludeAccountAntennasRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/exclude_antennas`)
|
||||
.then(({ data }) => dispatch(fetchExcludeAccountAntennasSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchExcludeAccountAntennasFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchExcludeAccountAntennasRequest = id => ({
|
||||
type:ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennasSuccess = (id, antennas) => ({
|
||||
type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_SUCCESS,
|
||||
id,
|
||||
antennas,
|
||||
});
|
||||
|
||||
export const fetchExcludeAccountAntennasFail = (id, err) => ({
|
||||
type: ANTENNA_ADDER_EXCLUDE_ANTENNAS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const addExcludeToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addExcludeToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeExcludeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(removeExcludeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
|
|
13
app/javascript/mastodon/actions/antennas_typed.ts
Normal file
13
app/javascript/mastodon/actions/antennas_typed.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { apiCreate, apiUpdate } from 'mastodon/api/antennas';
|
||||
import type { Antenna } from 'mastodon/models/antenna';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const createAntenna = createDataLoadingThunk(
|
||||
'antenna/create',
|
||||
(antenna: Partial<Antenna>) => apiCreate(antenna),
|
||||
);
|
||||
|
||||
export const updateAntenna = createDataLoadingThunk(
|
||||
'antenna/update',
|
||||
(antenna: Partial<Antenna>) => apiUpdate(antenna),
|
||||
);
|
|
@ -1,10 +1,6 @@
|
|||
import { bookmarkCategoryNeeded } from 'mastodon/initial_state';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
|
||||
import api, { getLinks } from '../api';
|
||||
|
||||
import { importFetchedStatuses } from './importer';
|
||||
import { unbookmark } from './interactions';
|
||||
|
||||
export const BOOKMARK_CATEGORY_FETCH_REQUEST = 'BOOKMARK_CATEGORY_FETCH_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_FETCH_SUCCESS';
|
||||
|
@ -14,18 +10,6 @@ export const BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORIES_FETCH_REQU
|
|||
export const BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORIES_FETCH_SUCCESS';
|
||||
export const BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORIES_FETCH_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE = 'BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_RESET = 'BOOKMARK_CATEGORY_EDITOR_RESET';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_SETUP = 'BOOKMARK_CATEGORY_EDITOR_SETUP';
|
||||
|
||||
export const BOOKMARK_CATEGORY_CREATE_REQUEST = 'BOOKMARK_CATEGORY_CREATE_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_CREATE_SUCCESS = 'BOOKMARK_CATEGORY_CREATE_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_CREATE_FAIL = 'BOOKMARK_CATEGORY_CREATE_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_UPDATE_REQUEST = 'BOOKMARK_CATEGORY_UPDATE_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_UPDATE_SUCCESS = 'BOOKMARK_CATEGORY_UPDATE_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_UPDATE_FAIL = 'BOOKMARK_CATEGORY_UPDATE_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_DELETE_REQUEST = 'BOOKMARK_CATEGORY_DELETE_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_DELETE_SUCCESS = 'BOOKMARK_CATEGORY_DELETE_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_DELETE_FAIL = 'BOOKMARK_CATEGORY_DELETE_FAIL';
|
||||
|
@ -34,25 +18,13 @@ export const BOOKMARK_CATEGORY_STATUSES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_STATU
|
|||
export const BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_FETCH_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL = 'BOOKMARK_CATEGORY_STATUSES_FETCH_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_ADD_FAIL = 'BOOKMARK_CATEGORY_EDITOR_ADD_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_ADDER_RESET = 'BOOKMARK_CATEGORY_ADDER_RESET';
|
||||
export const BOOKMARK_CATEGORY_ADDER_SETUP = 'BOOKMARK_CATEGORY_ADDER_SETUP';
|
||||
|
||||
export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL = 'BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_REQUEST';
|
||||
export const BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL = 'BOOKMARK_CATEGORY_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export const BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS';
|
||||
export const BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS = 'BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS';
|
||||
|
||||
export const fetchBookmarkCategory = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['bookmark_categories', id])) {
|
||||
return;
|
||||
|
@ -103,89 +75,6 @@ export const fetchBookmarkCategoriesFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const submitBookmarkCategoryEditor = shouldReset => (dispatch, getState) => {
|
||||
const bookmarkCategoryId = getState().getIn(['bookmarkCategoryEditor', 'bookmarkCategoryId']);
|
||||
const title = getState().getIn(['bookmarkCategoryEditor', 'title']);
|
||||
|
||||
if (bookmarkCategoryId === null) {
|
||||
dispatch(createBookmarkCategory(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateBookmarkCategory(bookmarkCategoryId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupBookmarkCategoryEditor = bookmarkCategoryId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_SETUP,
|
||||
bookmarkCategory: getState().getIn(['bookmark_categories', bookmarkCategoryId]),
|
||||
});
|
||||
|
||||
dispatch(fetchBookmarkCategoryStatuses(bookmarkCategoryId));
|
||||
};
|
||||
|
||||
export const changeBookmarkCategoryEditorTitle = value => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createBookmarkCategory = (title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(createBookmarkCategoryRequest());
|
||||
|
||||
api(getState).post('/api/v1/bookmark_categories', { title }).then(({ data }) => {
|
||||
dispatch(createBookmarkCategorySuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetBookmarkCategoryEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createBookmarkCategoryFail(err)));
|
||||
};
|
||||
|
||||
export const createBookmarkCategoryRequest = () => ({
|
||||
type: BOOKMARK_CATEGORY_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createBookmarkCategorySuccess = bookmarkCategory => ({
|
||||
type: BOOKMARK_CATEGORY_CREATE_SUCCESS,
|
||||
bookmarkCategory,
|
||||
});
|
||||
|
||||
export const createBookmarkCategoryFail = error => ({
|
||||
type: BOOKMARK_CATEGORY_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateBookmarkCategory = (id, title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(updateBookmarkCategoryRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/bookmark_categories/${id}`, { title }).then(({ data }) => {
|
||||
dispatch(updateBookmarkCategorySuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetBookmarkCategoryEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateBookmarkCategoryFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateBookmarkCategoryRequest = id => ({
|
||||
type: BOOKMARK_CATEGORY_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateBookmarkCategorySuccess = bookmarkCategory => ({
|
||||
type: BOOKMARK_CATEGORY_UPDATE_SUCCESS,
|
||||
bookmarkCategory,
|
||||
});
|
||||
|
||||
export const updateBookmarkCategoryFail = (id, error) => ({
|
||||
type: BOOKMARK_CATEGORY_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetBookmarkCategoryEditor = () => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteBookmarkCategory = id => (dispatch, getState) => {
|
||||
dispatch(deleteBookmarkCategoryRequest(id));
|
||||
|
||||
|
@ -238,116 +127,6 @@ export const fetchBookmarkCategoryStatusesFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const addToBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => {
|
||||
dispatch(addToBookmarkCategoryRequest(bookmarkCategoryId, statusId));
|
||||
|
||||
api(getState).post(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { status_ids: [statusId] })
|
||||
.then(() => dispatch(addToBookmarkCategorySuccess(bookmarkCategoryId, statusId)))
|
||||
.catch(err => dispatch(addToBookmarkCategoryFail(bookmarkCategoryId, statusId, err)));
|
||||
};
|
||||
|
||||
export const addToBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_ADD_REQUEST,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
export const addToBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
export const addToBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_ADD_FAIL,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromBookmarkCategory = (bookmarkCategoryId, statusId) => (dispatch, getState) => {
|
||||
dispatch(removeFromBookmarkCategoryRequest(bookmarkCategoryId, statusId));
|
||||
|
||||
api(getState).delete(`/api/v1/bookmark_categories/${bookmarkCategoryId}/statuses`, { params: { status_ids: [statusId] } })
|
||||
.then(() => dispatch(removeFromBookmarkCategorySuccess(bookmarkCategoryId, statusId)))
|
||||
.catch(err => dispatch(removeFromBookmarkCategoryFail(bookmarkCategoryId, statusId, err)));
|
||||
};
|
||||
|
||||
export const removeFromBookmarkCategoryRequest = (bookmarkCategoryId, statusId) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_REMOVE_REQUEST,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
export const removeFromBookmarkCategorySuccess = (bookmarkCategoryId, statusId) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
});
|
||||
|
||||
export const removeFromBookmarkCategoryFail = (bookmarkCategoryId, statusId, error) => ({
|
||||
type: BOOKMARK_CATEGORY_EDITOR_REMOVE_FAIL,
|
||||
bookmarkCategoryId,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetBookmarkCategoryAdder = () => ({
|
||||
type: BOOKMARK_CATEGORY_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupBookmarkCategoryAdder = statusId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: BOOKMARK_CATEGORY_ADDER_SETUP,
|
||||
status: getState().getIn(['statuses', statusId]),
|
||||
});
|
||||
dispatch(fetchBookmarkCategories());
|
||||
dispatch(fetchStatusBookmarkCategories(statusId));
|
||||
};
|
||||
|
||||
export const fetchStatusBookmarkCategories = statusId => (dispatch, getState) => {
|
||||
dispatch(fetchStatusBookmarkCategoriesRequest(statusId));
|
||||
|
||||
api(getState).get(`/api/v1/statuses/${statusId}/bookmark_categories`)
|
||||
.then(({ data }) => dispatch(fetchStatusBookmarkCategoriesSuccess(statusId, data)))
|
||||
.catch(err => dispatch(fetchStatusBookmarkCategoriesFail(statusId, err)));
|
||||
};
|
||||
|
||||
export const fetchStatusBookmarkCategoriesRequest = id => ({
|
||||
type:BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchStatusBookmarkCategoriesSuccess = (id, bookmarkCategories) => ({
|
||||
type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_SUCCESS,
|
||||
id,
|
||||
bookmarkCategories,
|
||||
});
|
||||
|
||||
export const fetchStatusBookmarkCategoriesFail = (id, err) => ({
|
||||
type: BOOKMARK_CATEGORY_ADDER_BOOKMARK_CATEGORIES_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => {
|
||||
dispatch(addToBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId'])));
|
||||
};
|
||||
|
||||
export const removeFromBookmarkCategoryAdder = bookmarkCategoryId => (dispatch, getState) => {
|
||||
if (bookmarkCategoryNeeded) {
|
||||
const categories = getState().getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']);
|
||||
if (categories && categories.count() <= 1) {
|
||||
const status = makeGetStatus()(getState(), { id: getState().getIn(['bookmarkCategoryAdder', 'statusId']) });
|
||||
dispatch(unbookmark(status));
|
||||
} else {
|
||||
dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId'])));
|
||||
}
|
||||
} else {
|
||||
dispatch(removeFromBookmarkCategory(bookmarkCategoryId, getState().getIn(['bookmarkCategoryAdder', 'statusId'])));
|
||||
}
|
||||
};
|
||||
|
||||
export function expandBookmarkCategoryStatuses(bookmarkCategoryId) {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['bookmark_categories', bookmarkCategoryId, 'next'], null);
|
||||
|
@ -392,3 +171,19 @@ export function expandBookmarkCategoryStatusesFail(id, error) {
|
|||
};
|
||||
}
|
||||
|
||||
export function bookmarkCategoryEditorAddSuccess(id, statusId) {
|
||||
return {
|
||||
type: BOOKMARK_CATEGORY_EDITOR_ADD_SUCCESS,
|
||||
id,
|
||||
statusId,
|
||||
};
|
||||
}
|
||||
|
||||
export function bookmarkCategoryEditorRemoveSuccess(id, statusId) {
|
||||
return {
|
||||
type: BOOKMARK_CATEGORY_EDITOR_REMOVE_SUCCESS,
|
||||
id,
|
||||
statusId,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
13
app/javascript/mastodon/actions/bookmark_categories_typed.ts
Normal file
13
app/javascript/mastodon/actions/bookmark_categories_typed.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { apiCreate, apiUpdate } from 'mastodon/api/bookmark_categories';
|
||||
import type { BookmarkCategory } from 'mastodon/models/bookmark_category';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const createBookmarkCategory = createDataLoadingThunk(
|
||||
'bookmark_category/create',
|
||||
(bookmarkCategory: Partial<BookmarkCategory>) => apiCreate(bookmarkCategory),
|
||||
);
|
||||
|
||||
export const updateBookmarkCategory = createDataLoadingThunk(
|
||||
'bookmark_category/update',
|
||||
(bookmarkCategory: Partial<BookmarkCategory>) => apiUpdate(bookmarkCategory),
|
||||
);
|
|
@ -1,7 +1,6 @@
|
|||
import api, { getLinks } from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts, importFetchedStatuses } from './importer';
|
||||
import { importFetchedStatuses } from './importer';
|
||||
|
||||
export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST';
|
||||
export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS';
|
||||
|
@ -11,10 +10,6 @@ export const CIRCLES_FETCH_REQUEST = 'CIRCLES_FETCH_REQUEST';
|
|||
export const CIRCLES_FETCH_SUCCESS = 'CIRCLES_FETCH_SUCCESS';
|
||||
export const CIRCLES_FETCH_FAIL = 'CIRCLES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_EDITOR_TITLE_CHANGE = 'CIRCLE_EDITOR_TITLE_CHANGE';
|
||||
export const CIRCLE_EDITOR_RESET = 'CIRCLE_EDITOR_RESET';
|
||||
export const CIRCLE_EDITOR_SETUP = 'CIRCLE_EDITOR_SETUP';
|
||||
|
||||
export const CIRCLE_CREATE_REQUEST = 'CIRCLE_CREATE_REQUEST';
|
||||
export const CIRCLE_CREATE_SUCCESS = 'CIRCLE_CREATE_SUCCESS';
|
||||
export const CIRCLE_CREATE_FAIL = 'CIRCLE_CREATE_FAIL';
|
||||
|
@ -27,29 +22,6 @@ export const CIRCLE_DELETE_REQUEST = 'CIRCLE_DELETE_REQUEST';
|
|||
export const CIRCLE_DELETE_SUCCESS = 'CIRCLE_DELETE_SUCCESS';
|
||||
export const CIRCLE_DELETE_FAIL = 'CIRCLE_DELETE_FAIL';
|
||||
|
||||
export const CIRCLE_ACCOUNTS_FETCH_REQUEST = 'CIRCLE_ACCOUNTS_FETCH_REQUEST';
|
||||
export const CIRCLE_ACCOUNTS_FETCH_SUCCESS = 'CIRCLE_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const CIRCLE_ACCOUNTS_FETCH_FAIL = 'CIRCLE_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_EDITOR_SUGGESTIONS_CHANGE = 'CIRCLE_EDITOR_SUGGESTIONS_CHANGE';
|
||||
export const CIRCLE_EDITOR_SUGGESTIONS_READY = 'CIRCLE_EDITOR_SUGGESTIONS_READY';
|
||||
export const CIRCLE_EDITOR_SUGGESTIONS_CLEAR = 'CIRCLE_EDITOR_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const CIRCLE_EDITOR_ADD_REQUEST = 'CIRCLE_EDITOR_ADD_REQUEST';
|
||||
export const CIRCLE_EDITOR_ADD_SUCCESS = 'CIRCLE_EDITOR_ADD_SUCCESS';
|
||||
export const CIRCLE_EDITOR_ADD_FAIL = 'CIRCLE_EDITOR_ADD_FAIL';
|
||||
|
||||
export const CIRCLE_EDITOR_REMOVE_REQUEST = 'CIRCLE_EDITOR_REMOVE_REQUEST';
|
||||
export const CIRCLE_EDITOR_REMOVE_SUCCESS = 'CIRCLE_EDITOR_REMOVE_SUCCESS';
|
||||
export const CIRCLE_EDITOR_REMOVE_FAIL = 'CIRCLE_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const CIRCLE_ADDER_RESET = 'CIRCLE_ADDER_RESET';
|
||||
export const CIRCLE_ADDER_SETUP = 'CIRCLE_ADDER_SETUP';
|
||||
|
||||
export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_REQUEST';
|
||||
export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS';
|
||||
export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL';
|
||||
|
||||
export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST';
|
||||
export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS';
|
||||
export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL';
|
||||
|
@ -108,89 +80,6 @@ export const fetchCirclesFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const submitCircleEditor = shouldReset => (dispatch, getState) => {
|
||||
const circleId = getState().getIn(['circleEditor', 'circleId']);
|
||||
const title = getState().getIn(['circleEditor', 'title']);
|
||||
|
||||
if (circleId === null) {
|
||||
dispatch(createCircle(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateCircle(circleId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupCircleEditor = circleId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CIRCLE_EDITOR_SETUP,
|
||||
circle: getState().getIn(['circles', circleId]),
|
||||
});
|
||||
|
||||
dispatch(fetchCircleAccounts(circleId));
|
||||
};
|
||||
|
||||
export const changeCircleEditorTitle = value => ({
|
||||
type: CIRCLE_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createCircle = (title, shouldReset) => (dispatch, getState) => {
|
||||
dispatch(createCircleRequest());
|
||||
|
||||
api(getState).post('/api/v1/circles', { title }).then(({ data }) => {
|
||||
dispatch(createCircleSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetCircleEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createCircleFail(err)));
|
||||
};
|
||||
|
||||
export const createCircleRequest = () => ({
|
||||
type: CIRCLE_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createCircleSuccess = circle => ({
|
||||
type: CIRCLE_CREATE_SUCCESS,
|
||||
circle,
|
||||
});
|
||||
|
||||
export const createCircleFail = error => ({
|
||||
type: CIRCLE_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateCircle = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
|
||||
dispatch(updateCircleRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/circles/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
|
||||
dispatch(updateCircleSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetCircleEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateCircleFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateCircleRequest = id => ({
|
||||
type: CIRCLE_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateCircleSuccess = circle => ({
|
||||
type: CIRCLE_UPDATE_SUCCESS,
|
||||
circle,
|
||||
});
|
||||
|
||||
export const updateCircleFail = (id, error) => ({
|
||||
type: CIRCLE_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetCircleEditor = () => ({
|
||||
type: CIRCLE_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteCircle = id => (dispatch, getState) => {
|
||||
dispatch(deleteCircleRequest(id));
|
||||
|
||||
|
@ -215,169 +104,6 @@ export const deleteCircleFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const fetchCircleAccounts = circleId => (dispatch, getState) => {
|
||||
dispatch(fetchCircleAccountsRequest(circleId));
|
||||
|
||||
api(getState).get(`/api/v1/circles/${circleId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchCircleAccountsSuccess(circleId, data));
|
||||
}).catch(err => dispatch(fetchCircleAccountsFail(circleId, err)));
|
||||
};
|
||||
|
||||
export const fetchCircleAccountsRequest = id => ({
|
||||
type: CIRCLE_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchCircleAccountsSuccess = (id, accounts, next) => ({
|
||||
type: CIRCLE_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchCircleAccountsFail = (id, error) => ({
|
||||
type: CIRCLE_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchCircleSuggestions = q => (dispatch, getState) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
follower: true,
|
||||
};
|
||||
|
||||
api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchCircleSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
export const fetchCircleSuggestionsReady = (query, accounts) => ({
|
||||
type: CIRCLE_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearCircleSuggestions = () => ({
|
||||
type: CIRCLE_EDITOR_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeCircleSuggestions = value => ({
|
||||
type: CIRCLE_EDITOR_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToCircleEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addToCircle(getState().getIn(['circleEditor', 'circleId']), accountId));
|
||||
};
|
||||
|
||||
export const addToCircle = (circleId, accountId) => (dispatch, getState) => {
|
||||
dispatch(addToCircleRequest(circleId, accountId));
|
||||
|
||||
api(getState).post(`/api/v1/circles/${circleId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToCircleSuccess(circleId, accountId)))
|
||||
.catch(err => dispatch(addToCircleFail(circleId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addToCircleRequest = (circleId, accountId) => ({
|
||||
type: CIRCLE_EDITOR_ADD_REQUEST,
|
||||
circleId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToCircleSuccess = (circleId, accountId) => ({
|
||||
type: CIRCLE_EDITOR_ADD_SUCCESS,
|
||||
circleId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToCircleFail = (circleId, accountId, error) => ({
|
||||
type: CIRCLE_EDITOR_ADD_FAIL,
|
||||
circleId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromCircleEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeFromCircle(getState().getIn(['circleEditor', 'circleId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromCircle = (circleId, accountId) => (dispatch, getState) => {
|
||||
dispatch(removeFromCircleRequest(circleId, accountId));
|
||||
|
||||
api(getState).delete(`/api/v1/circles/${circleId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromCircleSuccess(circleId, accountId)))
|
||||
.catch(err => dispatch(removeFromCircleFail(circleId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeFromCircleRequest = (circleId, accountId) => ({
|
||||
type: CIRCLE_EDITOR_REMOVE_REQUEST,
|
||||
circleId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromCircleSuccess = (circleId, accountId) => ({
|
||||
type: CIRCLE_EDITOR_REMOVE_SUCCESS,
|
||||
circleId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromCircleFail = (circleId, accountId, error) => ({
|
||||
type: CIRCLE_EDITOR_REMOVE_FAIL,
|
||||
circleId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetCircleAdder = () => ({
|
||||
type: CIRCLE_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupCircleAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CIRCLE_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchCircles());
|
||||
dispatch(fetchAccountCircles(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountCircles = accountId => (dispatch, getState) => {
|
||||
dispatch(fetchAccountCirclesRequest(accountId));
|
||||
|
||||
api(getState).get(`/api/v1/accounts/${accountId}/circles`)
|
||||
.then(({ data }) => dispatch(fetchAccountCirclesSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountCirclesFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountCirclesRequest = id => ({
|
||||
type:CIRCLE_ADDER_CIRCLES_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountCirclesSuccess = (id, circles) => ({
|
||||
type: CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS,
|
||||
id,
|
||||
circles,
|
||||
});
|
||||
|
||||
export const fetchAccountCirclesFail = (id, err) => ({
|
||||
type: CIRCLE_ADDER_CIRCLES_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToCircleAdder = circleId => (dispatch, getState) => {
|
||||
dispatch(addToCircle(circleId, getState().getIn(['circleAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromCircleAdder = circleId => (dispatch, getState) => {
|
||||
dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export function fetchCircleStatuses(circleId) {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['circles', circleId, 'isLoading'])) {
|
||||
|
|
13
app/javascript/mastodon/actions/circles_typed.ts
Normal file
13
app/javascript/mastodon/actions/circles_typed.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { apiCreate, apiUpdate } from 'mastodon/api/circles';
|
||||
import type { Circle } from 'mastodon/models/circle';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const createCircle = createDataLoadingThunk(
|
||||
'circle/create',
|
||||
(circle: Partial<Circle>) => apiCreate(circle),
|
||||
);
|
||||
|
||||
export const updateCircle = createDataLoadingThunk(
|
||||
'circle/update',
|
||||
(circle: Partial<Circle>) => apiUpdate(circle),
|
||||
);
|
|
@ -272,14 +272,14 @@ export function submitCompose() {
|
|||
insertIfOnline('home');
|
||||
}
|
||||
|
||||
if (statusId === null && response.data.in_reply_to_id === null && response.data.visibility_ex === 'public') {
|
||||
if (statusId === null && response.data.in_reply_to_id === null && ['public', 'public_unlisted', 'login'].includes(response.data.visibility_ex)) {
|
||||
insertIfOnline('community');
|
||||
insertIfOnline('public');
|
||||
insertIfOnline(`account:${response.data.account.id}`);
|
||||
}
|
||||
|
||||
if (statusId === null && privacy === 'circle' && circleId !== null && circleId !== 0) {
|
||||
dispatch(submitComposeWithCircleSuccess({ ...response.data }, circleId));
|
||||
dispatch(submitComposeWithCircleSuccess({ ...response.data }, `${circleId}`));
|
||||
}
|
||||
|
||||
dispatch(showAlert({
|
||||
|
@ -310,7 +310,7 @@ export function submitComposeSuccess(status) {
|
|||
export function submitComposeWithCircleSuccess(status, circleId) {
|
||||
return {
|
||||
type: COMPOSE_WITH_CIRCLE_SUCCESS,
|
||||
status,
|
||||
statusId: status.id,
|
||||
circleId,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
|
||||
import api from '../api';
|
||||
|
||||
import { showAlertForError } from './alerts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||
|
@ -13,45 +10,10 @@ export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
|
|||
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
||||
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
|
||||
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
|
||||
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
|
||||
|
||||
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
|
||||
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
|
||||
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
|
||||
|
||||
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
|
||||
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
|
||||
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
|
||||
|
||||
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
||||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||
|
||||
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
|
||||
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
|
||||
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
|
||||
|
||||
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
|
||||
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
|
||||
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
|
||||
|
||||
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
|
||||
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
|
||||
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
|
||||
|
||||
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
|
||||
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
|
||||
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
|
||||
|
||||
export const LIST_ADDER_RESET = 'LIST_ADDER_RESET';
|
||||
export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP';
|
||||
|
||||
export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST';
|
||||
export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS';
|
||||
export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL';
|
||||
|
||||
export const fetchList = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['lists', id])) {
|
||||
return;
|
||||
|
@ -102,94 +64,6 @@ export const fetchListsFail = error => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const submitListEditor = shouldReset => (dispatch, getState) => {
|
||||
const listId = getState().getIn(['listEditor', 'listId']);
|
||||
const title = getState().getIn(['listEditor', 'title']);
|
||||
|
||||
if (listId === null) {
|
||||
dispatch(createList(title, shouldReset));
|
||||
} else {
|
||||
dispatch(updateList(listId, title, shouldReset));
|
||||
}
|
||||
};
|
||||
|
||||
export const setupListEditor = listId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_EDITOR_SETUP,
|
||||
list: getState().getIn(['lists', listId]),
|
||||
});
|
||||
|
||||
dispatch(fetchListAccounts(listId));
|
||||
};
|
||||
|
||||
export const changeListEditorTitle = value => ({
|
||||
type: LIST_EDITOR_TITLE_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const createList = (title, shouldReset) => (dispatch) => {
|
||||
dispatch(createListRequest());
|
||||
|
||||
api().post('/api/v1/lists', { title }).then(({ data }) => {
|
||||
dispatch(createListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(createListFail(err)));
|
||||
};
|
||||
|
||||
export const createListRequest = () => ({
|
||||
type: LIST_CREATE_REQUEST,
|
||||
});
|
||||
|
||||
export const createListSuccess = list => ({
|
||||
type: LIST_CREATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const createListFail = error => ({
|
||||
type: LIST_CREATE_FAIL,
|
||||
error,
|
||||
});
|
||||
|
||||
export const updateList = (id, title, shouldReset, isExclusive, replies_policy, notify) => (dispatch) => {
|
||||
dispatch(updateListRequest(id));
|
||||
|
||||
api().put(`/api/v1/lists/${id}`, {
|
||||
title,
|
||||
replies_policy,
|
||||
exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive,
|
||||
notify: typeof notify === 'undefined' ? undefined : !!notify,
|
||||
}).then(({ data }) => {
|
||||
dispatch(updateListSuccess(data));
|
||||
|
||||
if (shouldReset) {
|
||||
dispatch(resetListEditor());
|
||||
}
|
||||
}).catch(err => dispatch(updateListFail(id, err)));
|
||||
};
|
||||
|
||||
export const updateListRequest = id => ({
|
||||
type: LIST_UPDATE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const updateListSuccess = list => ({
|
||||
type: LIST_UPDATE_SUCCESS,
|
||||
list,
|
||||
});
|
||||
|
||||
export const updateListFail = (id, error) => ({
|
||||
type: LIST_UPDATE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListEditor = () => ({
|
||||
type: LIST_EDITOR_RESET,
|
||||
});
|
||||
|
||||
export const deleteList = id => (dispatch) => {
|
||||
dispatch(deleteListRequest(id));
|
||||
|
||||
|
@ -213,166 +87,3 @@ export const deleteListFail = (id, error) => ({
|
|||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListAccounts = listId => (dispatch) => {
|
||||
dispatch(fetchListAccountsRequest(listId));
|
||||
|
||||
api().get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListAccountsSuccess(listId, data));
|
||||
}).catch(err => dispatch(fetchListAccountsFail(listId, err)));
|
||||
};
|
||||
|
||||
export const fetchListAccountsRequest = id => ({
|
||||
type: LIST_ACCOUNTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchListAccountsSuccess = (id, accounts, next) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_SUCCESS,
|
||||
id,
|
||||
accounts,
|
||||
next,
|
||||
});
|
||||
|
||||
export const fetchListAccountsFail = (id, error) => ({
|
||||
type: LIST_ACCOUNTS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchListSuggestions = q => (dispatch) => {
|
||||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
following: true,
|
||||
};
|
||||
|
||||
api().get('/api/v1/accounts/search', { params }).then(({ data }) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
dispatch(fetchListSuggestionsReady(q, data));
|
||||
}).catch(error => dispatch(showAlertForError(error)));
|
||||
};
|
||||
|
||||
export const fetchListSuggestionsReady = (query, accounts) => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_READY,
|
||||
query,
|
||||
accounts,
|
||||
});
|
||||
|
||||
export const clearListSuggestions = () => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
|
||||
});
|
||||
|
||||
export const changeListSuggestions = value => ({
|
||||
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
export const addToListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const addToList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(addToListRequest(listId, accountId));
|
||||
|
||||
api().post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
|
||||
.then(() => dispatch(addToListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(addToListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addToListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_ADD_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addToListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_ADD_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeFromListEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
|
||||
};
|
||||
|
||||
export const removeFromList = (listId, accountId) => (dispatch) => {
|
||||
dispatch(removeFromListRequest(listId, accountId));
|
||||
|
||||
api().delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
|
||||
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
|
||||
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeFromListRequest = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_REQUEST,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListSuccess = (listId, accountId) => ({
|
||||
type: LIST_EDITOR_REMOVE_SUCCESS,
|
||||
listId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeFromListFail = (listId, accountId, error) => ({
|
||||
type: LIST_EDITOR_REMOVE_FAIL,
|
||||
listId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const resetListAdder = () => ({
|
||||
type: LIST_ADDER_RESET,
|
||||
});
|
||||
|
||||
export const setupListAdder = accountId => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: LIST_ADDER_SETUP,
|
||||
account: getState().getIn(['accounts', accountId]),
|
||||
});
|
||||
dispatch(fetchLists());
|
||||
dispatch(fetchAccountLists(accountId));
|
||||
};
|
||||
|
||||
export const fetchAccountLists = accountId => (dispatch) => {
|
||||
dispatch(fetchAccountListsRequest(accountId));
|
||||
|
||||
api().get(`/api/v1/accounts/${accountId}/lists`)
|
||||
.then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data)))
|
||||
.catch(err => dispatch(fetchAccountListsFail(accountId, err)));
|
||||
};
|
||||
|
||||
export const fetchAccountListsRequest = id => ({
|
||||
type:LIST_ADDER_LISTS_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAccountListsSuccess = (id, lists) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_SUCCESS,
|
||||
id,
|
||||
lists,
|
||||
});
|
||||
|
||||
export const fetchAccountListsFail = (id, err) => ({
|
||||
type: LIST_ADDER_LISTS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
});
|
||||
|
||||
export const addToListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromListAdder = listId => (dispatch, getState) => {
|
||||
dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId'])));
|
||||
};
|
||||
|
|
17
app/javascript/mastodon/actions/lists_typed.ts
Normal file
17
app/javascript/mastodon/actions/lists_typed.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { apiCreate, apiUpdate } from 'mastodon/api/lists';
|
||||
import type { List } from 'mastodon/models/list';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const createList = createDataLoadingThunk(
|
||||
'list/create',
|
||||
(list: Partial<List>) => apiCreate(list),
|
||||
);
|
||||
|
||||
// Kmyblue tracking marker: copied antenna, circle, bookmark_category
|
||||
|
||||
export const updateList = createDataLoadingThunk(
|
||||
'list/update',
|
||||
(list: Partial<List>) => apiUpdate(list),
|
||||
);
|
||||
|
||||
// Kmyblue tracking marker: copied antenna, circle, bookmark_category
|
|
@ -141,6 +141,9 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||
|
||||
return { notifications };
|
||||
},
|
||||
{
|
||||
useLoadingBar: false,
|
||||
},
|
||||
);
|
||||
|
||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import api from '../api';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
|
||||
export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
|
||||
export const SUGGESTIONS_FETCH_FAIL = 'SUGGESTIONS_FETCH_FAIL';
|
||||
|
||||
export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
|
||||
|
||||
export function fetchSuggestions(withRelationships = false) {
|
||||
return (dispatch) => {
|
||||
dispatch(fetchSuggestionsRequest());
|
||||
|
||||
api().get('/api/v2/suggestions', { params: { limit: 20 } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data.map(x => x.account)));
|
||||
dispatch(fetchSuggestionsSuccess(response.data));
|
||||
|
||||
if (withRelationships) {
|
||||
dispatch(fetchRelationships(response.data.map(item => item.account.id)));
|
||||
}
|
||||
}).catch(error => dispatch(fetchSuggestionsFail(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSuggestionsRequest() {
|
||||
return {
|
||||
type: SUGGESTIONS_FETCH_REQUEST,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSuggestionsSuccess(suggestions) {
|
||||
return {
|
||||
type: SUGGESTIONS_FETCH_SUCCESS,
|
||||
suggestions,
|
||||
skipLoading: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSuggestionsFail(error) {
|
||||
return {
|
||||
type: SUGGESTIONS_FETCH_FAIL,
|
||||
error,
|
||||
skipLoading: true,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
export const dismissSuggestion = accountId => (dispatch) => {
|
||||
dispatch({
|
||||
type: SUGGESTIONS_DISMISS,
|
||||
id: accountId,
|
||||
});
|
||||
|
||||
api().delete(`/api/v1/suggestions/${accountId}`).catch(() => {});
|
||||
};
|
24
app/javascript/mastodon/actions/suggestions.ts
Normal file
24
app/javascript/mastodon/actions/suggestions.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
apiGetSuggestions,
|
||||
apiDeleteSuggestion,
|
||||
} from 'mastodon/api/suggestions';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
export const fetchSuggestions = createDataLoadingThunk(
|
||||
'suggestions/fetch',
|
||||
() => apiGetSuggestions(20),
|
||||
(data, { dispatch }) => {
|
||||
dispatch(importFetchedAccounts(data.map((x) => x.account)));
|
||||
dispatch(fetchRelationships(data.map((x) => x.account.id)));
|
||||
|
||||
return data;
|
||||
},
|
||||
);
|
||||
|
||||
export const dismissSuggestion = createDataLoadingThunk(
|
||||
'suggestions/dismiss',
|
||||
({ accountId }: { accountId: string }) => apiDeleteSuggestion(accountId),
|
||||
);
|
|
@ -68,6 +68,7 @@ export async function apiRequest<ApiResponse = unknown>(
|
|||
method: Method,
|
||||
url: string,
|
||||
args: {
|
||||
signal?: AbortSignal;
|
||||
params?: RequestParamsOrData;
|
||||
data?: RequestParamsOrData;
|
||||
timeout?: number;
|
||||
|
|
143
app/javascript/mastodon/api/antennas.ts
Normal file
143
app/javascript/mastodon/api/antennas.ts
Normal file
|
@ -0,0 +1,143 @@
|
|||
import {
|
||||
apiRequestPost,
|
||||
apiRequestPut,
|
||||
apiRequestGet,
|
||||
apiRequestDelete,
|
||||
} from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiAntennaJSON } from 'mastodon/api_types/antennas';
|
||||
|
||||
export const apiCreate = (antenna: Partial<ApiAntennaJSON>) =>
|
||||
apiRequestPost<ApiAntennaJSON>('v1/antennas', antenna);
|
||||
|
||||
export const apiUpdate = (antenna: Partial<ApiAntennaJSON>) =>
|
||||
apiRequestPut<ApiAntennaJSON>(`v1/antennas/${antenna.id}`, antenna);
|
||||
|
||||
export const apiGetAccounts = (antennaId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(`v1/antennas/${antennaId}/accounts`, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
export const apiGetExcludeAccounts = (antennaId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(`v1/antennas/${antennaId}/exclude_accounts`, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
export const apiGetDomains = (antennaId: string) =>
|
||||
apiRequestGet<{ domains: string[]; exclude_domains: string[] }>(
|
||||
`v1/antennas/${antennaId}/domains`,
|
||||
{
|
||||
limit: 0,
|
||||
},
|
||||
);
|
||||
|
||||
export const apiAddDomain = (antennaId: string, domain: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/domains`, {
|
||||
domains: [domain],
|
||||
});
|
||||
|
||||
export const apiRemoveDomain = (antennaId: string, domain: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/domains`, {
|
||||
domains: [domain],
|
||||
});
|
||||
|
||||
export const apiAddExcludeDomain = (antennaId: string, domain: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/exclude_domains`, {
|
||||
domains: [domain],
|
||||
});
|
||||
|
||||
export const apiRemoveExcludeDomain = (antennaId: string, domain: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/exclude_domains`, {
|
||||
domains: [domain],
|
||||
});
|
||||
|
||||
export const apiGetTags = (antennaId: string) =>
|
||||
apiRequestGet<{ tags: string[]; exclude_tags: string[] }>(
|
||||
`v1/antennas/${antennaId}/tags`,
|
||||
{
|
||||
limit: 0,
|
||||
},
|
||||
);
|
||||
|
||||
export const apiAddTag = (antennaId: string, tag: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/tags`, {
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
export const apiRemoveTag = (antennaId: string, tag: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/tags`, {
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
export const apiAddExcludeTag = (antennaId: string, tag: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/exclude_tags`, {
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
export const apiRemoveExcludeTag = (antennaId: string, tag: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/exclude_tags`, {
|
||||
tags: [tag],
|
||||
});
|
||||
|
||||
export const apiGetKeywords = (antennaId: string) =>
|
||||
apiRequestGet<{ keywords: string[]; exclude_keywords: string[] }>(
|
||||
`v1/antennas/${antennaId}/keywords`,
|
||||
{
|
||||
limit: 0,
|
||||
},
|
||||
);
|
||||
|
||||
export const apiAddKeyword = (antennaId: string, keyword: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/keywords`, {
|
||||
keywords: [keyword],
|
||||
});
|
||||
|
||||
export const apiRemoveKeyword = (antennaId: string, keyword: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/keywords`, {
|
||||
keywords: [keyword],
|
||||
});
|
||||
|
||||
export const apiAddExcludeKeyword = (antennaId: string, keyword: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/exclude_keywords`, {
|
||||
keywords: [keyword],
|
||||
});
|
||||
|
||||
export const apiRemoveExcludeKeyword = (antennaId: string, keyword: string) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/exclude_keywords`, {
|
||||
keywords: [keyword],
|
||||
});
|
||||
|
||||
export const apiGetAccountAntennas = (accountId: string) =>
|
||||
apiRequestGet<ApiAntennaJSON[]>(`v1/accounts/${accountId}/antennas`);
|
||||
|
||||
export const apiAddAccountToAntenna = (antennaId: string, accountId: string) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiRemoveAccountFromAntenna = (
|
||||
antennaId: string,
|
||||
accountId: string,
|
||||
) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiGetExcludeAccountAntennas = (accountId: string) =>
|
||||
apiRequestGet<ApiAntennaJSON[]>(`v1/accounts/${accountId}/exclude_antennas`);
|
||||
|
||||
export const apiAddExcludeAccountToAntenna = (
|
||||
antennaId: string,
|
||||
accountId: string,
|
||||
) =>
|
||||
apiRequestPost(`v1/antennas/${antennaId}/exclude_accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiRemoveExcludeAccountFromAntenna = (
|
||||
antennaId: string,
|
||||
accountId: string,
|
||||
) =>
|
||||
apiRequestDelete(`v1/antennas/${antennaId}/exclude_accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
49
app/javascript/mastodon/api/bookmark_categories.ts
Normal file
49
app/javascript/mastodon/api/bookmark_categories.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
apiRequestPost,
|
||||
apiRequestPut,
|
||||
apiRequestGet,
|
||||
apiRequestDelete,
|
||||
} from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiBookmarkCategoryJSON } from 'mastodon/api_types/bookmark_categories';
|
||||
|
||||
export const apiCreate = (bookmarkCategory: Partial<ApiBookmarkCategoryJSON>) =>
|
||||
apiRequestPost<ApiBookmarkCategoryJSON>(
|
||||
'v1/bookmark_categories',
|
||||
bookmarkCategory,
|
||||
);
|
||||
|
||||
export const apiUpdate = (bookmarkCategory: Partial<ApiBookmarkCategoryJSON>) =>
|
||||
apiRequestPut<ApiBookmarkCategoryJSON>(
|
||||
`v1/bookmark_categories/${bookmarkCategory.id}`,
|
||||
bookmarkCategory,
|
||||
);
|
||||
|
||||
export const apiGetStatuses = (bookmarkCategoryId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(
|
||||
`v1/bookmark_categories/${bookmarkCategoryId}/statuses`,
|
||||
{
|
||||
limit: 0,
|
||||
},
|
||||
);
|
||||
|
||||
export const apiGetStatusBookmarkCategories = (accountId: string) =>
|
||||
apiRequestGet<ApiBookmarkCategoryJSON[]>(
|
||||
`v1/statuses/${accountId}/bookmark_categories`,
|
||||
);
|
||||
|
||||
export const apiAddStatusToBookmarkCategory = (
|
||||
bookmarkCategoryId: string,
|
||||
statusId: string,
|
||||
) =>
|
||||
apiRequestPost(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, {
|
||||
status_ids: [statusId],
|
||||
});
|
||||
|
||||
export const apiRemoveStatusFromBookmarkCategory = (
|
||||
bookmarkCategoryId: string,
|
||||
statusId: string,
|
||||
) =>
|
||||
apiRequestDelete(`v1/bookmark_categories/${bookmarkCategoryId}/statuses`, {
|
||||
status_ids: [statusId],
|
||||
});
|
35
app/javascript/mastodon/api/circles.ts
Normal file
35
app/javascript/mastodon/api/circles.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
apiRequestPost,
|
||||
apiRequestPut,
|
||||
apiRequestGet,
|
||||
apiRequestDelete,
|
||||
} from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiCircleJSON } from 'mastodon/api_types/circles';
|
||||
|
||||
export const apiCreate = (circle: Partial<ApiCircleJSON>) =>
|
||||
apiRequestPost<ApiCircleJSON>('v1/circles', circle);
|
||||
|
||||
export const apiUpdate = (circle: Partial<ApiCircleJSON>) =>
|
||||
apiRequestPut<ApiCircleJSON>(`v1/circles/${circle.id}`, circle);
|
||||
|
||||
export const apiGetAccounts = (circleId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(`v1/circles/${circleId}/accounts`, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
export const apiGetAccountCircles = (accountId: string) =>
|
||||
apiRequestGet<ApiCircleJSON[]>(`v1/accounts/${accountId}/circles`);
|
||||
|
||||
export const apiAddAccountToCircle = (circleId: string, accountId: string) =>
|
||||
apiRequestPost(`v1/circles/${circleId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiRemoveAccountFromCircle = (
|
||||
circleId: string,
|
||||
accountId: string,
|
||||
) =>
|
||||
apiRequestDelete(`v1/circles/${circleId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
32
app/javascript/mastodon/api/lists.ts
Normal file
32
app/javascript/mastodon/api/lists.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
apiRequestPost,
|
||||
apiRequestPut,
|
||||
apiRequestGet,
|
||||
apiRequestDelete,
|
||||
} from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
|
||||
export const apiCreate = (list: Partial<ApiListJSON>) =>
|
||||
apiRequestPost<ApiListJSON>('v1/lists', list);
|
||||
|
||||
export const apiUpdate = (list: Partial<ApiListJSON>) =>
|
||||
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
|
||||
|
||||
export const apiGetAccounts = (listId: string) =>
|
||||
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
export const apiGetAccountLists = (accountId: string) =>
|
||||
apiRequestGet<ApiListJSON[]>(`v1/accounts/${accountId}/lists`);
|
||||
|
||||
export const apiAddAccountToList = (listId: string, accountId: string) =>
|
||||
apiRequestPost(`v1/lists/${listId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
||||
|
||||
export const apiRemoveAccountFromList = (listId: string, accountId: string) =>
|
||||
apiRequestDelete(`v1/lists/${listId}/accounts`, {
|
||||
account_ids: [accountId],
|
||||
});
|
8
app/javascript/mastodon/api/suggestions.ts
Normal file
8
app/javascript/mastodon/api/suggestions.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { apiRequestGet, apiRequestDelete } from 'mastodon/api';
|
||||
import type { ApiSuggestionJSON } from 'mastodon/api_types/suggestions';
|
||||
|
||||
export const apiGetSuggestions = (limit: number) =>
|
||||
apiRequestGet<ApiSuggestionJSON[]>('v2/suggestions', { limit });
|
||||
|
||||
export const apiDeleteSuggestion = (accountId: string) =>
|
||||
apiRequestDelete(`v1/suggestions/${accountId}`);
|
16
app/javascript/mastodon/api_types/antennas.ts
Normal file
16
app/javascript/mastodon/api_types/antennas.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
// See app/serializers/rest/antenna_serializer.rb
|
||||
|
||||
import type { ApiListJSON } from './lists';
|
||||
|
||||
export interface ApiAntennaJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
stl: boolean;
|
||||
ltl: boolean;
|
||||
insert_feeds: boolean;
|
||||
with_media_only: boolean;
|
||||
ignore_reblog: boolean;
|
||||
list: ApiListJSON | null;
|
||||
|
||||
list_id: string | undefined;
|
||||
}
|
6
app/javascript/mastodon/api_types/bookmark_categories.ts
Normal file
6
app/javascript/mastodon/api_types/bookmark_categories.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// See app/serializers/rest/bookmark_category_serializer.rb
|
||||
|
||||
export interface ApiBookmarkCategoryJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
6
app/javascript/mastodon/api_types/circles.ts
Normal file
6
app/javascript/mastodon/api_types/circles.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// See app/serializers/rest/circle_serializer.rb
|
||||
|
||||
export interface ApiCircleJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
// A similar definition will eventually be added in the main house. These definitions will replace it.
|
||||
|
||||
export interface ApiListJSON_KmyDummy {
|
||||
id: string;
|
||||
title: string;
|
||||
exclusive: boolean;
|
||||
notify: boolean;
|
||||
|
||||
// replies_policy
|
||||
// antennas
|
||||
}
|
14
app/javascript/mastodon/api_types/lists.ts
Normal file
14
app/javascript/mastodon/api_types/lists.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
// See app/serializers/rest/list_serializer.rb
|
||||
|
||||
import type { ApiAntennaJSON } from './antennas';
|
||||
|
||||
export type RepliesPolicyType = 'list' | 'followed' | 'none';
|
||||
|
||||
export interface ApiListJSON {
|
||||
id: string;
|
||||
title: string;
|
||||
exclusive: boolean;
|
||||
replies_policy: RepliesPolicyType;
|
||||
notify: boolean;
|
||||
antennas?: ApiAntennaJSON[];
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
import type { AccountWarningAction } from 'mastodon/models/notification_group';
|
||||
|
||||
import type { ApiAccountJSON } from './accounts';
|
||||
import type { ApiListJSON_KmyDummy } from './dummy_types';
|
||||
import type { ApiListJSON } from './lists';
|
||||
import type { ApiReportJSON } from './reports';
|
||||
import type { ApiStatusJSON } from './statuses';
|
||||
|
||||
|
@ -71,7 +71,7 @@ export interface BaseNotificationJSON {
|
|||
group_key: string;
|
||||
account: ApiAccountJSON;
|
||||
emoji_reaction?: NotifyEmojiReactionJSON;
|
||||
list?: ApiListJSON_KmyDummy;
|
||||
list?: ApiListJSON;
|
||||
}
|
||||
|
||||
export interface BaseNotificationGroupJSON {
|
||||
|
@ -84,7 +84,7 @@ export interface BaseNotificationGroupJSON {
|
|||
page_min_id?: string;
|
||||
page_max_id?: string;
|
||||
emoji_reaction_groups?: NotificationEmojiReactionGroupJSON[];
|
||||
list?: ApiListJSON_KmyDummy;
|
||||
list?: ApiListJSON;
|
||||
}
|
||||
|
||||
interface NotificationGroupWithStatusJSON extends BaseNotificationGroupJSON {
|
||||
|
|
13
app/javascript/mastodon/api_types/suggestions.ts
Normal file
13
app/javascript/mastodon/api_types/suggestions.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
|
||||
export type ApiSuggestionSourceJSON =
|
||||
| 'featured'
|
||||
| 'most_followed'
|
||||
| 'most_interactions'
|
||||
| 'similar_to_recently_followed'
|
||||
| 'friends_of_friends';
|
||||
|
||||
export interface ApiSuggestionJSON {
|
||||
sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]];
|
||||
account: ApiAccountJSON;
|
||||
}
|
|
@ -10,6 +10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { EmptyAccount } from 'mastodon/components/empty_account';
|
||||
import { FollowButton } from 'mastodon/components/follow_button';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
|
||||
|
@ -23,9 +24,6 @@ import { DisplayName } from './display_name';
|
|||
import { RelativeTimestamp } from './relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
mute_notifications: { id: 'account.mute_notifications_short', defaultMessage: 'Mute notifications' },
|
||||
|
@ -35,13 +33,9 @@ const messages = defineMessages({
|
|||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => {
|
||||
const Account = ({ size = 46, account, onBlock, onMute, onMuteNotifications, hidden, hideButtons, minimal, defaultAction, children, withBio }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleFollow = useCallback(() => {
|
||||
onFollow(account);
|
||||
}, [onFollow, account]);
|
||||
|
||||
const handleBlock = useCallback(() => {
|
||||
onBlock(account);
|
||||
}, [onBlock, account]);
|
||||
|
@ -74,13 +68,12 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
|
|||
let buttons;
|
||||
|
||||
if (!hideButtons && account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = <Button text={intl.formatMessage(messages.cancel_follow_request)} onClick={handleFollow} />;
|
||||
buttons = <FollowButton accountId={account.get('id')} />;
|
||||
} else if (blocking) {
|
||||
buttons = <Button text={intl.formatMessage(messages.unblock)} onClick={handleBlock} />;
|
||||
} else if (muting) {
|
||||
|
@ -109,9 +102,11 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
|
|||
buttons = <Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />;
|
||||
} else if (defaultAction === 'block') {
|
||||
buttons = <Button text={intl.formatMessage(messages.block)} onClick={handleBlock} />;
|
||||
} else if (!account.get('suspended') && !account.get('moved') || following) {
|
||||
buttons = <Button text={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={handleFollow} />;
|
||||
} else {
|
||||
buttons = <FollowButton accountId={account.get('id')} />;
|
||||
}
|
||||
} else if (!hideButtons) {
|
||||
buttons = <FollowButton accountId={account.get('id')} />;
|
||||
}
|
||||
|
||||
let muteTimeRemaining;
|
||||
|
@ -179,7 +174,6 @@ const Account = ({ size = 46, account, onFollow, onBlock, onMute, onMuteNotifica
|
|||
Account.propTypes = {
|
||||
size: PropTypes.number,
|
||||
account: ImmutablePropTypes.record,
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onMuteNotifications: PropTypes.func,
|
||||
|
|
|
@ -7,11 +7,11 @@ import { Icon } from './icon';
|
|||
|
||||
interface Props {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
indeterminate: boolean;
|
||||
name: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label: React.ReactNode;
|
||||
checked?: boolean;
|
||||
indeterminate?: boolean;
|
||||
name?: string;
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const CheckBox: React.FC<Props> = ({
|
||||
|
@ -30,6 +30,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||
value={value}
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
readOnly={!onChange}
|
||||
/>
|
||||
|
||||
<span
|
||||
|
@ -42,7 +43,7 @@ export const CheckBox: React.FC<Props> = ({
|
|||
)}
|
||||
</span>
|
||||
|
||||
<span>{label}</span>
|
||||
{label && <span>{label}</span>}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ function useHandleClick(onClick?: OnClickCallback) {
|
|||
}, [history, onClick]);
|
||||
}
|
||||
|
||||
export const ColumnBackButton: React.FC<{ onClick: OnClickCallback }> = ({
|
||||
export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const handleClick = useHandleClick(onClick);
|
||||
|
|
67
app/javascript/mastodon/components/column_search_header.tsx
Normal file
67
app/javascript/mastodon/components/column_search_header.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export const ColumnSearchHeader: React.FC<{
|
||||
onBack: () => void;
|
||||
onSubmit: (value: string) => void;
|
||||
onActivate: () => void;
|
||||
placeholder: string;
|
||||
active: boolean;
|
||||
}> = ({ onBack, onActivate, onSubmit, placeholder, active }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) {
|
||||
setValue('');
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(value);
|
||||
onSubmit(value);
|
||||
},
|
||||
[setValue, onSubmit],
|
||||
);
|
||||
|
||||
const handleKeyUp = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onBack();
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
},
|
||||
[onBack],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
onActivate();
|
||||
}, [onActivate]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(value);
|
||||
}, [onSubmit, value]);
|
||||
|
||||
return (
|
||||
<form className='column-search-header' onSubmit={handleSubmit}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='search'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
placeholder={placeholder}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
|
||||
{active && (
|
||||
<button type='button' className='link-button' onClick={onBack}>
|
||||
<FormattedMessage id='column_search.cancel' defaultMessage='Cancel' />
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -103,7 +103,12 @@ export const FollowButton: React.FC<{
|
|||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={relationship?.blocked_by || relationship?.blocking}
|
||||
disabled={
|
||||
relationship?.blocked_by ||
|
||||
relationship?.blocking ||
|
||||
(!(relationship?.following || relationship?.requested) &&
|
||||
(account?.suspended || !!account?.moved))
|
||||
}
|
||||
secondary={following}
|
||||
className={following ? 'button--destructive' : undefined}
|
||||
>
|
||||
|
|
|
@ -80,6 +80,7 @@ class ScrollableList extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
bindToDocument: PropTypes.bool,
|
||||
preventScroll: PropTypes.bool,
|
||||
footer: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -324,7 +325,7 @@ class ScrollableList extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
|
||||
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, footer, emptyMessage, onLoadMore } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
const childrenCount = Children.count(children);
|
||||
|
||||
|
@ -342,11 +343,13 @@ class ScrollableList extends PureComponent {
|
|||
<div className='scrollable__append'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
} else if (isLoading || childrenCount > 0 || numPending > 0 || hasMore || !emptyMessage) {
|
||||
scrollableArea = (
|
||||
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div className={classNames('scrollable scrollable--flex', { fullscreen })} ref={this.setRef} onMouseMove={this.handleMouseMove}>
|
||||
<div role='feed' className='item-list'>
|
||||
{prepend}
|
||||
|
||||
|
@ -375,6 +378,8 @@ class ScrollableList extends PureComponent {
|
|||
|
||||
{!hasMore && append}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -385,6 +390,8 @@ class ScrollableList extends PureComponent {
|
|||
<div className='empty-column-indicator'>
|
||||
{emptyMessage}
|
||||
</div>
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
|||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -92,7 +94,6 @@ class Status extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.record,
|
||||
contextType: PropTypes.string,
|
||||
previousId: PropTypes.string,
|
||||
nextInReplyToId: PropTypes.string,
|
||||
rootId: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onReply: PropTypes.func,
|
||||
|
@ -174,32 +175,18 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleClick = e => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.handleHotkeyOpen(e);
|
||||
};
|
||||
|
||||
handleMouseUp = e => {
|
||||
// Only handle clicks on the empty space above the content
|
||||
|
||||
if (e.target !== e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
this.handleHotkeyOpen();
|
||||
};
|
||||
|
||||
handlePrependAccountClick = e => {
|
||||
this.handleAccountClick(e, false);
|
||||
};
|
||||
|
||||
handleAccountClick = (e, proper = true) => {
|
||||
if (e && (e.button !== 0 || e.ctrlKey || e.metaKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
this._openProfile(proper);
|
||||
this.handleClick(e);
|
||||
};
|
||||
|
||||
handleExpandedToggle = () => {
|
||||
|
@ -297,7 +284,7 @@ class Status extends ImmutablePureComponent {
|
|||
this.props.onMention(this._properStatus().get('account'));
|
||||
};
|
||||
|
||||
handleHotkeyOpen = () => {
|
||||
handleHotkeyOpen = (e) => {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
return;
|
||||
|
@ -310,7 +297,13 @@ class Status extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
|
||||
const path = `/@${status.getIn(['account', 'acct'])}/${status.get('id')}`;
|
||||
|
||||
if (e?.button === 0 && !(e?.ctrlKey || e?.metaKey)) {
|
||||
history.push(path);
|
||||
} else if (e?.button === 1 || (e?.button === 0 && (e?.ctrlKey || e?.metaKey))) {
|
||||
window.open(path, '_blank', 'noreferrer noopener');
|
||||
}
|
||||
};
|
||||
|
||||
handleHotkeyOpenProfile = () => {
|
||||
|
@ -380,7 +373,7 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, nextInReplyToId, rootId, withoutQuote, skipPrepend, avatarSize = 46 } = this.props;
|
||||
const { intl, hidden, featured, unfocusable, unread, showThread, scrollKey, pictureInPicture, previousId, rootId, withoutQuote, skipPrepend, avatarSize = 46 } = this.props;
|
||||
|
||||
let { status, account, ...other } = this.props;
|
||||
|
||||
|
@ -408,7 +401,6 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
const connectUp = previousId && previousId === status.get('in_reply_to_id');
|
||||
const connectToRoot = rootId && rootId === status.get('in_reply_to_id');
|
||||
const connectReply = nextInReplyToId && nextInReplyToId === status.get('id');
|
||||
const matchedFilters = status.get('matched_filters');
|
||||
|
||||
let visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility');
|
||||
|
@ -427,7 +419,7 @@ class Status extends ImmutablePureComponent {
|
|||
<div className='status__prepend'>
|
||||
<div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div>
|
||||
<div className='status__prepend__icon'><VisibilityIcon visibility={visibilityName} /></div>
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <Link data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} to={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></Link> }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -590,31 +582,24 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
<div className={classNames('status', `status-${status.get('visibility_ex')}`, { 'status-reply': !!status.get('in_reply_to_id'), 'status--in-thread': !!rootId, 'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted })} data-id={status.get('id')}>
|
||||
|
||||
{(!matchedFilters || this.state.showDespiteFilter || status.get('filter_action_ex') === 'half_warn') && (
|
||||
<>
|
||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
||||
<div onClick={this.handleClick} className='status__info'>
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time' target='_blank' rel='noopener noreferrer'>
|
||||
<div onMouseUp={this.handleMouseUp} className='status__info'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} className='status__relative-time'>
|
||||
{withQuote}
|
||||
{withReference}
|
||||
{withExpiration}
|
||||
{withLimited}
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={visibilityName} /></span>
|
||||
<span className='status__visibility-icon'><VisibilityIcon visibility={status.get('visibility_ex')} /></span>
|
||||
<RelativeTimestamp timestamp={status.get('created_at')} />{status.get('edited_at') && <abbr title={intl.formatMessage(messages.edited, { date: intl.formatDate(status.get('edited_at'), { year: 'numeric', month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }) })}> *</abbr>}
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<a onClick={this.handleAccountClick} href={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<Link to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} data-hover-card-account={status.getIn(['account', 'id'])} className='status__display-name'>
|
||||
<div className='status__avatar'>
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{matchedFilters && <FilterWarning title={matchedFilters.join(', ')} expanded={this.state.showDespiteFilter} onClick={this.handleFilterToggle} />}
|
||||
|
||||
|
|
|
@ -204,8 +204,8 @@ class StatusContent extends PureComponent {
|
|||
element = element.parentNode;
|
||||
}
|
||||
|
||||
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
|
||||
this.props.onClick();
|
||||
if (deltaX + deltaY < 5 && (e.button === 0 || e.button === 1) && this.props.onClick) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
|
||||
this.startXY = null;
|
||||
|
|
|
@ -15,6 +15,13 @@ const mapStateToProps = state => ({
|
|||
openedViaKeyboard: state.dropdownMenu.keyboard,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {any} dispatch
|
||||
* @param {Object} root0
|
||||
* @param {any} [root0.status]
|
||||
* @param {any} root0.items
|
||||
* @param {any} [root0.scrollKey]
|
||||
*/
|
||||
const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({
|
||||
onOpen(id, onItemClick, keyboard) {
|
||||
if (status) {
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromAntennaAdder, addToAntennaAdder, removeExcludeFromAntennaAdder, addExcludeToAntennaAdder } from '../../../actions/antennas';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
|
||||
add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { antennaId, added }) => ({
|
||||
antenna: state.get('antennas').get(antennaId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['antennaAdder', 'antennas', 'items']).includes(antennaId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { antennaId }) => ({
|
||||
onRemove: () => dispatch(removeFromAntennaAdder(antennaId)),
|
||||
onAdd: () => dispatch(addToAntennaAdder(antennaId)),
|
||||
onExcludeRemove: () => dispatch(removeExcludeFromAntennaAdder(antennaId)),
|
||||
onExcludeAdd: () => dispatch(addExcludeToAntennaAdder(antennaId)),
|
||||
});
|
||||
|
||||
class Antenna extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
antenna: ImmutablePropTypes.map.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
onExcludeRemove: PropTypes.func.isRequired,
|
||||
onExcludeAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
handleRemove = () => {
|
||||
if (this.props.isExclude) {
|
||||
this.props.onExcludeRemove();
|
||||
} else {
|
||||
this.props.onRemove();
|
||||
}
|
||||
};
|
||||
|
||||
handleAdd = () => {
|
||||
if (this.props.isExclude) {
|
||||
this.props.onExcludeAdd();
|
||||
} else {
|
||||
this.props.onAdd();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { antenna, intl, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={this.handleRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={this.handleAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='wifi' icon={AntennaIcon} className='column-link__icon' fixedWidth />
|
||||
{antenna.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(Antenna));
|
|
@ -1,84 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import { setupAntennaAdder, resetAntennaAdder, setupExcludeAntennaAdder } from '../../actions/antennas';
|
||||
import NewAntennaForm from '../antennas/components/new_antenna_form';
|
||||
import Account from '../list_adder/components/account';
|
||||
|
||||
import Antenna from './components/antenna';
|
||||
// hack
|
||||
|
||||
const getOrderedAntennas = createSelector([state => state.get('antennas')], antennas => {
|
||||
if (!antennas) {
|
||||
return antennas;
|
||||
}
|
||||
|
||||
return antennas.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
antennaIds: getOrderedAntennas(state).map(antenna=>antenna.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupAntennaAdder(accountId)),
|
||||
onExcludeInitialize: accountId => dispatch(setupExcludeAntennaAdder(accountId)),
|
||||
onReset: () => dispatch(resetAntennaAdder()),
|
||||
});
|
||||
|
||||
class AntennaAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onExcludeInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
antennaIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { isExclude, onInitialize, onExcludeInitialize, accountId } = this.props;
|
||||
if (isExclude) {
|
||||
onExcludeInitialize(accountId);
|
||||
} else {
|
||||
onInitialize(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountId, antennaIds, isExclude } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewAntennaForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{antennaIds.map(antennaId => <Antenna key={antennaId} antennaId={antennaId} isExclude={isExclude} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaAdder));
|
227
app/javascript/mastodon/features/antenna_adder/index.tsx
Normal file
227
app/javascript/mastodon/features/antenna_adder/index.tsx
Normal file
|
@ -0,0 +1,227 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import { fetchAntennas } from 'mastodon/actions/antennas';
|
||||
import { createAntenna } from 'mastodon/actions/antennas_typed';
|
||||
import {
|
||||
apiGetAccountAntennas,
|
||||
apiAddAccountToAntenna,
|
||||
apiAddExcludeAccountToAntenna,
|
||||
apiRemoveAccountFromAntenna,
|
||||
apiRemoveExcludeAccountFromAntenna,
|
||||
} from 'mastodon/api/antennas';
|
||||
import type { ApiAntennaJSON } from 'mastodon/api_types/antennas';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedAntennas } from 'mastodon/selectors/antennas';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newAntenna: {
|
||||
id: 'antennas.new_antenna_name',
|
||||
defaultMessage: 'New antenna name',
|
||||
},
|
||||
createAntenna: {
|
||||
id: 'antennas.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const AntennaItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='antennas__item'>
|
||||
<div className='antennas__item__title'>
|
||||
<Icon id='antenna-ul' icon={AntennaIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewAntennaItem: React.FC<{
|
||||
onCreate: (antenna: ApiAntennaJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createAntenna({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='antennas__item' onSubmit={handleSubmit}>
|
||||
<label className='antennas__item__title'>
|
||||
<Icon id='antenna-ul' icon={AntennaIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newAntenna)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button text={intl.formatMessage(messages.createAntenna)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const AntennaAdder: React.FC<{
|
||||
accountId: string;
|
||||
isExclude: boolean;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, isExclude, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const antennas = useAppSelector((state) => getOrderedAntennas(state));
|
||||
const [antennaIds, setAntennaIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAntennas());
|
||||
|
||||
apiGetAccountAntennas(accountId)
|
||||
.then((data) => {
|
||||
setAntennaIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setAntennaIds, accountId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(antennaId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setAntennaIds((currentAntennaIds) => [antennaId, ...currentAntennaIds]);
|
||||
|
||||
const func = isExclude
|
||||
? apiAddExcludeAccountToAntenna
|
||||
: apiAddAccountToAntenna;
|
||||
|
||||
func(antennaId, accountId).catch(() => {
|
||||
setAntennaIds((currentAntennaIds) =>
|
||||
currentAntennaIds.filter((id) => id !== antennaId),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setAntennaIds((currentAntennaIds) =>
|
||||
currentAntennaIds.filter((id) => id !== antennaId),
|
||||
);
|
||||
|
||||
const func = isExclude
|
||||
? apiRemoveExcludeAccountFromAntenna
|
||||
: apiRemoveAccountFromAntenna;
|
||||
|
||||
func(antennaId, accountId).catch(() => {
|
||||
setAntennaIds((currentAntennaIds) => [
|
||||
antennaId,
|
||||
...currentAntennaIds,
|
||||
]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setAntennaIds, accountId, isExclude],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(antenna: ApiAntennaJSON) => {
|
||||
setAntennaIds((currentAntennaIds) => [antenna.id, ...currentAntennaIds]);
|
||||
|
||||
apiAddAccountToAntenna(antenna.id, accountId).catch(() => {
|
||||
setAntennaIds((currentAntennaIds) =>
|
||||
currentAntennaIds.filter((id) => id !== antenna.id),
|
||||
);
|
||||
});
|
||||
},
|
||||
[setAntennaIds, accountId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='antennas.add_to_antennas'
|
||||
defaultMessage='Add {name} to antennas'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='antennas-scrollable'>
|
||||
<NewAntennaItem onCreate={handleCreate} />
|
||||
|
||||
{antennas.map((antenna) => (
|
||||
<AntennaItem
|
||||
key={antenna.id}
|
||||
id={antenna.id}
|
||||
title={antenna.title}
|
||||
checked={antennaIds.includes(antenna.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AntennaAdder;
|
|
@ -1,88 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { removeFromAntennaEditor, addToAntennaEditor, removeExcludeFromAntennaEditor, addExcludeToAntennaEditor } from '../../../actions/antennas';
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
|
||||
add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['antennaEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromAntennaEditor(accountId)),
|
||||
onAdd: () => dispatch(addToAntennaEditor(accountId)),
|
||||
onExcludeRemove: () => dispatch(removeExcludeFromAntennaEditor(accountId)),
|
||||
onExcludeAdd: () => dispatch(addExcludeToAntennaEditor(accountId)),
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
onExcludeRemove: PropTypes.func.isRequired,
|
||||
onExcludeAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, isExclude, onRemove, onAdd, onExcludeRemove, onExcludeAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={isExclude ? onExcludeRemove : onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={isExclude ? onExcludeAdd : onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeAntennaEditorTitle, submitAntennaEditor } from '../../../actions/antennas';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'antennas.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['antennaEditor', 'title']),
|
||||
disabled: !state.getIn(['antennaEditor', 'isChanged']) || !state.getIn(['antennaEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeAntennaEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitAntennaEditor(false)),
|
||||
});
|
||||
|
||||
class AntennaForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaForm));
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchAntennaSuggestions, clearAntennaSuggestions, changeAntennaSuggestions } from '../../../actions/antennas';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'antennas.search', defaultMessage: 'Search among people you follow' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['antennaEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchAntennaSuggestions(value)),
|
||||
onClear: () => dispatch(clearAntennaSuggestions()),
|
||||
onChange: value => dispatch(changeAntennaSuggestions(value)),
|
||||
});
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
|
@ -1,84 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { setupAntennaEditor, setupExcludeAntennaEditor, clearAntennaSuggestions, resetAntennaEditor } from '../../actions/antennas';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import Account from './components/account';
|
||||
import EditAntennaForm from './components/edit_antenna_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
accountIds: state.getIn(['antennaEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['antennaEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { isExclude }) => ({
|
||||
onInitialize: antennaId => dispatch(isExclude ? setupExcludeAntennaEditor(antennaId) : setupAntennaEditor(antennaId)),
|
||||
onClear: () => dispatch(clearAntennaSuggestions()),
|
||||
onReset: () => dispatch(resetAntennaEditor()),
|
||||
});
|
||||
|
||||
class AntennaEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
antennaId: PropTypes.string.isRequired,
|
||||
isExclude: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, antennaId } = this.props;
|
||||
onInitialize(antennaId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear, isExclude } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditAntennaForm />
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} isExclude={isExclude} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} isExclude={isExclude} />)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaEditor));
|
|
@ -1,46 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
class RadioPanel extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
values: ImmutablePropTypes.list.isRequired,
|
||||
value: PropTypes.object.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
const value = e.currentTarget.getAttribute('data-value');
|
||||
|
||||
if (value !== this.props.value.get('value')) {
|
||||
this.props.onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { values, value } = this.props;
|
||||
|
||||
return (
|
||||
<div className='setting-radio-panel'>
|
||||
{values.map((val) => (
|
||||
<button className={classNames('setting-radio-panel__item', {'setting-radio-panel__item__active': value.get('value') === val.get('value')})}
|
||||
key={val.get('value')} onClick={this.handleChange} data-value={val.get('value')}>
|
||||
{val.get('label')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect()(injectIntl(RadioPanel));
|
|
@ -1,104 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
class TextListItem extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
icon: PropTypes.string.isRequired,
|
||||
iconComponent: PropTypes.func.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleRemove = () => {
|
||||
this.props.onRemove(this.props.value);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { icon, iconComponent, value } = this.props;
|
||||
|
||||
return (
|
||||
<div className='setting-text-list-item'>
|
||||
<Icon id={icon} icon={iconComponent} />
|
||||
<span className='label'>{value}</span>
|
||||
<IconButton icon='trash' iconComponent={DeleteIcon} onClick={this.handleRemove} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TextList extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
values: ImmutablePropTypes.list.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
icon: PropTypes.string.isRequired,
|
||||
iconComponent: PropTypes.func.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleAdd = () => {
|
||||
this.props.onAdd();
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.handleAdd();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { icon, iconComponent, value, values, disabled, label, title } = this.props;
|
||||
|
||||
return (
|
||||
<div className='setting-text-list'>
|
||||
{values.map((val) => (
|
||||
<TextListItem key={val} value={val} icon={icon} iconComponent={iconComponent} onRemove={this.props.onRemove} />
|
||||
))}
|
||||
|
||||
<form className='add-text-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={this.handleAdd}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect()(injectIntl(TextList));
|
|
@ -1,587 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Select, { NonceProvider } from 'react-select';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import DomainIcon from '@/material-icons/400-24px/dns.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||
import KeywordIcon from '@/material-icons/400-24px/title.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import {
|
||||
fetchAntenna,
|
||||
deleteAntenna,
|
||||
updateAntenna,
|
||||
addDomainToAntenna,
|
||||
removeDomainFromAntenna,
|
||||
addExcludeDomainToAntenna,
|
||||
removeExcludeDomainFromAntenna,
|
||||
fetchAntennaDomains,
|
||||
fetchAntennaKeywords,
|
||||
removeKeywordFromAntenna,
|
||||
addKeywordToAntenna,
|
||||
removeExcludeKeywordFromAntenna,
|
||||
addExcludeKeywordToAntenna,
|
||||
fetchAntennaTags,
|
||||
removeTagFromAntenna,
|
||||
addTagToAntenna,
|
||||
removeExcludeTagFromAntenna,
|
||||
addExcludeTagToAntenna,
|
||||
} from 'mastodon/actions/antennas';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { enableLocalTimeline } from 'mastodon/initial_state';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import RadioPanel from './components/radio_panel';
|
||||
import TextList from './components/text_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_antenna.message', defaultMessage: 'Are you sure you want to permanently delete this antenna?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_antenna.confirm', defaultMessage: 'Delete' },
|
||||
editAccounts: { id: 'antennas.edit_accounts', defaultMessage: 'Edit accounts' },
|
||||
noOptions: { id: 'antennas.select.no_options_message', defaultMessage: 'Empty lists' },
|
||||
placeholder: { id: 'antennas.select.placeholder', defaultMessage: 'Select list' },
|
||||
addDomainLabel: { id: 'antennas.add_domain_placeholder', defaultMessage: 'New domain' },
|
||||
addKeywordLabel: { id: 'antennas.add_keyword_placeholder', defaultMessage: 'New keyword' },
|
||||
addTagLabel: { id: 'antennas.add_tag_placeholder', defaultMessage: 'New tag' },
|
||||
addDomainTitle: { id: 'antennas.add_domain', defaultMessage: 'Add domain' },
|
||||
addKeywordTitle: { id: 'antennas.add_keyword', defaultMessage: 'Add keyword' },
|
||||
addTagTitle: { id: 'antennas.add_tag', defaultMessage: 'Add tag' },
|
||||
accounts: { id: 'antennas.accounts', defaultMessage: '{count} accounts' },
|
||||
domains: { id: 'antennas.domains', defaultMessage: '{count} domains' },
|
||||
tags: { id: 'antennas.tags', defaultMessage: '{count} tags' },
|
||||
keywords: { id: 'antennas.keywords', defaultMessage: '{count} keywords' },
|
||||
setHome: { id: 'antennas.select.set_home', defaultMessage: 'Set home' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
antenna: state.getIn(['antennas', props.params.id]),
|
||||
lists: state.get('lists'),
|
||||
domains: state.getIn(['antennas', props.params.id, 'domains']) || ImmutableMap(),
|
||||
keywords: state.getIn(['antennas', props.params.id, 'keywords']) || ImmutableMap(),
|
||||
tags: state.getIn(['antennas', props.params.id, 'tags']) || ImmutableMap(),
|
||||
});
|
||||
|
||||
class AntennaSetting extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
antenna: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
lists: ImmutablePropTypes.map,
|
||||
domains: ImmutablePropTypes.map,
|
||||
keywords: ImmutablePropTypes.map,
|
||||
tags: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
state = {
|
||||
domainName: '',
|
||||
excludeDomainName: '',
|
||||
keywordName: '',
|
||||
excludeKeywordName: '',
|
||||
tagName: '',
|
||||
excludeTagName: '',
|
||||
rangeRadioValue: null,
|
||||
contentRadioValue: null,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('ANTENNA', { id: this.props.params.id }));
|
||||
this.props.history.push('/');
|
||||
}
|
||||
};
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(fetchAntenna(id));
|
||||
dispatch(fetchAntennaDomains(id));
|
||||
dispatch(fetchAntennaKeywords(id));
|
||||
dispatch(fetchAntennaTags(id));
|
||||
dispatch(fetchLists());
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
dispatch(fetchAntenna(id));
|
||||
dispatch(fetchAntennaKeywords(id));
|
||||
dispatch(fetchAntennaDomains(id));
|
||||
dispatch(fetchAntennaKeywords(id));
|
||||
dispatch(fetchAntennaTags(id));
|
||||
dispatch(fetchLists());
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'ANTENNA_EDITOR',
|
||||
modalProps: { antennaId: this.props.params.id, isExclude: false },
|
||||
}));
|
||||
};
|
||||
|
||||
handleExcludeEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'ANTENNA_EDITOR',
|
||||
modalProps: { antennaId: this.props.params.id, isExclude: true },
|
||||
}));
|
||||
};
|
||||
|
||||
handleEditAntennaClick = () => {
|
||||
window.open(`/antennas/${this.props.params.id}/edit`, '_blank');
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteAntenna(id));
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
this.props.history.push('/antennasw');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleTimelineClick = () => {
|
||||
this.props.history.push(`/antennast/${this.props.params.id}`);
|
||||
};
|
||||
|
||||
onStlToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, undefined, target.checked, undefined, undefined, undefined, undefined));
|
||||
};
|
||||
|
||||
onLtlToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, undefined, undefined, target.checked, undefined, undefined, undefined));
|
||||
};
|
||||
|
||||
onMediaOnlyToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, target.checked, undefined, undefined));
|
||||
};
|
||||
|
||||
onIgnoreReblogToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, target.checked, undefined));
|
||||
};
|
||||
|
||||
onNoInsertFeedsToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, undefined, undefined, undefined, undefined, undefined, target.checked));
|
||||
};
|
||||
|
||||
onSelect = value => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, value.value, undefined, undefined, undefined, undefined, undefined));
|
||||
};
|
||||
|
||||
onHomeSelect = () => this.onSelect({ value: '0' });
|
||||
|
||||
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
|
||||
|
||||
onRangeRadioChanged = (value) => this.setState({ rangeRadioValue: value });
|
||||
|
||||
onContentRadioChanged = (value) => this.setState({ contentRadioValue: value });
|
||||
|
||||
onDomainNameChanged = (value) => this.setState({ domainName: value });
|
||||
|
||||
onDomainAdd = () => {
|
||||
this.props.dispatch(addDomainToAntenna(this.props.params.id, this.state.domainName));
|
||||
this.setState({ domainName: '' });
|
||||
};
|
||||
|
||||
onDomainRemove = (value) => this.props.dispatch(removeDomainFromAntenna(this.props.params.id, value));
|
||||
|
||||
onKeywordNameChanged = (value) => this.setState({ keywordName: value });
|
||||
|
||||
onKeywordAdd = () => {
|
||||
this.props.dispatch(addKeywordToAntenna(this.props.params.id, this.state.keywordName));
|
||||
this.setState({ keywordName: '' });
|
||||
};
|
||||
|
||||
onKeywordRemove = (value) => this.props.dispatch(removeKeywordFromAntenna(this.props.params.id, value));
|
||||
|
||||
onTagNameChanged = (value) => this.setState({ tagName: value });
|
||||
|
||||
onTagAdd = () => {
|
||||
this.props.dispatch(addTagToAntenna(this.props.params.id, this.state.tagName));
|
||||
this.setState({ tagName: '' });
|
||||
};
|
||||
|
||||
onTagRemove = (value) => this.props.dispatch(removeTagFromAntenna(this.props.params.id, value));
|
||||
|
||||
onExcludeDomainNameChanged = (value) => this.setState({ excludeDomainName: value });
|
||||
|
||||
onExcludeDomainAdd = () => {
|
||||
this.props.dispatch(addExcludeDomainToAntenna(this.props.params.id, this.state.excludeDomainName));
|
||||
this.setState({ excludeDomainName: '' });
|
||||
};
|
||||
|
||||
onExcludeDomainRemove = (value) => this.props.dispatch(removeExcludeDomainFromAntenna(this.props.params.id, value));
|
||||
|
||||
onExcludeKeywordNameChanged = (value) => this.setState({ excludeKeywordName: value });
|
||||
|
||||
onExcludeKeywordAdd = () => {
|
||||
this.props.dispatch(addExcludeKeywordToAntenna(this.props.params.id, this.state.excludeKeywordName));
|
||||
this.setState({ excludeKeywordName: '' });
|
||||
};
|
||||
|
||||
onExcludeKeywordRemove = (value) => this.props.dispatch(removeExcludeKeywordFromAntenna(this.props.params.id, value));
|
||||
|
||||
onExcludeTagNameChanged = (value) => this.setState({ excludeTagName: value });
|
||||
|
||||
onExcludeTagAdd = () => {
|
||||
this.props.dispatch(addExcludeTagToAntenna(this.props.params.id, this.state.excludeTagName));
|
||||
this.setState({ excludeTagName: '' });
|
||||
};
|
||||
|
||||
onExcludeTagRemove = (value) => this.props.dispatch(removeExcludeTagFromAntenna(this.props.params.id, value));
|
||||
|
||||
render () {
|
||||
const { columnId, multiColumn, antenna, lists, domains, keywords, tags, intl } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = antenna ? antenna.get('title') : id;
|
||||
const isStl = antenna ? antenna.get('stl') : undefined;
|
||||
const isLtl = antenna ? antenna.get('ltl') : undefined;
|
||||
const isMediaOnly = antenna ? antenna.get('with_media_only') : undefined;
|
||||
const isIgnoreReblog = antenna ? antenna.get('ignore_reblog') : undefined;
|
||||
const isInsertFeeds = antenna ? antenna.get('insert_feeds') : undefined;
|
||||
|
||||
if (typeof antenna === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (antenna === false) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
let columnSettings;
|
||||
if (!isStl && !isLtl) {
|
||||
columnSettings = (
|
||||
<>
|
||||
<section className='similar-row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-mediaonly`} checked={isMediaOnly} onChange={this.onMediaOnlyToggle} />
|
||||
<label htmlFor={`antenna-${id}-mediaonly`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.media_only' defaultMessage='Media only' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='similar-row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-ignorereblog`} checked={isIgnoreReblog} onChange={this.onIgnoreReblogToggle} />
|
||||
<label htmlFor={`antenna-${id}-ignorereblog`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.ignore_reblog' defaultMessage='Exclude boosts' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let stlAlert;
|
||||
if (isStl) {
|
||||
stlAlert = (
|
||||
<div className='antenna-setting'>
|
||||
<p><FormattedMessage id='antennas.in_stl_mode' defaultMessage='This antenna is in STL mode.' /></p>
|
||||
</div>
|
||||
);
|
||||
} else if (isLtl) {
|
||||
stlAlert = (
|
||||
<div className='antenna-setting'>
|
||||
<p><FormattedMessage id='antennas.in_ltl_mode' defaultMessage='This antenna is in LTL mode.' /></p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rangeRadioValues = ImmutableList([
|
||||
ImmutableMap({ value: 'accounts', label: intl.formatMessage(messages.accounts, { count: antenna.get('accounts_count') }) }),
|
||||
ImmutableMap({ value: 'domains', label: intl.formatMessage(messages.domains, { count: antenna.get('domains_count') }) }),
|
||||
]);
|
||||
const rangeRadioValue = ImmutableMap({ value: this.state.rangeRadioValue || (antenna.get('domains_count') > 0 ? 'domains' : 'accounts') });
|
||||
const rangeRadioAlert = antenna.get(rangeRadioValue.get('value') === 'accounts' ? 'domains_count' : 'accounts_count') > 0;
|
||||
|
||||
const contentRadioValues = ImmutableList([
|
||||
ImmutableMap({ value: 'keywords', label: intl.formatMessage(messages.keywords, { count: antenna.get('keywords_count') }) }),
|
||||
ImmutableMap({ value: 'tags', label: intl.formatMessage(messages.tags, { count: antenna.get('tags_count') }) }),
|
||||
]);
|
||||
const contentRadioValue = ImmutableMap({ value: this.state.contentRadioValue || (antenna.get('tags_count') > 0 ? 'tags' : 'keywords') });
|
||||
const contentRadioAlert = antenna.get(contentRadioValue.get('value') === 'tags' ? 'keywords_count' : 'tags_count') > 0;
|
||||
|
||||
const listOptions = lists.toArray().filter((list) => list.length >= 2 && list[1]).map((list) => {
|
||||
return { value: list[1].get('id'), label: list[1].get('title') };
|
||||
});
|
||||
|
||||
const isShowStlToggle = !isLtl && (enableLocalTimeline || isStl);
|
||||
const isShowLtlToggle = !isStl && (enableLocalTimeline || isLtl);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
|
||||
<ColumnHeader
|
||||
icon='wifi'
|
||||
iconComponent={AntennaIcon}
|
||||
title={title}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditAntennaClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='antennas.edit_static' defaultMessage='Edit antenna' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='antennas.delete' defaultMessage='Delete antenna' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleTimelineClick}>
|
||||
<Icon id='wifi' icon={AntennaIcon} /> <FormattedMessage id='antennas.go_timeline' defaultMessage='Go to antenna timeline' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{isShowStlToggle && (
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-stl`} checked={isStl} onChange={this.onStlToggle} />
|
||||
<label htmlFor={`antenna-${id}-stl`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.stl' defaultMessage='STL mode' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{isShowLtlToggle && (
|
||||
<section className={isShowStlToggle && 'similar-row'}>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-ltl`} checked={isLtl} onChange={this.onLtlToggle} />
|
||||
<label htmlFor={`antenna-${id}-ltl`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.ltl' defaultMessage='LTL mode' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className={(isShowStlToggle || isShowLtlToggle) && 'similar-row'}>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-noinsertfeeds`} checked={isInsertFeeds} onChange={this.onNoInsertFeedsToggle} />
|
||||
<label htmlFor={`antenna-${id}-noinsertfeeds`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.insert_feeds' defaultMessage='Insert to feeds' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{columnSettings}
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
{stlAlert}
|
||||
<div className='antenna-setting'>
|
||||
{isInsertFeeds && (
|
||||
<>
|
||||
{antenna.get('list') ? (
|
||||
<p><FormattedMessage id='antennas.related_list' defaultMessage='This antenna is related to {listTitle}.' values={{ listTitle: antenna.getIn(['list', 'title']) }} /></p>
|
||||
) : (
|
||||
<p><FormattedMessage id='antennas.not_related_list' defaultMessage='This antenna is not related list. Posts will appear in home timeline. Open edit page to set list.' /></p>
|
||||
)}
|
||||
|
||||
<NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content} cacheKey='lists'>
|
||||
<Select
|
||||
value={{ value: antenna.getIn(['list', 'id']), label: antenna.getIn(['list', 'title']) }}
|
||||
options={listOptions}
|
||||
noOptionsMessage={this.noOptionsMessage}
|
||||
onChange={this.onSelect}
|
||||
className='column-content-select__container'
|
||||
classNamePrefix='column-content-select'
|
||||
name='lists'
|
||||
placeholder={this.props.intl.formatMessage(messages.placeholder)}
|
||||
defaultOptions
|
||||
/>
|
||||
</NonceProvider>
|
||||
|
||||
<Button secondary text={this.props.intl.formatMessage(messages.setHome)} onClick={this.onHomeSelect} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isStl && !isLtl && (
|
||||
<>
|
||||
<h2><FormattedMessage id='antennas.filter' defaultMessage='Filter' /></h2>
|
||||
<RadioPanel values={rangeRadioValues} value={rangeRadioValue} onChange={this.onRangeRadioChanged} />
|
||||
|
||||
{rangeRadioValue.get('value') === 'accounts' && <Button text={intl.formatMessage(messages.editAccounts)} onClick={this.handleEditClick} />}
|
||||
|
||||
{rangeRadioValue.get('value') === 'domains' && (
|
||||
<TextList
|
||||
onChange={this.onDomainNameChanged}
|
||||
onAdd={this.onDomainAdd}
|
||||
onRemove={this.onDomainRemove}
|
||||
value={this.state.domainName}
|
||||
values={domains.get('domains') || ImmutableList()}
|
||||
icon='sitemap'
|
||||
iconComponent={DomainIcon}
|
||||
label={intl.formatMessage(messages.addDomainLabel)}
|
||||
title={intl.formatMessage(messages.addDomainTitle)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rangeRadioAlert && <div className='alert'><FormattedMessage id='antennas.warnings.range_radio' defaultMessage='Simultaneous account and domain designation is not recommended.' /></div>}
|
||||
|
||||
<RadioPanel values={contentRadioValues} value={contentRadioValue} onChange={this.onContentRadioChanged} />
|
||||
|
||||
{contentRadioValue.get('value') === 'tags' && (
|
||||
<TextList
|
||||
onChange={this.onTagNameChanged}
|
||||
onAdd={this.onTagAdd}
|
||||
onRemove={this.onTagRemove}
|
||||
value={this.state.tagName}
|
||||
values={tags.get('tags') || ImmutableList()}
|
||||
icon='hashtag'
|
||||
iconComponent={HashtagIcon}
|
||||
label={intl.formatMessage(messages.addTagLabel)}
|
||||
title={intl.formatMessage(messages.addTagTitle)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{contentRadioValue.get('value') === 'keywords' && (
|
||||
<TextList
|
||||
onChange={this.onKeywordNameChanged}
|
||||
onAdd={this.onKeywordAdd}
|
||||
onRemove={this.onKeywordRemove}
|
||||
value={this.state.keywordName}
|
||||
values={keywords.get('keywords') || ImmutableList()}
|
||||
icon='paragraph'
|
||||
iconComponent={KeywordIcon}
|
||||
label={intl.formatMessage(messages.addKeywordLabel)}
|
||||
title={intl.formatMessage(messages.addKeywordTitle)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{contentRadioAlert && <div className='alert'><FormattedMessage id='antennas.warnings.content_radio' defaultMessage='Simultaneous keyword and tag designation is not recommended.' /></div>}
|
||||
|
||||
<h2><FormattedMessage id='antennas.filter_not' defaultMessage='Filter Not' /></h2>
|
||||
<h3><FormattedMessage id='antennas.exclude_accounts' defaultMessage='Exclude accounts' /></h3>
|
||||
<Button text={intl.formatMessage(messages.editAccounts)} onClick={this.handleExcludeEditClick} />
|
||||
<h3><FormattedMessage id='antennas.exclude_domains' defaultMessage='Exclude domains' /></h3>
|
||||
<TextList
|
||||
onChange={this.onExcludeDomainNameChanged}
|
||||
onAdd={this.onExcludeDomainAdd}
|
||||
onRemove={this.onExcludeDomainRemove}
|
||||
value={this.state.excludeDomainName}
|
||||
values={domains.get('exclude_domains') || ImmutableList()}
|
||||
icon='sitemap'
|
||||
iconComponent={DomainIcon}
|
||||
label={intl.formatMessage(messages.addDomainLabel)}
|
||||
title={intl.formatMessage(messages.addDomainTitle)}
|
||||
/>
|
||||
<h3><FormattedMessage id='antennas.exclude_keywords' defaultMessage='Exclude keywords' /></h3>
|
||||
<TextList
|
||||
onChange={this.onExcludeKeywordNameChanged}
|
||||
onAdd={this.onExcludeKeywordAdd}
|
||||
onRemove={this.onExcludeKeywordRemove}
|
||||
value={this.state.excludeKeywordName}
|
||||
values={keywords.get('exclude_keywords') || ImmutableList()}
|
||||
icon='paragraph'
|
||||
iconComponent={KeywordIcon}
|
||||
label={intl.formatMessage(messages.addKeywordLabel)}
|
||||
title={intl.formatMessage(messages.addKeywordTitle)}
|
||||
/>
|
||||
<h3><FormattedMessage id='antennas.exclude_tags' defaultMessage='Exclude tags' /></h3>
|
||||
<TextList
|
||||
onChange={this.onExcludeTagNameChanged}
|
||||
onAdd={this.onExcludeTagAdd}
|
||||
onRemove={this.onExcludeTagRemove}
|
||||
value={this.state.excludeTagName}
|
||||
values={tags.get('exclude_tags') || ImmutableList()}
|
||||
icon='hashtag'
|
||||
iconComponent={HashtagIcon}
|
||||
label={intl.formatMessage(messages.addTagLabel)}
|
||||
title={intl.formatMessage(messages.addTagTitle)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(AntennaSetting)));
|
|
@ -113,7 +113,7 @@ class AntennaTimeline extends PureComponent {
|
|||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.history.push(`/antennasw/${this.props.params.id}`);
|
||||
this.props.history.push(`/antennas/${this.props.params.id}/edit`);
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeAntennaEditorTitle, submitAntennaEditor } from 'mastodon/actions/antennas';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'antennas.new.title_placeholder', defaultMessage: 'New antenna title' },
|
||||
title: { id: 'antennas.new.create', defaultMessage: 'Add antenna' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['antennaEditor', 'title']),
|
||||
disabled: state.getIn(['antennaEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeAntennaEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitAntennaEditor(true)),
|
||||
});
|
||||
|
||||
class NewAntennaForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewAntennaForm));
|
719
app/javascript/mastodon/features/antennas/filtering.tsx
Normal file
719
app/javascript/mastodon/features/antennas/filtering.tsx
Normal file
|
@ -0,0 +1,719 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import DomainIcon from '@/material-icons/400-24px/dns.svg?react';
|
||||
import HashtagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||
import KeywordIcon from '@/material-icons/400-24px/title.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import { fetchAntenna } from 'mastodon/actions/antennas';
|
||||
import {
|
||||
apiGetAccounts,
|
||||
apiGetDomains,
|
||||
apiAddDomain,
|
||||
apiRemoveDomain,
|
||||
apiGetTags,
|
||||
apiAddTag,
|
||||
apiRemoveTag,
|
||||
apiGetKeywords,
|
||||
apiAddKeyword,
|
||||
apiRemoveKeyword,
|
||||
apiAddExcludeDomain,
|
||||
apiRemoveExcludeDomain,
|
||||
apiAddExcludeTag,
|
||||
apiRemoveExcludeTag,
|
||||
apiAddExcludeKeyword,
|
||||
apiRemoveExcludeKeyword,
|
||||
apiGetExcludeAccounts,
|
||||
} from 'mastodon/api/antennas';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: {
|
||||
id: 'confirmations.delete_antenna.message',
|
||||
defaultMessage: 'Are you sure you want to permanently delete this antenna?',
|
||||
},
|
||||
deleteConfirm: {
|
||||
id: 'confirmations.delete_antenna.confirm',
|
||||
defaultMessage: 'Delete',
|
||||
},
|
||||
editAccounts: {
|
||||
id: 'antennas.edit_accounts',
|
||||
defaultMessage: 'Edit accounts',
|
||||
},
|
||||
noOptions: {
|
||||
id: 'antennas.select.no_options_message',
|
||||
defaultMessage: 'Empty lists',
|
||||
},
|
||||
placeholder: {
|
||||
id: 'antennas.select.placeholder',
|
||||
defaultMessage: 'Select list',
|
||||
},
|
||||
addDomainLabel: {
|
||||
id: 'antennas.add_domain_placeholder',
|
||||
defaultMessage: 'New domain',
|
||||
},
|
||||
addKeywordLabel: {
|
||||
id: 'antennas.add_keyword_placeholder',
|
||||
defaultMessage: 'New keyword',
|
||||
},
|
||||
addTagLabel: {
|
||||
id: 'antennas.add_tag_placeholder',
|
||||
defaultMessage: 'New tag',
|
||||
},
|
||||
addDomainTitle: { id: 'antennas.add_domain', defaultMessage: 'Add domain' },
|
||||
addKeywordTitle: {
|
||||
id: 'antennas.add_keyword',
|
||||
defaultMessage: 'Add keyword',
|
||||
},
|
||||
addTagTitle: { id: 'antennas.add_tag', defaultMessage: 'Add tag' },
|
||||
accounts: { id: 'antennas.accounts', defaultMessage: '{count} accounts' },
|
||||
domains: { id: 'antennas.domains', defaultMessage: '{count} domains' },
|
||||
tags: { id: 'antennas.tags', defaultMessage: '{count} tags' },
|
||||
keywords: { id: 'antennas.keywords', defaultMessage: '{count} keywords' },
|
||||
setHome: { id: 'antennas.select.set_home', defaultMessage: 'Set home' },
|
||||
});
|
||||
|
||||
const TextListItem: React.FC<{
|
||||
icon: string;
|
||||
iconComponent: IconProp;
|
||||
value: string;
|
||||
onRemove: (value: string) => void;
|
||||
}> = ({ icon, iconComponent, value, onRemove }) => {
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove(value);
|
||||
}, [onRemove, value]);
|
||||
|
||||
return (
|
||||
<div className='setting-text-list-item'>
|
||||
<Icon id={icon} icon={iconComponent} />
|
||||
<span className='label'>{value}</span>
|
||||
<IconButton
|
||||
title='Delete'
|
||||
icon='trash'
|
||||
iconComponent={DeleteIcon}
|
||||
onClick={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TextList: React.FC<{
|
||||
values: string[];
|
||||
disabled?: boolean;
|
||||
icon: string;
|
||||
iconComponent: IconProp;
|
||||
label: string;
|
||||
title: string;
|
||||
onAdd: (value: string) => void;
|
||||
onRemove: (value: string) => void;
|
||||
}> = ({
|
||||
values,
|
||||
disabled,
|
||||
icon,
|
||||
iconComponent,
|
||||
label,
|
||||
title,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(value);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
onAdd(value);
|
||||
setValue('');
|
||||
}, [onAdd, value]);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(removeValue: string) => {
|
||||
onRemove(removeValue);
|
||||
},
|
||||
[onRemove],
|
||||
);
|
||||
|
||||
const handleSubmit = handleAdd;
|
||||
|
||||
return (
|
||||
<div className='setting-text-list'>
|
||||
<form className='add-text-form' onSubmit={handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={handleValueChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={handleAdd}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{values.map((val) => (
|
||||
<TextListItem
|
||||
key={val}
|
||||
value={val}
|
||||
icon={icon}
|
||||
iconComponent={iconComponent}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RadioPanel: React.FC<{
|
||||
items: { title: string; value: string }[];
|
||||
valueLengths: number[];
|
||||
alertMessage: React.ReactElement;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ items, valueLengths, alertMessage, onChange }) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (valueLengths.length >= 2) {
|
||||
setError(valueLengths.filter((v) => v > 0).length > 1);
|
||||
} else {
|
||||
setError(false);
|
||||
}
|
||||
}, [valueLengths]);
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length > 0) {
|
||||
for (let i = 0; i < valueLengths.length; i++) {
|
||||
const length = valueLengths[i] ?? 0;
|
||||
const item = items[i] ?? { value: '' };
|
||||
if (length > 0) {
|
||||
setValue(item.value);
|
||||
onChange(item.value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setValue(items[0]?.value ?? '');
|
||||
}
|
||||
}, [items, valueLengths, setValue, onChange]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const selected = currentTarget.getAttribute('data-value') ?? '';
|
||||
if (value !== selected) {
|
||||
onChange(selected);
|
||||
setValue(selected);
|
||||
}
|
||||
},
|
||||
[value, setValue, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='setting-radio-panel'>
|
||||
{items.map((item) => (
|
||||
<button
|
||||
className={classNames('setting-radio-panel__item', {
|
||||
'setting-radio-panel__item__active': value === item.value,
|
||||
})}
|
||||
key={item.value}
|
||||
onClick={handleChange}
|
||||
data-value={item.value}
|
||||
>
|
||||
{item.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <div className='alert'>{alertMessage}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersLink: React.FC<{
|
||||
id: string;
|
||||
isExclude: boolean;
|
||||
onCountFetched?: (count: number) => void;
|
||||
}> = ({ id, isExclude, onCountFetched }) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const [avatars, setAvatars] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const api = isExclude ? apiGetExcludeAccounts : apiGetAccounts;
|
||||
void api(id)
|
||||
.then((data) => {
|
||||
setCount(data.length);
|
||||
if (onCountFetched) {
|
||||
onCountFetched(data.length);
|
||||
}
|
||||
setAvatars(data.slice(0, 3).map((a) => a.avatar));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [id, setCount, setAvatars, isExclude, onCountFetched]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/antennas/${id}/${isExclude ? 'exclude_members' : 'members'}`}
|
||||
className='app-form__link'
|
||||
>
|
||||
<div className='app-form__link__text'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='antennas.antenna_accounts'
|
||||
defaultMessage='Antenna accounts'
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='antennas.antenna_accounts_count'
|
||||
defaultMessage='{count, plural, one {# member} other {# accounts}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='avatar-pile'>
|
||||
{avatars.map((url) => (
|
||||
<img key={url} src={url} alt='' />
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const AntennaSetting: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const intl = useIntl();
|
||||
//const history = useHistory();
|
||||
|
||||
const antenna = useAppSelector((state) =>
|
||||
id ? state.antennas.get(id) : undefined,
|
||||
);
|
||||
const [domainList, setDomainList] = useState([] as string[]);
|
||||
const [excludeDomainList, setExcludeDomainList] = useState([] as string[]);
|
||||
const [tagList, setTagList] = useState([] as string[]);
|
||||
const [excludeTagList, setExcludeTagList] = useState([] as string[]);
|
||||
const [keywordList, setKeywordList] = useState([] as string[]);
|
||||
const [excludeKeywordList, setExcludeKeywordList] = useState([] as string[]);
|
||||
const [accountsCount, setAccountsCount] = useState(0);
|
||||
const [rangeMode, setRangeMode] = useState('accounts');
|
||||
const [contentMode, setContentMode] = useState('keywords');
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
dispatch(fetchAntenna(id));
|
||||
|
||||
void apiGetDomains(id).then((data) => {
|
||||
setDomainList(data.domains);
|
||||
setExcludeDomainList(data.exclude_domains);
|
||||
return true;
|
||||
});
|
||||
|
||||
void apiGetTags(id).then((data) => {
|
||||
setTagList(data.tags);
|
||||
setExcludeTagList(data.exclude_tags);
|
||||
return true;
|
||||
});
|
||||
|
||||
void apiGetKeywords(id).then((data) => {
|
||||
setKeywordList(data.keywords);
|
||||
setExcludeKeywordList(data.exclude_keywords);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
id,
|
||||
setDomainList,
|
||||
setExcludeDomainList,
|
||||
setTagList,
|
||||
setExcludeTagList,
|
||||
setKeywordList,
|
||||
setExcludeKeywordList,
|
||||
]);
|
||||
|
||||
const handleAccountsFetched = useCallback(
|
||||
(count: number) => {
|
||||
setAccountsCount(count);
|
||||
},
|
||||
[setAccountsCount],
|
||||
);
|
||||
|
||||
const handleAddDomain = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddDomain(id, value).then(() => {
|
||||
setDomainList([...domainList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, domainList, setDomainList],
|
||||
);
|
||||
|
||||
const handleRemoveDomain = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveDomain(id, value).then(() => {
|
||||
setDomainList(domainList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, domainList, setDomainList],
|
||||
);
|
||||
|
||||
const handleAddExcludeDomain = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddExcludeDomain(id, value).then(() => {
|
||||
setExcludeDomainList([...excludeDomainList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeDomainList, setExcludeDomainList],
|
||||
);
|
||||
|
||||
const handleRemoveExcludeDomain = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveExcludeDomain(id, value).then(() => {
|
||||
setExcludeDomainList(excludeDomainList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeDomainList, setExcludeDomainList],
|
||||
);
|
||||
|
||||
const handleAddTag = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddTag(id, value).then(() => {
|
||||
setTagList([...tagList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, tagList, setTagList],
|
||||
);
|
||||
|
||||
const handleRemoveTag = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveTag(id, value).then(() => {
|
||||
setTagList(tagList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, tagList, setTagList],
|
||||
);
|
||||
|
||||
const handleAddExcludeTag = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddExcludeTag(id, value).then(() => {
|
||||
setExcludeTagList([...excludeTagList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeTagList, setExcludeTagList],
|
||||
);
|
||||
|
||||
const handleRemoveExcludeTag = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveExcludeTag(id, value).then(() => {
|
||||
setExcludeTagList(excludeTagList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeTagList, setExcludeTagList],
|
||||
);
|
||||
|
||||
const handleAddKeyword = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddKeyword(id, value).then(() => {
|
||||
setKeywordList([...keywordList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, keywordList, setKeywordList],
|
||||
);
|
||||
|
||||
const handleRemoveKeyword = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveKeyword(id, value).then(() => {
|
||||
setKeywordList(keywordList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, keywordList, setKeywordList],
|
||||
);
|
||||
|
||||
const handleAddExcludeKeyword = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiAddExcludeKeyword(id, value).then(() => {
|
||||
setExcludeKeywordList([...excludeKeywordList, value]);
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeKeywordList, setExcludeKeywordList],
|
||||
);
|
||||
|
||||
const handleRemoveExcludeKeyword = useCallback(
|
||||
(value: string) => {
|
||||
if (!id) return;
|
||||
|
||||
void apiRemoveExcludeKeyword(id, value).then(() => {
|
||||
setExcludeKeywordList(excludeKeywordList.filter((v) => v !== value));
|
||||
return value;
|
||||
});
|
||||
},
|
||||
[id, excludeKeywordList, setExcludeKeywordList],
|
||||
);
|
||||
|
||||
const handleRangeRadioChange = useCallback(
|
||||
(value: string) => {
|
||||
setRangeMode(value);
|
||||
},
|
||||
[setRangeMode],
|
||||
);
|
||||
|
||||
const handleContentRadioChange = useCallback(
|
||||
(value: string) => {
|
||||
setContentMode(value);
|
||||
},
|
||||
[setContentMode],
|
||||
);
|
||||
|
||||
if (!antenna || !id) return <div />;
|
||||
|
||||
if (antenna.stl)
|
||||
return (
|
||||
<div className='antenna-setting'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='antennas.in_stl_mode'
|
||||
defaultMessage='This antenna is in STL mode.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (antenna.ltl)
|
||||
return (
|
||||
<div className='antenna-setting'>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='antennas.in_ltl_mode'
|
||||
defaultMessage='This antenna is in LTL mode.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const rangeRadioItems = [
|
||||
{
|
||||
value: 'accounts',
|
||||
title: intl.formatMessage(messages.accounts, { count: accountsCount }),
|
||||
},
|
||||
{
|
||||
value: 'domains',
|
||||
title: intl.formatMessage(messages.domains, { count: domainList.length }),
|
||||
},
|
||||
];
|
||||
const rangeRadioLengths = [accountsCount, domainList.length];
|
||||
|
||||
const contentRadioItems = [
|
||||
{
|
||||
value: 'keywords',
|
||||
title: intl.formatMessage(messages.keywords, {
|
||||
count: keywordList.length,
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'tags',
|
||||
title: intl.formatMessage(messages.tags, { count: tagList.length }),
|
||||
},
|
||||
];
|
||||
const contentRadioLengths = [keywordList.length, tagList.length];
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={antenna.title}>
|
||||
<ColumnHeader
|
||||
title={antenna.title}
|
||||
icon='antenna-ul'
|
||||
iconComponent={AntennaIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable antenna-setting'>
|
||||
<RadioPanel
|
||||
items={rangeRadioItems}
|
||||
valueLengths={rangeRadioLengths}
|
||||
alertMessage={
|
||||
<div className='alert'>
|
||||
<FormattedMessage
|
||||
id='antennas.warnings.range_radio'
|
||||
defaultMessage='Simultaneous account and domain designation is not recommended.'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onChange={handleRangeRadioChange}
|
||||
/>
|
||||
{rangeMode === 'accounts' && (
|
||||
<MembersLink
|
||||
id={id}
|
||||
onCountFetched={handleAccountsFetched}
|
||||
isExclude={false}
|
||||
/>
|
||||
)}
|
||||
{rangeMode === 'domains' && (
|
||||
<TextList
|
||||
values={domainList}
|
||||
icon='sitemap'
|
||||
iconComponent={DomainIcon}
|
||||
label={intl.formatMessage(messages.addDomainLabel)}
|
||||
title={intl.formatMessage(messages.addDomainTitle)}
|
||||
onAdd={handleAddDomain}
|
||||
onRemove={handleRemoveDomain}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RadioPanel
|
||||
items={contentRadioItems}
|
||||
valueLengths={contentRadioLengths}
|
||||
alertMessage={
|
||||
<div className='alert'>
|
||||
<FormattedMessage
|
||||
id='antennas.warnings.content_radio'
|
||||
defaultMessage='Simultaneous keyword and tag designation is not recommended.'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onChange={handleContentRadioChange}
|
||||
/>
|
||||
{contentMode === 'keywords' && (
|
||||
<TextList
|
||||
values={keywordList}
|
||||
icon='paragraph'
|
||||
iconComponent={KeywordIcon}
|
||||
label={intl.formatMessage(messages.addKeywordLabel)}
|
||||
title={intl.formatMessage(messages.addKeywordTitle)}
|
||||
onAdd={handleAddKeyword}
|
||||
onRemove={handleRemoveKeyword}
|
||||
/>
|
||||
)}
|
||||
{contentMode === 'tags' && (
|
||||
<TextList
|
||||
values={tagList}
|
||||
icon='hashtag'
|
||||
iconComponent={HashtagIcon}
|
||||
label={intl.formatMessage(messages.addTagLabel)}
|
||||
title={intl.formatMessage(messages.addTagTitle)}
|
||||
onAdd={handleAddTag}
|
||||
onRemove={handleRemoveTag}
|
||||
/>
|
||||
)}
|
||||
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id='antennas.filter_not'
|
||||
defaultMessage='Filter Not'
|
||||
/>
|
||||
</h2>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='antennas.exclude_accounts'
|
||||
defaultMessage='Exclude accounts'
|
||||
/>
|
||||
</h3>
|
||||
<MembersLink id={id} isExclude />
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='antennas.exclude_domains'
|
||||
defaultMessage='Exclude domains'
|
||||
/>
|
||||
</h3>
|
||||
<TextList
|
||||
values={excludeDomainList}
|
||||
icon='sitemap'
|
||||
iconComponent={DomainIcon}
|
||||
label={intl.formatMessage(messages.addDomainLabel)}
|
||||
title={intl.formatMessage(messages.addDomainTitle)}
|
||||
onAdd={handleAddExcludeDomain}
|
||||
onRemove={handleRemoveExcludeDomain}
|
||||
/>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='antennas.exclude_keywords'
|
||||
defaultMessage='Exclude keywords'
|
||||
/>
|
||||
</h3>
|
||||
<TextList
|
||||
values={excludeKeywordList}
|
||||
icon='paragraph'
|
||||
iconComponent={KeywordIcon}
|
||||
label={intl.formatMessage(messages.addKeywordLabel)}
|
||||
title={intl.formatMessage(messages.addKeywordTitle)}
|
||||
onAdd={handleAddExcludeKeyword}
|
||||
onRemove={handleRemoveExcludeKeyword}
|
||||
/>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='antennas.exclude_tags'
|
||||
defaultMessage='Exclude tags'
|
||||
/>
|
||||
</h3>
|
||||
<TextList
|
||||
values={excludeTagList}
|
||||
icon='paragraph'
|
||||
iconComponent={HashtagIcon}
|
||||
label={intl.formatMessage(messages.addTagLabel)}
|
||||
title={intl.formatMessage(messages.addTagTitle)}
|
||||
onAdd={handleAddExcludeTag}
|
||||
onRemove={handleRemoveExcludeTag}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AntennaSetting;
|
|
@ -1,97 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import { fetchAntennas } from 'mastodon/actions/antennas';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
|
||||
|
||||
import NewAntennaForm from './components/new_antenna_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.antennas', defaultMessage: 'Antennas' },
|
||||
subheading: { id: 'antennas.subheading', defaultMessage: 'Your antennas' },
|
||||
insert_list: { id: 'antennas.insert_list', defaultMessage: 'List' },
|
||||
insert_home: { id: 'antennas.insert_home', defaultMessage: 'Home' },
|
||||
});
|
||||
|
||||
const getOrderedAntennas = createSelector([state => state.get('antennas')], antennas => {
|
||||
if (!antennas) {
|
||||
return antennas;
|
||||
}
|
||||
|
||||
return antennas.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
antennas: getOrderedAntennas(state),
|
||||
});
|
||||
|
||||
class Antennas extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
antennas: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchAntennas());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, antennas, multiColumn } = this.props;
|
||||
|
||||
if (!antennas) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.antennas' defaultMessage="You don't have any antennas yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='wifi' iconComponent={AntennaIcon} multiColumn={multiColumn} />
|
||||
|
||||
<NewAntennaForm />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='antennas'
|
||||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{antennas.map(antenna => (
|
||||
<ColumnLink key={antenna.get('id')} to={`/antennast/${antenna.get('id')}`} icon='wifi' iconComponent={AntennaIcon} text={antenna.get('title')}
|
||||
badge={antenna.get('insert_feeds') ? intl.formatMessage(antenna.get('list') ? messages.insert_list : messages.insert_home) : undefined} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Antennas));
|
190
app/javascript/mastodon/features/antennas/index.tsx
Normal file
190
app/javascript/mastodon/features/antennas/index.tsx
Normal file
|
@ -0,0 +1,190 @@
|
|||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchAntennas } from 'mastodon/actions/antennas';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { getOrderedAntennas } from 'mastodon/selectors/antennas';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.antennas', defaultMessage: 'Antennas' },
|
||||
create: { id: 'antennas.create_antenna', defaultMessage: 'Create antenna' },
|
||||
edit: { id: 'antennas.edit', defaultMessage: 'Edit antenna' },
|
||||
delete: { id: 'antennas.delete', defaultMessage: 'Delete antenna' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const AntennaItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
insert_feeds: boolean;
|
||||
isList: boolean;
|
||||
listTitle?: string;
|
||||
stl: boolean;
|
||||
ltl: boolean;
|
||||
}> = ({ id, title, insert_feeds, isList, listTitle, stl, ltl }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_ANTENNA',
|
||||
modalProps: {
|
||||
antennaId: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{ text: intl.formatMessage(messages.edit), to: `/antennas/${id}/edit` },
|
||||
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='lists__item'>
|
||||
<Link to={`/antennas/${id}`} className='lists__item__title'>
|
||||
<Icon id='antenna-ul' icon={AntennaIcon} />
|
||||
<span>
|
||||
{title}
|
||||
|
||||
{stl && (
|
||||
<span className='column-link__badge'>
|
||||
<FormattedMessage id='antennas.badge_stl' defaultMessage='STL' />
|
||||
</span>
|
||||
)}
|
||||
{ltl && (
|
||||
<span className='column-link__badge'>
|
||||
<FormattedMessage id='antennas.badge_ltl' defaultMessage='LTL' />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{insert_feeds && (
|
||||
<span className='lists__item__memo'>
|
||||
{isList && listTitle && (
|
||||
<FormattedMessage
|
||||
id='antennas.memo_insert_list'
|
||||
defaultMessage='List: "{title}"'
|
||||
values={{ title: listTitle }}
|
||||
/>
|
||||
)}
|
||||
{!isList && (
|
||||
<FormattedMessage
|
||||
id='antennas.memo_insert_home'
|
||||
defaultMessage='Inserts home timeline.'
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuContainer
|
||||
scrollKey='antennas'
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Antennas: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const antennas = useAppSelector((state) => getOrderedAntennas(state));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchAntennas());
|
||||
}, [dispatch]);
|
||||
|
||||
const emptyMessage = (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='antennas.no_antennas_yet'
|
||||
defaultMessage='No antennas yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='antennas.create_a_antenna_to_organize'
|
||||
defaultMessage='Create a new antenna to organize your Home feed'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='antenna-ul'
|
||||
iconComponent={AntennaIcon}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<Link
|
||||
to='/antennas/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.create)}
|
||||
aria-label={intl.formatMessage(messages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='antennas'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{antennas.map((antenna) => (
|
||||
<AntennaItem
|
||||
key={antenna.id}
|
||||
id={antenna.id}
|
||||
title={antenna.title}
|
||||
insert_feeds={antenna.insert_feeds}
|
||||
isList={!!antenna.list}
|
||||
listTitle={antenna.list?.title}
|
||||
stl={antenna.stl}
|
||||
ltl={antenna.ltl}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Antennas;
|
392
app/javascript/mastodon/features/antennas/members.tsx
Normal file
392
app/javascript/mastodon/features/antennas/members.tsx
Normal file
|
@ -0,0 +1,392 @@
|
|||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchFollowing } from 'mastodon/actions/accounts';
|
||||
import { fetchAntenna } from 'mastodon/actions/antennas';
|
||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||
import { apiRequest } from 'mastodon/api';
|
||||
import {
|
||||
apiGetAccounts,
|
||||
apiAddAccountToAntenna,
|
||||
apiRemoveAccountFromAntenna,
|
||||
apiRemoveExcludeAccountFromAntenna,
|
||||
apiAddExcludeAccountToAntenna,
|
||||
apiGetExcludeAccounts,
|
||||
} from 'mastodon/api/antennas';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { FollowersCounter } from 'mastodon/components/counters';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'column.antenna_members',
|
||||
defaultMessage: 'Manage antenna members',
|
||||
},
|
||||
placeholder: {
|
||||
id: 'antennas.search_placeholder',
|
||||
defaultMessage: 'Search people you follow',
|
||||
},
|
||||
enterSearch: {
|
||||
id: 'antennas.add_to_antenna',
|
||||
defaultMessage: 'Add to antenna',
|
||||
},
|
||||
add: { id: 'antennas.add_member', defaultMessage: 'Add' },
|
||||
remove: { id: 'antennas.remove_member', defaultMessage: 'Remove' },
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
type Mode = 'remove' | 'add';
|
||||
|
||||
const ColumnSearchHeader: React.FC<{
|
||||
onBack: () => void;
|
||||
onSubmit: (value: string) => void;
|
||||
}> = ({ onBack, onSubmit }) => {
|
||||
const intl = useIntl();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(value);
|
||||
onSubmit(value);
|
||||
},
|
||||
[setValue, onSubmit],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(value);
|
||||
}, [onSubmit, value]);
|
||||
|
||||
return (
|
||||
<ButtonInTabsBar>
|
||||
<form className='column-search-header' onSubmit={handleSubmit}>
|
||||
<button
|
||||
type='button'
|
||||
className='column-header__back-button compact'
|
||||
onClick={onBack}
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type='search'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</ButtonInTabsBar>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountItem: React.FC<{
|
||||
accountId: string;
|
||||
antennaId: string;
|
||||
partOfAntenna: boolean;
|
||||
isExclude?: boolean;
|
||||
onToggle: (accountId: string) => void;
|
||||
}> = ({ accountId, antennaId, partOfAntenna, isExclude, onToggle }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (partOfAntenna) {
|
||||
const api = isExclude
|
||||
? apiRemoveExcludeAccountFromAntenna
|
||||
: apiRemoveAccountFromAntenna;
|
||||
void api(antennaId, accountId);
|
||||
} else {
|
||||
const api = isExclude
|
||||
? apiAddExcludeAccountToAntenna
|
||||
: apiAddAccountToAntenna;
|
||||
void api(antennaId, accountId);
|
||||
}
|
||||
|
||||
onToggle(accountId);
|
||||
}, [accountId, antennaId, partOfAntenna, onToggle, isExclude]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link
|
||||
key={account.id}
|
||||
className='account__display-name'
|
||||
title={account.acct}
|
||||
to={`/@${account.acct}`}
|
||||
data-hover-card-account={account.id}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
|
||||
<div className='account__details'>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
isHide={account.other_settings.hide_followers_count}
|
||||
/>{' '}
|
||||
{firstVerifiedField && (
|
||||
<VerifiedBadge link={firstVerifiedField.value} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='account__relationship'>
|
||||
<Button
|
||||
text={intl.formatMessage(
|
||||
partOfAntenna ? messages.remove : messages.add,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AntennaMembers: React.FC<{
|
||||
isExclude?: boolean;
|
||||
multiColumn?: boolean;
|
||||
}> = ({ isExclude, multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const intl = useIntl();
|
||||
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mode, setMode] = useState<Mode>('remove');
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
dispatch(fetchAntenna(id));
|
||||
|
||||
const api = isExclude ? apiGetExcludeAccounts : apiGetAccounts;
|
||||
void api(id)
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
dispatch(fetchFollowing(me));
|
||||
}
|
||||
}, [dispatch, id, isExclude]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
setMode('add');
|
||||
}, [setMode]);
|
||||
|
||||
const handleDismissSearchClick = useCallback(() => {
|
||||
setMode('remove');
|
||||
setSearching(false);
|
||||
}, [setMode]);
|
||||
|
||||
const handleAccountToggle = useCallback(
|
||||
(accountId: string) => {
|
||||
const partOfAntenna = accountIds.includes(accountId);
|
||||
|
||||
if (partOfAntenna) {
|
||||
setAccountIds(accountIds.filter((id) => id !== accountId));
|
||||
} else {
|
||||
setAccountIds([accountId, ...accountIds]);
|
||||
}
|
||||
},
|
||||
[accountIds, setAccountIds],
|
||||
);
|
||||
|
||||
const searchRequestRef = useRef<AbortController | null>(null);
|
||||
|
||||
const handleSearch = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
if (searchRequestRef.current) {
|
||||
searchRequestRef.current.abort();
|
||||
}
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
searchRequestRef.current = new AbortController();
|
||||
|
||||
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
||||
signal: searchRequestRef.current.signal,
|
||||
params: {
|
||||
q: value,
|
||||
resolve: false,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setSearchAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
setSearching(true);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setSearching(true);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
let displayedAccountIds: string[];
|
||||
|
||||
if (mode === 'add') {
|
||||
displayedAccountIds = searching ? searchAccountIds : accountIds;
|
||||
} else {
|
||||
displayedAccountIds = accountIds;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
{mode === 'remove' ? (
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='antenna-ul'
|
||||
iconComponent={AntennaIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<button
|
||||
onClick={handleSearchClick}
|
||||
type='button'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.enterSearch)}
|
||||
aria-label={intl.formatMessage(messages.enterSearch)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ColumnSearchHeader
|
||||
onBack={handleDismissSearchClick}
|
||||
onSubmit={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='antenna_members'
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
isLoading={loading}
|
||||
showLoading={loading && displayedAccountIds.length === 0}
|
||||
hasMore={false}
|
||||
footer={
|
||||
mode === 'remove' && (
|
||||
<>
|
||||
{displayedAccountIds.length > 0 && <div className='spacer' />}
|
||||
|
||||
<div className='column-footer'>
|
||||
<Link
|
||||
to={`/antennas/${id}/filtering`}
|
||||
className='button button--block'
|
||||
>
|
||||
<FormattedMessage id='antennas.done' defaultMessage='Done' />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
emptyMessage={
|
||||
mode === 'remove' ? (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='antennas.no_members_yet'
|
||||
defaultMessage='No members yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='antennas.find_users_to_add'
|
||||
defaultMessage='Find users to add'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='antennas.no_results_found'
|
||||
defaultMessage='No results found.'
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayedAccountIds.map((accountId) => (
|
||||
<AccountItem
|
||||
key={accountId}
|
||||
accountId={accountId}
|
||||
antennaId={id}
|
||||
partOfAntenna={
|
||||
displayedAccountIds === accountIds ||
|
||||
accountIds.includes(accountId)
|
||||
}
|
||||
isExclude={isExclude}
|
||||
onToggle={handleAccountToggle}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default AntennaMembers;
|
491
app/javascript/mastodon/features/antennas/new.tsx
Normal file
491
app/javascript/mastodon/features/antennas/new.tsx
Normal file
|
@ -0,0 +1,491 @@
|
|||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, useHistory, Link } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import AntennaIcon from '@/material-icons/400-24px/wifi.svg?react';
|
||||
import { fetchAntenna } from 'mastodon/actions/antennas';
|
||||
import { createAntenna, updateAntenna } from 'mastodon/actions/antennas_typed';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: { id: 'column.edit_antenna', defaultMessage: 'Edit antenna' },
|
||||
create: { id: 'column.create_antenna', defaultMessage: 'Create antenna' },
|
||||
});
|
||||
|
||||
const FiltersLink: React.FC<{
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
return (
|
||||
<Link to={`/antennas/${id}/filtering`} className='app-form__link'>
|
||||
<div className='app-form__link__text'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='antennas.filter_items'
|
||||
defaultMessage='Move to antenna filter setting'
|
||||
/>
|
||||
</strong>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NewAntenna: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const antenna = useAppSelector((state) =>
|
||||
id ? state.antennas.get(id) : undefined,
|
||||
);
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
const [title, setTitle] = useState('');
|
||||
const [stl, setStl] = useState(false);
|
||||
const [ltl, setLtl] = useState(false);
|
||||
const [insertFeeds, setInsertFeeds] = useState(false);
|
||||
const [listId, setListId] = useState('');
|
||||
const [withMediaOnly, setWithMediaOnly] = useState(false);
|
||||
const [ignoreReblog, setIgnoreReblog] = useState(false);
|
||||
const [mode, setMode] = useState('filtering');
|
||||
const [destination, setDestination] = useState('timeline');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
dispatch(fetchAntenna(id));
|
||||
dispatch(fetchLists());
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && antenna) {
|
||||
setTitle(antenna.title);
|
||||
setStl(antenna.stl);
|
||||
setLtl(antenna.ltl);
|
||||
setInsertFeeds(antenna.insert_feeds);
|
||||
setListId(antenna.list?.id ?? '0');
|
||||
setWithMediaOnly(antenna.with_media_only);
|
||||
setIgnoreReblog(antenna.ignore_reblog);
|
||||
|
||||
if (antenna.stl) {
|
||||
setMode('stl');
|
||||
} else if (antenna.ltl) {
|
||||
setMode('ltl');
|
||||
} else {
|
||||
setMode('filtering');
|
||||
}
|
||||
|
||||
if (antenna.insert_feeds) {
|
||||
if (antenna.list) {
|
||||
setDestination('list');
|
||||
} else {
|
||||
setDestination('home');
|
||||
}
|
||||
} else {
|
||||
setDestination('timeline');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
setTitle,
|
||||
setStl,
|
||||
setLtl,
|
||||
setInsertFeeds,
|
||||
setListId,
|
||||
setWithMediaOnly,
|
||||
setIgnoreReblog,
|
||||
setMode,
|
||||
setDestination,
|
||||
id,
|
||||
antenna,
|
||||
lists,
|
||||
]);
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleListIdChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setListId(value);
|
||||
},
|
||||
[setListId],
|
||||
);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (value === 'stl') {
|
||||
setStl(true);
|
||||
setLtl(false);
|
||||
} else if (value === 'ltl') {
|
||||
setStl(false);
|
||||
setLtl(true);
|
||||
} else if (value === 'filtering') {
|
||||
setStl(false);
|
||||
setLtl(false);
|
||||
}
|
||||
|
||||
setMode(value);
|
||||
},
|
||||
[setLtl, setStl, setMode],
|
||||
);
|
||||
|
||||
const handleDestinationChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
if (value === 'list') {
|
||||
setInsertFeeds(true);
|
||||
if (listId === '0' && lists.length > 0) {
|
||||
setListId(lists[0]?.id ?? '0');
|
||||
}
|
||||
} else if (value === 'home') {
|
||||
setInsertFeeds(true);
|
||||
// listId = 0
|
||||
} else if (value === 'timeline') {
|
||||
setInsertFeeds(false);
|
||||
}
|
||||
|
||||
setDestination(value);
|
||||
},
|
||||
[setDestination, setListId, listId, lists],
|
||||
);
|
||||
|
||||
const handleWithMediaOnlyChange = useCallback(
|
||||
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setWithMediaOnly(checked);
|
||||
},
|
||||
[setWithMediaOnly],
|
||||
);
|
||||
|
||||
const handleIgnoreReblogChange = useCallback(
|
||||
({ target: { checked } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIgnoreReblog(checked);
|
||||
},
|
||||
[setIgnoreReblog],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (id) {
|
||||
void dispatch(
|
||||
updateAntenna({
|
||||
id,
|
||||
title,
|
||||
stl,
|
||||
ltl,
|
||||
insert_feeds: insertFeeds,
|
||||
list_id: destination === 'list' ? listId : '0',
|
||||
with_media_only: withMediaOnly,
|
||||
ignore_reblog: ignoreReblog,
|
||||
}),
|
||||
).then(() => {
|
||||
setSubmitting(false);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(
|
||||
createAntenna({
|
||||
title,
|
||||
stl,
|
||||
ltl,
|
||||
insert_feeds: insertFeeds,
|
||||
list_id: destination === 'list' ? listId : '0',
|
||||
with_media_only: withMediaOnly,
|
||||
ignore_reblog: ignoreReblog,
|
||||
}),
|
||||
).then((result) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(`/antennas/${result.payload.id}/edit`);
|
||||
history.push(`/antennas/${result.payload.id}/members`);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [
|
||||
history,
|
||||
dispatch,
|
||||
setSubmitting,
|
||||
id,
|
||||
title,
|
||||
stl,
|
||||
ltl,
|
||||
insertFeeds,
|
||||
listId,
|
||||
withMediaOnly,
|
||||
ignoreReblog,
|
||||
destination,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
icon='antenna-ul'
|
||||
iconComponent={AntennaIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='antenna_title'>
|
||||
<FormattedMessage
|
||||
id='antennas.antenna_name'
|
||||
defaultMessage='Antenna name'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<input
|
||||
id='antenna_title'
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder=' '
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='antenna_list'>
|
||||
<FormattedMessage id='antennas.mode' defaultMessage='Mode' />
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<select
|
||||
id='antenna_insert_list'
|
||||
value={mode}
|
||||
onChange={handleModeChange}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='antennas.mode.stl'
|
||||
defaultMessage='Social timeline mode'
|
||||
>
|
||||
{(msg) => <option value='stl'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='antennas.mode.ltl'
|
||||
defaultMessage='Local timeline mode'
|
||||
>
|
||||
{(msg) => <option value='ltl'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='antennas.mode.filtering'
|
||||
defaultMessage='Filtering'
|
||||
>
|
||||
{(msg) => <option value='filtering'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='antenna_list'>
|
||||
<FormattedMessage
|
||||
id='antennas.destination'
|
||||
defaultMessage='Destination'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<select
|
||||
id='antenna_insert_destination'
|
||||
value={destination}
|
||||
onChange={handleDestinationChange}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='antennas.destination.home'
|
||||
defaultMessage='Insert to home'
|
||||
>
|
||||
{(msg) => <option value='home'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='antennas.destination.list'
|
||||
defaultMessage='Insert to list'
|
||||
>
|
||||
{(msg) => <option value='list'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
<FormattedMessage
|
||||
id='antennas.destination.timeline'
|
||||
defaultMessage='Antenna timeline only'
|
||||
>
|
||||
{(msg) => <option value='timeline'>{msg}</option>}
|
||||
</FormattedMessage>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{destination === 'list' && (
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='antenna_list'>
|
||||
<FormattedMessage
|
||||
id='antennas.list_selection'
|
||||
defaultMessage='List to insert'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<select
|
||||
id='antenna_insert_list'
|
||||
value={listId}
|
||||
onChange={handleListIdChange}
|
||||
>
|
||||
{lists.map((list) => (
|
||||
<option key={list.id} value={list.id}>
|
||||
{list.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{id && mode === 'filtering' && (
|
||||
<div className='fields-group'>
|
||||
<FiltersLink id={id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!id && mode === 'filtering' && (
|
||||
<div className='fields-group'>
|
||||
<div className='app-form__memo'>
|
||||
<FormattedMessage
|
||||
id='antennas.save_to_edit_filtering'
|
||||
defaultMessage='You can edit the filtering after saving.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'filtering' && (
|
||||
<>
|
||||
<div className='fields-group'>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='antennas.media_only'
|
||||
defaultMessage='Media only'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
<FormattedMessage
|
||||
id='antennas.media_only_hint'
|
||||
defaultMessage='Only posts with media will be added antenna.'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Toggle
|
||||
checked={withMediaOnly}
|
||||
onChange={handleWithMediaOnlyChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='fields-group'>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label className='app-form__toggle'>
|
||||
<div className='app-form__toggle__label'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='antennas.ignore_reblog'
|
||||
defaultMessage='Exclude boosts'
|
||||
/>
|
||||
</strong>
|
||||
<span className='hint'>
|
||||
<FormattedMessage
|
||||
id='antennas.ignore_reblog_hint'
|
||||
defaultMessage='Boosts will be excluded from antenna detection.'
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='app-form__toggle__toggle'>
|
||||
<div>
|
||||
<Toggle
|
||||
checked={ignoreReblog}
|
||||
onChange={handleIgnoreReblogChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='actions'>
|
||||
<button className='button' type='submit'>
|
||||
{submitting ? (
|
||||
<LoadingIndicator />
|
||||
) : id ? (
|
||||
<FormattedMessage id='antennas.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='antennas.create'
|
||||
defaultMessage='Create'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default NewAntenna;
|
|
@ -1,80 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeBookmarkCategoryEditorTitle, submitBookmarkCategoryEditor } from 'mastodon/actions/bookmark_categories';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'bookmark_categories.new.title_placeholder', defaultMessage: 'New category title' },
|
||||
title: { id: 'bookmark_categories.new.create', defaultMessage: 'Add category' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['bookmarkCategoryEditor', 'title']),
|
||||
disabled: state.getIn(['bookmarkCategoryEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeBookmarkCategoryEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitBookmarkCategoryEditor(true)),
|
||||
});
|
||||
|
||||
class NewBookmarkCategoryForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewBookmarkCategoryForm));
|
|
@ -1,98 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
|
||||
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
|
||||
import { fetchBookmarkCategories } from 'mastodon/actions/bookmark_categories';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
|
||||
|
||||
import NewListForm from './components/new_bookmark_category_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmark_categories', defaultMessage: 'Bookmark categories' },
|
||||
subheading: { id: 'bookmark_categories.subheading', defaultMessage: 'Your categories' },
|
||||
allBookmarks: { id: 'bookmark_categories.all_bookmarks', defaultMessage: 'All bookmarks' },
|
||||
});
|
||||
|
||||
const getOrderedCategories = createSelector([state => state.get('bookmark_categories')], categories => {
|
||||
if (!categories) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
return categories.toList().filter(item => !!item && typeof item.get('title') !== 'undefined' && item.get('title') !== null).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
categories: getOrderedCategories(state),
|
||||
});
|
||||
|
||||
class BookmarkCategories extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
categories: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchBookmarkCategories());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, categories, multiColumn } = this.props;
|
||||
|
||||
if (!categories) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.bookmark_categories' defaultMessage="You don't have any categories yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='bookmark' iconComponent={BookmarksIcon} multiColumn={multiColumn} />
|
||||
|
||||
<NewListForm />
|
||||
|
||||
<ColumnLink to='/bookmarks' icon='bookmark' iconComponent={BookmarkIcon} text={intl.formatMessage(messages.allBookmarks)} />
|
||||
<ScrollableList
|
||||
scrollKey='bookmark_categories'
|
||||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{categories.map(category =>
|
||||
<ColumnLink key={category.get('id')} to={`/bookmark_categories/${category.get('id')}`} icon='bookmark' iconComponent={BookmarkIcon} text={category.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(BookmarkCategories));
|
181
app/javascript/mastodon/features/bookmark_categories/index.tsx
Normal file
181
app/javascript/mastodon/features/bookmark_categories/index.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||
import BookmarksIcon from '@/material-icons/400-24px/bookmarks.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchBookmarkCategories } from 'mastodon/actions/bookmark_categories';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { getOrderedBookmarkCategories } from 'mastodon/selectors/bookmark_categories';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'column.bookmark_categories',
|
||||
defaultMessage: 'BookmarkCategories',
|
||||
},
|
||||
create: {
|
||||
id: 'bookmark_categories.create_bookmark_category',
|
||||
defaultMessage: 'Create category',
|
||||
},
|
||||
edit: {
|
||||
id: 'bookmark_categories.edit',
|
||||
defaultMessage: 'Edit category',
|
||||
},
|
||||
delete: {
|
||||
id: 'bookmark_categories.delete',
|
||||
defaultMessage: 'Delete category',
|
||||
},
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const BookmarkCategoryItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_BOOKMARK_CATEGORY',
|
||||
modalProps: {
|
||||
bookmark_categoryId: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{
|
||||
text: intl.formatMessage(messages.edit),
|
||||
to: `/bookmark_categories/${id}/edit`,
|
||||
},
|
||||
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='lists__item'>
|
||||
<Link to={`/bookmark_categories/${id}`} className='lists__item__title'>
|
||||
<Icon id='bookmark_category-ul' icon={BookmarkIcon} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuContainer
|
||||
scrollKey='bookmark_categories'
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const BookmarkCategories: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const bookmark_categories = useAppSelector((state) =>
|
||||
getOrderedBookmarkCategories(state),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBookmarkCategories());
|
||||
}, [dispatch]);
|
||||
|
||||
const emptyMessage = (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.no_bookmark_categories_yet'
|
||||
defaultMessage='No bookmark_categories yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.create_a_bookmark_category_to_organize'
|
||||
defaultMessage='Create a new bookmark_category to organize your Home feed'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='bookmark_category-ul'
|
||||
iconComponent={BookmarkIcon}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<Link
|
||||
to='/bookmark_categories/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.create)}
|
||||
aria-label={intl.formatMessage(messages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='bookmark_categories'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
alwaysPrepend
|
||||
prepend={
|
||||
<div className='lists__item'>
|
||||
<Link to={'/bookmarks'} className='lists__item__title'>
|
||||
<Icon id='bookmarks' icon={BookmarksIcon} />
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.all_bookmarks'
|
||||
defaultMessage='All bookmarks'
|
||||
/>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{bookmark_categories.map((bookmark_category) => (
|
||||
<BookmarkCategoryItem
|
||||
key={bookmark_category.id}
|
||||
id={bookmark_category.id}
|
||||
title={bookmark_category.title}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default BookmarkCategories;
|
167
app/javascript/mastodon/features/bookmark_categories/new.tsx
Normal file
167
app/javascript/mastodon/features/bookmark_categories/new.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||
import { fetchBookmarkCategory } from 'mastodon/actions/bookmark_categories';
|
||||
import {
|
||||
createBookmarkCategory,
|
||||
updateBookmarkCategory,
|
||||
} from 'mastodon/actions/bookmark_categories_typed';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: {
|
||||
id: 'column.edit_bookmark_category',
|
||||
defaultMessage: 'Edit bookmark_category',
|
||||
},
|
||||
create: {
|
||||
id: 'column.create_bookmark_category',
|
||||
defaultMessage: 'Create bookmark_category',
|
||||
},
|
||||
});
|
||||
|
||||
const NewBookmarkCategory: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const bookmark_category = useAppSelector((state) =>
|
||||
id ? state.bookmark_categories.get(id) : undefined,
|
||||
);
|
||||
const [title, setTitle] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
dispatch(fetchBookmarkCategory(id));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && bookmark_category) {
|
||||
setTitle(bookmark_category.title);
|
||||
}
|
||||
}, [setTitle, id, bookmark_category]);
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (id) {
|
||||
void dispatch(
|
||||
updateBookmarkCategory({
|
||||
id,
|
||||
title,
|
||||
}),
|
||||
).then(() => {
|
||||
setSubmitting(false);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(
|
||||
createBookmarkCategory({
|
||||
title,
|
||||
}),
|
||||
).then((result) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(`/bookmark_categories/${result.payload.id}/edit`);
|
||||
history.push(`/bookmark_categories/${result.payload.id}/members`);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [history, dispatch, setSubmitting, id, title]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
icon='bookmark_category-ul'
|
||||
iconComponent={BookmarkIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='bookmark_category_title'>
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.bookmark_category_name'
|
||||
defaultMessage='BookmarkCategory name'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<input
|
||||
id='bookmark_category_title'
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder=' '
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='actions'>
|
||||
<button className='button' type='submit'>
|
||||
{submitting ? (
|
||||
<LoadingIndicator />
|
||||
) : id ? (
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.create'
|
||||
defaultMessage='Create'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default NewBookmarkCategory;
|
|
@ -1,43 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromBookmarkCategoryAdder, addToBookmarkCategoryAdder } from '../../../actions/bookmark_categories';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'bookmark_categories.status.remove', defaultMessage: 'Remove from bookmark category' },
|
||||
add: { id: 'bookmark_categories.status.add', defaultMessage: 'Add to bookmark category' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { bookmarkCategoryId, added }) => ({
|
||||
bookmarkCategory: state.get('bookmark_categories').get(bookmarkCategoryId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['bookmarkCategoryAdder', 'bookmarkCategories', 'items']).includes(bookmarkCategoryId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { bookmarkCategoryId }) => ({
|
||||
onRemove: () => dispatch(removeFromBookmarkCategoryAdder(bookmarkCategoryId)),
|
||||
onAdd: () => dispatch(addToBookmarkCategoryAdder(bookmarkCategoryId)),
|
||||
});
|
||||
|
||||
class BookmarkCategory extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
bookmarkCategory: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { bookmarkCategory, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='bookmark' icon={BookmarkIcon} className='column-link__icon' fixedWidth />
|
||||
{bookmarkCategory.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(BookmarkCategory));
|
|
@ -1,78 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import { setupBookmarkCategoryAdder, resetBookmarkCategoryAdder } from '../../actions/bookmark_categories';
|
||||
import NewBookmarkCategoryForm from '../bookmark_categories/components/new_bookmark_category_form';
|
||||
|
||||
// import Account from './components/account';
|
||||
import BookmarkCategory from './components/bookmark_category';
|
||||
|
||||
const getOrderedBookmarkCategories = createSelector([state => state.get('bookmark_categories')], bookmarkCategories => {
|
||||
if (!bookmarkCategories) {
|
||||
return bookmarkCategories;
|
||||
}
|
||||
|
||||
return bookmarkCategories.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
bookmarkCategoryIds: getOrderedBookmarkCategories(state).map(bookmarkCategory=>bookmarkCategory.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: statusId => dispatch(setupBookmarkCategoryAdder(statusId)),
|
||||
onReset: () => dispatch(resetBookmarkCategoryAdder()),
|
||||
});
|
||||
|
||||
class BookmarkCategoryAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
statusId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
bookmarkCategoryIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, statusId } = this.props;
|
||||
onInitialize(statusId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { bookmarkCategoryIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
{/*
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<NewBookmarkCategoryForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{bookmarkCategoryIds.map(BookmarkCategoryId => <BookmarkCategory key={BookmarkCategoryId} bookmarkCategoryId={BookmarkCategoryId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(BookmarkCategoryAdder));
|
|
@ -0,0 +1,268 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import {
|
||||
bookmarkCategoryEditorAddSuccess,
|
||||
bookmarkCategoryEditorRemoveSuccess,
|
||||
fetchBookmarkCategories,
|
||||
} from 'mastodon/actions/bookmark_categories';
|
||||
import { createBookmarkCategory } from 'mastodon/actions/bookmark_categories_typed';
|
||||
import { unbookmark } from 'mastodon/actions/interactions';
|
||||
import {
|
||||
apiGetStatusBookmarkCategories,
|
||||
apiAddStatusToBookmarkCategory,
|
||||
apiRemoveStatusFromBookmarkCategory,
|
||||
} from 'mastodon/api/bookmark_categories';
|
||||
import type { ApiBookmarkCategoryJSON } from 'mastodon/api_types/bookmark_categories';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { bookmarkCategoryNeeded } from 'mastodon/initial_state';
|
||||
import { getOrderedBookmarkCategories } from 'mastodon/selectors/bookmark_categories';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newBookmarkCategory: {
|
||||
id: 'bookmark_categories.new_bookmark_category_name',
|
||||
defaultMessage: 'New bookmark_category name',
|
||||
},
|
||||
createBookmarkCategory: {
|
||||
id: 'bookmark_categories.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const BookmarkCategoryItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='bookmark_category-ul' icon={BookmarkIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewBookmarkCategoryItem: React.FC<{
|
||||
onCreate: (bookmark_category: ApiBookmarkCategoryJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createBookmarkCategory({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='lists__item' onSubmit={handleSubmit}>
|
||||
<label className='lists__item__title'>
|
||||
<Icon id='bookmark_category-ul' icon={BookmarkIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newBookmarkCategory)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
text={intl.formatMessage(messages.createBookmarkCategory)}
|
||||
type='submit'
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const BookmarkCategoryAdder: React.FC<{
|
||||
statusId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ statusId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const bookmark_categories = useAppSelector((state) =>
|
||||
getOrderedBookmarkCategories(state),
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return
|
||||
const status = useAppSelector((state) => state.statuses.get(statusId));
|
||||
const [bookmark_categoryIds, setBookmarkCategoryIds] = useState<string[]>(
|
||||
[] as string[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBookmarkCategories());
|
||||
|
||||
apiGetStatusBookmarkCategories(statusId)
|
||||
.then((data) => {
|
||||
setBookmarkCategoryIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setBookmarkCategoryIds, statusId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(bookmark_categoryId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) => [
|
||||
bookmark_categoryId,
|
||||
...currentBookmarkCategoryIds,
|
||||
]);
|
||||
|
||||
apiAddStatusToBookmarkCategory(bookmark_categoryId, statusId)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
bookmarkCategoryEditorAddSuccess(bookmark_categoryId, statusId),
|
||||
);
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) =>
|
||||
currentBookmarkCategoryIds.filter(
|
||||
(id) => id !== bookmark_categoryId,
|
||||
),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) =>
|
||||
currentBookmarkCategoryIds.filter((id) => id !== bookmark_categoryId),
|
||||
);
|
||||
|
||||
apiRemoveStatusFromBookmarkCategory(bookmark_categoryId, statusId)
|
||||
.then(() => {
|
||||
dispatch(
|
||||
bookmarkCategoryEditorRemoveSuccess(
|
||||
bookmark_categoryId,
|
||||
statusId,
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
bookmarkCategoryNeeded &&
|
||||
bookmark_categoryIds.filter((id) => id !== bookmark_categoryId)
|
||||
.length === 0
|
||||
) {
|
||||
dispatch(unbookmark(status));
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(() => {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) => [
|
||||
bookmark_categoryId,
|
||||
...currentBookmarkCategoryIds,
|
||||
]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setBookmarkCategoryIds, statusId, dispatch, bookmark_categoryIds, status],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(bookmark_category: ApiBookmarkCategoryJSON) => {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) => [
|
||||
bookmark_category.id,
|
||||
...currentBookmarkCategoryIds,
|
||||
]);
|
||||
|
||||
apiAddStatusToBookmarkCategory(bookmark_category.id, statusId).catch(
|
||||
() => {
|
||||
setBookmarkCategoryIds((currentBookmarkCategoryIds) =>
|
||||
currentBookmarkCategoryIds.filter(
|
||||
(id) => id !== bookmark_category.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
[setBookmarkCategoryIds, statusId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='bookmark_categories.add_to_bookmark_categories'
|
||||
defaultMessage='Add {name} to bookmark_categories'
|
||||
values={{ name: <strong>@</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='lists-scrollable'>
|
||||
<NewBookmarkCategoryItem onCreate={handleCreate} />
|
||||
|
||||
{bookmark_categories.map((bookmark_category) => (
|
||||
<BookmarkCategoryItem
|
||||
key={bookmark_category.id}
|
||||
id={bookmark_category.id}
|
||||
title={bookmark_category.title}
|
||||
checked={bookmark_categoryIds.includes(bookmark_category.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default BookmarkCategoryAdder;
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeBookmarkCategoryEditorTitle, submitBookmarkCategoryEditor } from '../../../actions/bookmark_categories';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'bookmark_categories.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['bookmarkCategoryEditor', 'title']),
|
||||
disabled: !state.getIn(['bookmarkCategoryEditor', 'isChanged']) || !state.getIn(['bookmarkCategoryEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeBookmarkCategoryEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitBookmarkCategoryEditor(false)),
|
||||
});
|
||||
|
||||
class EditBookmarkCategoryForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(EditBookmarkCategoryForm));
|
|
@ -16,7 +16,7 @@ import { debounce } from 'lodash';
|
|||
import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import { deleteBookmarkCategory, expandBookmarkCategoryStatuses, fetchBookmarkCategory, fetchBookmarkCategoryStatuses , setupBookmarkCategoryEditor } from 'mastodon/actions/bookmark_categories';
|
||||
import { deleteBookmarkCategory, expandBookmarkCategoryStatuses, fetchBookmarkCategory, fetchBookmarkCategoryStatuses } from 'mastodon/actions/bookmark_categories';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
|
@ -28,8 +28,6 @@ import BundleColumnError from 'mastodon/features/ui/components/bundle_column_err
|
|||
import { getBookmarkCategoryStatusList } from 'mastodon/selectors';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
import EditBookmarkCategoryForm from './components/edit_bookmark_category_form';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: { id: 'confirmations.delete_bookmark_category.message', defaultMessage: 'Are you sure you want to permanently delete this category?' },
|
||||
|
@ -40,9 +38,8 @@ const messages = defineMessages({
|
|||
const mapStateToProps = (state, { params }) => ({
|
||||
bookmarkCategory: state.getIn(['bookmark_categories', params.id]),
|
||||
statusIds: getBookmarkCategoryStatusList(state, params.id),
|
||||
isLoading: state.getIn(['bookmark_categories', params.id, 'isLoading'], true),
|
||||
isEditing: state.getIn(['bookmarkCategoryEditor', 'bookmarkCategoryId']) === params.id,
|
||||
hasMore: !!state.getIn(['bookmark_categories', params.id, 'next']),
|
||||
isLoading: state.getIn(['status_lists', 'bookmark_category_statuses', params.id, 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'bookmark_category_statuses', params.id, 'next']),
|
||||
});
|
||||
|
||||
class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
||||
|
@ -57,7 +54,6 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
||||
|
@ -66,6 +62,16 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
this.props.dispatch(fetchBookmarkCategoryStatuses(this.props.params.id));
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
dispatch(fetchBookmarkCategory(id));
|
||||
dispatch(fetchBookmarkCategoryStatuses(id));
|
||||
}
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
|
@ -87,7 +93,7 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(setupBookmarkCategoryEditor(this.props.params.id));
|
||||
this.props.history.push(`/bookmark_categories/${this.props.params.id}/edit`);
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
|
@ -121,7 +127,7 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, bookmarkCategory, statusIds, columnId, multiColumn, hasMore, isLoading, isEditing } = this.props;
|
||||
const { intl, bookmarkCategory, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
if (typeof bookmarkCategory === 'undefined') {
|
||||
|
@ -140,10 +146,6 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked posts yet. When you bookmark one, it will show up here." />;
|
||||
|
||||
const editor = isEditing && (
|
||||
<EditBookmarkCategoryForm />
|
||||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader
|
||||
|
@ -165,8 +167,6 @@ class BookmarkCategoryStatuses extends ImmutablePureComponent {
|
|||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='bookmark_categories.delete' defaultMessage='Delete category' />
|
||||
</button>
|
||||
|
||||
{editor}
|
||||
</section>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,75 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromCircleAdder, addToCircleAdder } from '../../../actions/circles';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'circles.account.remove', defaultMessage: 'Remove from circle' },
|
||||
add: { id: 'circles.account.add', defaultMessage: 'Add to circle' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { circleId, added }) => ({
|
||||
circle: state.get('circles').get(circleId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['circleAdder', 'circles', 'items']).includes(circleId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { circleId }) => ({
|
||||
onRemove: () => dispatch(removeFromCircleAdder(circleId)),
|
||||
onAdd: () => dispatch(addToCircleAdder(circleId)),
|
||||
});
|
||||
|
||||
class Circle extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
circle: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { circle, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='user-circle' icon={CircleIcon} className='column-link__icon' fixedWidth />
|
||||
{circle.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(Circle));
|
|
@ -1,77 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import { setupCircleAdder, resetCircleAdder } from '../../actions/circles';
|
||||
import NewCircleForm from '../circles/components/new_circle_form';
|
||||
|
||||
import Account from './components/account';
|
||||
import Circle from './components/circle';
|
||||
// hack
|
||||
|
||||
const getOrderedCircles = createSelector([state => state.get('circles')], circles => {
|
||||
if (!circles) {
|
||||
return circles;
|
||||
}
|
||||
|
||||
return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
circleIds: getOrderedCircles(state).map(circle=>circle.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupCircleAdder(accountId)),
|
||||
onReset: () => dispatch(resetCircleAdder()),
|
||||
});
|
||||
|
||||
class CircleAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
circleIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountId, circleIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewCircleForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{circleIds.map(CircleId => <Circle key={CircleId} circleId={CircleId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(CircleAdder));
|
213
app/javascript/mastodon/features/circle_adder/index.tsx
Normal file
213
app/javascript/mastodon/features/circle_adder/index.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { fetchCircles } from 'mastodon/actions/circles';
|
||||
import { createCircle } from 'mastodon/actions/circles_typed';
|
||||
import {
|
||||
apiGetAccountCircles,
|
||||
apiAddAccountToCircle,
|
||||
apiRemoveAccountFromCircle,
|
||||
} from 'mastodon/api/circles';
|
||||
import type { ApiCircleJSON } from 'mastodon/api_types/circles';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedCircles } from 'mastodon/selectors/circles';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newCircle: {
|
||||
id: 'circles.new_circle_name',
|
||||
defaultMessage: 'New circle name',
|
||||
},
|
||||
createCircle: {
|
||||
id: 'circles.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const CircleItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='circle-ul' icon={CircleIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewCircleItem: React.FC<{
|
||||
onCreate: (circle: ApiCircleJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createCircle({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='lists__item' onSubmit={handleSubmit}>
|
||||
<label className='lists__item__title'>
|
||||
<Icon id='circle-ul' icon={CircleIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newCircle)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button text={intl.formatMessage(messages.createCircle)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const CircleAdder: React.FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const circles = useAppSelector((state) => getOrderedCircles(state));
|
||||
const [circleIds, setCircleIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCircles());
|
||||
|
||||
apiGetAccountCircles(accountId)
|
||||
.then((data) => {
|
||||
setCircleIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setCircleIds, accountId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(circleId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setCircleIds((currentCircleIds) => [circleId, ...currentCircleIds]);
|
||||
|
||||
apiAddAccountToCircle(circleId, accountId).catch(() => {
|
||||
setCircleIds((currentCircleIds) =>
|
||||
currentCircleIds.filter((id) => id !== circleId),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setCircleIds((currentCircleIds) =>
|
||||
currentCircleIds.filter((id) => id !== circleId),
|
||||
);
|
||||
|
||||
apiRemoveAccountFromCircle(circleId, accountId).catch(() => {
|
||||
setCircleIds((currentCircleIds) => [circleId, ...currentCircleIds]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setCircleIds, accountId],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(circle: ApiCircleJSON) => {
|
||||
setCircleIds((currentCircleIds) => [circle.id, ...currentCircleIds]);
|
||||
|
||||
apiAddAccountToCircle(circle.id, accountId).catch(() => {
|
||||
setCircleIds((currentCircleIds) =>
|
||||
currentCircleIds.filter((id) => id !== circle.id),
|
||||
);
|
||||
});
|
||||
},
|
||||
[setCircleIds, accountId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='circles.add_to_circles'
|
||||
defaultMessage='Add {name} to circles'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='lists-scrollable'>
|
||||
<NewCircleItem onCreate={handleCreate} />
|
||||
|
||||
{circles.map((circle) => (
|
||||
<CircleItem
|
||||
key={circle.id}
|
||||
id={circle.id}
|
||||
title={circle.title}
|
||||
checked={circleIds.includes(circle.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default CircleAdder;
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { removeFromCircleEditor, addToCircleEditor } from '../../../actions/circles';
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'circles.account.remove', defaultMessage: 'Remove from circle' },
|
||||
add: { id: 'circles.account.add', defaultMessage: 'Add to circle' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['circleEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromCircleEditor(accountId)),
|
||||
onAdd: () => dispatch(addToCircleEditor(accountId)),
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
|
@ -1,76 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeCircleEditorTitle, submitCircleEditor } from '../../../actions/circles';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'circles.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['circleEditor', 'title']),
|
||||
disabled: !state.getIn(['circleEditor', 'isChanged']) || !state.getIn(['circleEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeCircleEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitCircleEditor(false)),
|
||||
});
|
||||
|
||||
class CircleForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(CircleForm));
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchCircleSuggestions, clearCircleSuggestions, changeCircleSuggestions } from '../../../actions/circles';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'circles.search', defaultMessage: 'Search among people follow you' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['circleEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchCircleSuggestions(value)),
|
||||
onClear: () => dispatch(clearCircleSuggestions()),
|
||||
onChange: value => dispatch(changeCircleSuggestions(value)),
|
||||
});
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
|
@ -1,83 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { setupCircleEditor, clearCircleSuggestions, resetCircleEditor } from '../../actions/circles';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import Account from './components/account';
|
||||
import EditCircleForm from './components/edit_circle_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['circleEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['circleEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: circleId => dispatch(setupCircleEditor(circleId)),
|
||||
onClear: () => dispatch(clearCircleSuggestions()),
|
||||
onReset: () => dispatch(resetCircleEditor()),
|
||||
});
|
||||
|
||||
class CircleEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
circleId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, circleId } = this.props;
|
||||
onInitialize(circleId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditCircleForm />
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(CircleEditor));
|
|
@ -38,9 +38,8 @@ const messages = defineMessages({
|
|||
const mapStateToProps = (state, { params }) => ({
|
||||
circle: state.getIn(['circles', params.id]),
|
||||
statusIds: getCircleStatusList(state, params.id),
|
||||
isLoading: state.getIn(['circles', params.id, 'isLoading'], true),
|
||||
isEditing: state.getIn(['circleEditor', 'circleId']) === params.id,
|
||||
hasMore: !!state.getIn(['circles', params.id, 'next']),
|
||||
isLoading: state.getIn(['status_lists', 'circle_statuses', params.id, 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'circle_statuses', params.id, 'next']),
|
||||
});
|
||||
|
||||
class CircleStatuses extends ImmutablePureComponent {
|
||||
|
@ -63,6 +62,16 @@ class CircleStatuses extends ImmutablePureComponent {
|
|||
this.props.dispatch(fetchCircleStatuses(this.props.params.id));
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
dispatch(fetchCircle(id));
|
||||
dispatch(fetchCircleStatuses(id));
|
||||
}
|
||||
}
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
|
@ -84,10 +93,7 @@ class CircleStatuses extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'CIRCLE_EDITOR',
|
||||
modalProps: { circleId: this.props.params.id },
|
||||
}));
|
||||
this.props.history.push(`/circles/${this.props.params.id}/edit`);
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeCircleEditorTitle, submitCircleEditor } from 'mastodon/actions/circles';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'circles.new.title_placeholder', defaultMessage: 'New circle title' },
|
||||
title: { id: 'circles.new.create', defaultMessage: 'Add circle' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['circleEditor', 'title']),
|
||||
disabled: state.getIn(['circleEditor', 'isSubmitting']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeCircleEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitCircleEditor(true)),
|
||||
});
|
||||
|
||||
class NewCircleForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={disabled || !value}
|
||||
text={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(NewCircleForm));
|
|
@ -1,125 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
|
||||
import CirclesIcon from '@/material-icons/400-24px/account_circle-fill.svg?react';
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import { fetchCircles, deleteCircle } from 'mastodon/actions/circles';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import ColumnLink from 'mastodon/features/ui/components/column_link';
|
||||
import ColumnSubheading from 'mastodon/features/ui/components/column_subheading';
|
||||
|
||||
import NewCircleForm from './components/new_circle_form';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
subheading: { id: 'circles.subheading', defaultMessage: 'Your circles' },
|
||||
deleteMessage: { id: 'confirmations.delete_circle.message', defaultMessage: 'Are you sure you want to permanently delete this circle?' },
|
||||
deleteConfirm: { id: 'confirmations.delete_circle.confirm', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const getOrderedCircles = createSelector([state => state.get('circles')], circles => {
|
||||
if (!circles) {
|
||||
return circles;
|
||||
}
|
||||
|
||||
return circles.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
circles: getOrderedCircles(state),
|
||||
});
|
||||
|
||||
class Circles extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
circles: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
UNSAFE_componentWillMount () {
|
||||
this.props.dispatch(fetchCircles());
|
||||
}
|
||||
|
||||
handleEditClick = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'CIRCLE_EDITOR',
|
||||
modalProps: { circleId: e.currentTarget.getAttribute('data-id') },
|
||||
}));
|
||||
};
|
||||
|
||||
handleRemoveClick = (e) => {
|
||||
const { dispatch, intl } = this.props;
|
||||
|
||||
e.preventDefault();
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.deleteMessage),
|
||||
confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
onConfirm: () => {
|
||||
dispatch(deleteCircle(id));
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, circles, multiColumn } = this.props;
|
||||
|
||||
if (!circles) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.circles' defaultMessage="You don't have any circles yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.heading)}>
|
||||
<ColumnHeader title={intl.formatMessage(messages.heading)} icon='user-circle' iconComponent={CirclesIcon} multiColumn={multiColumn} />
|
||||
|
||||
<NewCircleForm />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='circles'
|
||||
emptyMessage={emptyMessage}
|
||||
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{circles.map(circle =>
|
||||
<ColumnLink key={circle.get('id')} to={`/circles/${circle.get('id')}`} icon='user-circle' iconComponent={CircleIcon} text={circle.get('title')} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Circles));
|
145
app/javascript/mastodon/features/circles/index.tsx
Normal file
145
app/javascript/mastodon/features/circles/index.tsx
Normal file
|
@ -0,0 +1,145 @@
|
|||
import { useEffect, useMemo, useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchCircles } from 'mastodon/actions/circles';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { getOrderedCircles } from 'mastodon/selectors/circles';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
create: { id: 'circles.create_circle', defaultMessage: 'Create circle' },
|
||||
edit: { id: 'circles.edit', defaultMessage: 'Edit circle' },
|
||||
delete: { id: 'circles.delete', defaultMessage: 'Delete circle' },
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
});
|
||||
|
||||
const CircleItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
}> = ({ id, title }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_DELETE_CIRCLE',
|
||||
modalProps: {
|
||||
circleId: id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const menu = useMemo(
|
||||
() => [
|
||||
{ text: intl.formatMessage(messages.edit), to: `/circles/${id}/edit` },
|
||||
{ text: intl.formatMessage(messages.delete), action: handleDeleteClick },
|
||||
],
|
||||
[intl, id, handleDeleteClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='lists__item'>
|
||||
<Link to={`/circles/${id}`} className='lists__item__title'>
|
||||
<Icon id='circle-ul' icon={CircleIcon} />
|
||||
<span>{title}</span>
|
||||
</Link>
|
||||
|
||||
<DropdownMenuContainer
|
||||
scrollKey='circles'
|
||||
items={menu}
|
||||
icons='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Circles: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const circles = useAppSelector((state) => getOrderedCircles(state));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCircles());
|
||||
}, [dispatch]);
|
||||
|
||||
const emptyMessage = (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='circles.no_circles_yet'
|
||||
defaultMessage='No circles yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='circles.create_a_circle_to_organize'
|
||||
defaultMessage='Create a new circle to organize your Home feed'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='circle-ul'
|
||||
iconComponent={CircleIcon}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<Link
|
||||
to='/circles/new'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.create)}
|
||||
aria-label={intl.formatMessage(messages.create)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='circles'
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{circles.map((circle) => (
|
||||
<CircleItem key={circle.id} id={circle.id} title={circle.title} />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Circles;
|
377
app/javascript/mastodon/features/circles/members.tsx
Normal file
377
app/javascript/mastodon/features/circles/members.tsx
Normal file
|
@ -0,0 +1,377 @@
|
|||
import { useCallback, useState, useEffect, useRef } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, Link } from 'react-router-dom';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import SquigglyArrow from '@/svg-icons/squiggly_arrow.svg?react';
|
||||
import { fetchFollowers } from 'mastodon/actions/accounts';
|
||||
import { fetchCircle } from 'mastodon/actions/circles';
|
||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||
import { apiRequest } from 'mastodon/api';
|
||||
import {
|
||||
apiGetAccounts,
|
||||
apiAddAccountToCircle,
|
||||
apiRemoveAccountFromCircle,
|
||||
} from 'mastodon/api/circles';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { FollowersCounter } from 'mastodon/components/counters';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: {
|
||||
id: 'column.circle_members',
|
||||
defaultMessage: 'Manage circle members',
|
||||
},
|
||||
placeholder: {
|
||||
id: 'circles.search_placeholder',
|
||||
defaultMessage: 'Search people you follow',
|
||||
},
|
||||
enterSearch: { id: 'circles.add_to_circle', defaultMessage: 'Add to circle' },
|
||||
add: { id: 'circles.add_member', defaultMessage: 'Add' },
|
||||
remove: { id: 'circles.remove_member', defaultMessage: 'Remove' },
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
type Mode = 'remove' | 'add';
|
||||
|
||||
const ColumnSearchHeader: React.FC<{
|
||||
onBack: () => void;
|
||||
onSubmit: (value: string) => void;
|
||||
}> = ({ onBack, onSubmit }) => {
|
||||
const intl = useIntl();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(value);
|
||||
onSubmit(value);
|
||||
},
|
||||
[setValue, onSubmit],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(value);
|
||||
}, [onSubmit, value]);
|
||||
|
||||
return (
|
||||
<ButtonInTabsBar>
|
||||
<form className='column-search-header' onSubmit={handleSubmit}>
|
||||
<button
|
||||
type='button'
|
||||
className='column-header__back-button compact'
|
||||
onClick={onBack}
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
>
|
||||
<Icon
|
||||
id='chevron-left'
|
||||
icon={ArrowBackIcon}
|
||||
className='column-back-button__icon'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type='search'
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</ButtonInTabsBar>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountItem: React.FC<{
|
||||
accountId: string;
|
||||
circleId: string;
|
||||
partOfCircle: boolean;
|
||||
onToggle: (accountId: string) => void;
|
||||
}> = ({ accountId, circleId, partOfCircle, onToggle }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (partOfCircle) {
|
||||
void apiRemoveAccountFromCircle(circleId, accountId);
|
||||
} else {
|
||||
void apiAddAccountToCircle(circleId, accountId);
|
||||
}
|
||||
|
||||
onToggle(accountId);
|
||||
}, [accountId, circleId, partOfCircle, onToggle]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstVerifiedField = account.fields.find((item) => !!item.verified_at);
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Link
|
||||
key={account.id}
|
||||
className='account__display-name'
|
||||
title={account.acct}
|
||||
to={`/@${account.acct}`}
|
||||
data-hover-card-account={account.id}
|
||||
>
|
||||
<div className='account__avatar-wrapper'>
|
||||
<Avatar account={account} size={36} />
|
||||
</div>
|
||||
|
||||
<div className='account__contents'>
|
||||
<DisplayName account={account} />
|
||||
|
||||
<div className='account__details'>
|
||||
<ShortNumber
|
||||
value={account.followers_count}
|
||||
renderer={FollowersCounter}
|
||||
isHide={account.other_settings.hide_followers_count}
|
||||
/>{' '}
|
||||
{firstVerifiedField && (
|
||||
<VerifiedBadge link={firstVerifiedField.value} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className='account__relationship'>
|
||||
<Button
|
||||
text={intl.formatMessage(
|
||||
partOfCircle ? messages.remove : messages.add,
|
||||
)}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CircleMembers: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const intl = useIntl();
|
||||
|
||||
const followingAccountIds = useAppSelector(
|
||||
(state) => state.user_lists.getIn(['followers', me, 'items']) as string[],
|
||||
);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
const [searchAccountIds, setSearchAccountIds] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mode, setMode] = useState<Mode>('remove');
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
setLoading(true);
|
||||
dispatch(fetchCircle(id));
|
||||
|
||||
void apiGetAccounts(id)
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
dispatch(fetchFollowers(me));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
setMode('add');
|
||||
}, [setMode]);
|
||||
|
||||
const handleDismissSearchClick = useCallback(() => {
|
||||
setMode('remove');
|
||||
setSearching(false);
|
||||
}, [setMode]);
|
||||
|
||||
const handleAccountToggle = useCallback(
|
||||
(accountId: string) => {
|
||||
const partOfCircle = accountIds.includes(accountId);
|
||||
|
||||
if (partOfCircle) {
|
||||
setAccountIds(accountIds.filter((id) => id !== accountId));
|
||||
} else {
|
||||
setAccountIds([accountId, ...accountIds]);
|
||||
}
|
||||
},
|
||||
[accountIds, setAccountIds],
|
||||
);
|
||||
|
||||
const searchRequestRef = useRef<AbortController | null>(null);
|
||||
|
||||
const handleSearch = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
if (searchRequestRef.current) {
|
||||
searchRequestRef.current.abort();
|
||||
}
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
searchRequestRef.current = new AbortController();
|
||||
|
||||
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
||||
signal: searchRequestRef.current.signal,
|
||||
params: {
|
||||
q: value,
|
||||
resolve: false,
|
||||
following: true,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
dispatch(importFetchedAccounts(data));
|
||||
setSearchAccountIds(data.map((a) => a.id));
|
||||
setLoading(false);
|
||||
setSearching(true);
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
setSearching(true);
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
let displayedAccountIds: string[];
|
||||
|
||||
if (mode === 'add') {
|
||||
displayedAccountIds = searching ? searchAccountIds : followingAccountIds;
|
||||
} else {
|
||||
displayedAccountIds = accountIds;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
{mode === 'remove' ? (
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
icon='circle-ul'
|
||||
iconComponent={CircleIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<button
|
||||
onClick={handleSearchClick}
|
||||
type='button'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.enterSearch)}
|
||||
aria-label={intl.formatMessage(messages.enterSearch)}
|
||||
>
|
||||
<Icon id='plus' icon={AddIcon} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ColumnSearchHeader
|
||||
onBack={handleDismissSearchClick}
|
||||
onSubmit={handleSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='circle_members'
|
||||
trackScroll={!multiColumn}
|
||||
bindToDocument={!multiColumn}
|
||||
isLoading={loading}
|
||||
showLoading={loading && displayedAccountIds.length === 0}
|
||||
hasMore={false}
|
||||
footer={
|
||||
mode === 'remove' && (
|
||||
<>
|
||||
{displayedAccountIds.length > 0 && <div className='spacer' />}
|
||||
|
||||
<div className='column-footer'>
|
||||
<Link to={`/circles/${id}`} className='button button--block'>
|
||||
<FormattedMessage id='circles.done' defaultMessage='Done' />
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
emptyMessage={
|
||||
mode === 'remove' ? (
|
||||
<>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='circles.no_members_yet'
|
||||
defaultMessage='No members yet.'
|
||||
/>
|
||||
<br />
|
||||
<FormattedMessage
|
||||
id='circles.find_users_to_add'
|
||||
defaultMessage='Find users to add'
|
||||
/>
|
||||
</span>
|
||||
|
||||
<SquigglyArrow className='empty-column-indicator__arrow' />
|
||||
</>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='circles.no_results_found'
|
||||
defaultMessage='No results found.'
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
{displayedAccountIds.map((accountId) => (
|
||||
<AccountItem
|
||||
key={accountId}
|
||||
accountId={accountId}
|
||||
circleId={id}
|
||||
partOfCircle={
|
||||
displayedAccountIds === accountIds ||
|
||||
accountIds.includes(accountId)
|
||||
}
|
||||
onToggle={handleAccountToggle}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default CircleMembers;
|
212
app/javascript/mastodon/features/circles/new.tsx
Normal file
212
app/javascript/mastodon/features/circles/new.tsx
Normal file
|
@ -0,0 +1,212 @@
|
|||
import { useCallback, useState, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams, useHistory, Link } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import { fetchCircle } from 'mastodon/actions/circles';
|
||||
import { createCircle, updateCircle } from 'mastodon/actions/circles_typed';
|
||||
import { apiGetAccounts } from 'mastodon/api/circles';
|
||||
import Column from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: { id: 'column.edit_circle', defaultMessage: 'Edit circle' },
|
||||
create: { id: 'column.create_circle', defaultMessage: 'Create circle' },
|
||||
});
|
||||
|
||||
const MembersLink: React.FC<{
|
||||
id: string;
|
||||
}> = ({ id }) => {
|
||||
const [count, setCount] = useState(0);
|
||||
const [avatars, setAvatars] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
void apiGetAccounts(id)
|
||||
.then((data) => {
|
||||
setCount(data.length);
|
||||
setAvatars(data.slice(0, 3).map((a) => a.avatar));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [id, setCount, setAvatars]);
|
||||
|
||||
return (
|
||||
<Link to={`/circles/${id}/members`} className='app-form__link'>
|
||||
<div className='app-form__link__text'>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='circles.circle_members'
|
||||
defaultMessage='Circle members'
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='circles.circle_members_count'
|
||||
defaultMessage='{count, plural, one {# member} other {# members}}'
|
||||
values={{ count }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='avatar-pile'>
|
||||
{avatars.map((url) => (
|
||||
<img key={url} src={url} alt='' />
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NewCircle: React.FC<{
|
||||
multiColumn?: boolean;
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const circle = useAppSelector((state) =>
|
||||
id ? state.circles.get(id) : undefined,
|
||||
);
|
||||
const [title, setTitle] = useState('');
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
dispatch(fetchCircle(id));
|
||||
}
|
||||
}, [dispatch, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (id && circle) {
|
||||
setTitle(circle.title);
|
||||
}
|
||||
}, [setTitle, id, circle]);
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (id) {
|
||||
void dispatch(
|
||||
updateCircle({
|
||||
id,
|
||||
title,
|
||||
}),
|
||||
).then(() => {
|
||||
setSubmitting(false);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(
|
||||
createCircle({
|
||||
title,
|
||||
}),
|
||||
).then((result) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (isFulfilled(result)) {
|
||||
history.replace(`/circles/${result.payload.id}/edit`);
|
||||
history.push(`/circles/${result.payload.id}/members`);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [history, dispatch, setSubmitting, id, title]);
|
||||
|
||||
return (
|
||||
<Column
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
>
|
||||
<ColumnHeader
|
||||
title={intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
icon='circle-ul'
|
||||
iconComponent={CircleIcon}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
/>
|
||||
|
||||
<div className='scrollable'>
|
||||
<form className='simple_form app-form' onSubmit={handleSubmit}>
|
||||
<div className='fields-group'>
|
||||
<div className='input with_label'>
|
||||
<div className='label_input'>
|
||||
<label htmlFor='circle_title'>
|
||||
<FormattedMessage
|
||||
id='circles.circle_name'
|
||||
defaultMessage='Circle name'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='label_input__wrapper'>
|
||||
<input
|
||||
id='circle_title'
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder=' '
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{id && (
|
||||
<div className='fields-group'>
|
||||
<MembersLink id={id} />
|
||||
</div>
|
||||
)}
|
||||
{!id && (
|
||||
<div className='fields-group'>
|
||||
<div className='app-form__memo'>
|
||||
<FormattedMessage
|
||||
id='circles.save_to_edit_member'
|
||||
defaultMessage='You can edit circle members after saving.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='actions'>
|
||||
<button className='button' type='submit'>
|
||||
{submitting ? (
|
||||
<LoadingIndicator />
|
||||
) : id ? (
|
||||
<FormattedMessage id='circles.save' defaultMessage='Save' />
|
||||
) : (
|
||||
<FormattedMessage id='circles.create' defaultMessage='Create' />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>
|
||||
{intl.formatMessage(id ? messages.edit : messages.create)}
|
||||
</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default NewCircle;
|
|
@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||
|
@ -15,15 +14,15 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
|||
import { Card } from './components/card';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
isLoading: state.getIn(['suggestions', 'isLoading']),
|
||||
suggestions: state.suggestions.items,
|
||||
isLoading: state.suggestions.isLoading,
|
||||
});
|
||||
|
||||
class Suggestions extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
suggestions: PropTypes.array,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
|
@ -32,17 +31,17 @@ class Suggestions extends PureComponent {
|
|||
const { dispatch, suggestions, history } = this.props;
|
||||
|
||||
// If we're navigating back to the screen, do not trigger a reload
|
||||
if (history.action === 'POP' && suggestions.size > 0) {
|
||||
if (history.action === 'POP' && suggestions.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchSuggestions(true));
|
||||
dispatch(fetchSuggestions());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isLoading, suggestions } = this.props;
|
||||
|
||||
if (!isLoading && suggestions.isEmpty()) {
|
||||
if (!isLoading && suggestions.length === 0) {
|
||||
return (
|
||||
<div className='explore__suggestions scrollable scrollable--flex'>
|
||||
<div className='empty-column-indicator'>
|
||||
|
@ -56,9 +55,9 @@ class Suggestions extends PureComponent {
|
|||
<div className='explore__suggestions scrollable' data-nosnippet>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<Card
|
||||
key={suggestion.get('account')}
|
||||
id={suggestion.get('account')}
|
||||
source={suggestion.getIn(['sources', 0])}
|
||||
key={suggestion.account_id}
|
||||
id={suggestion.account_id}
|
||||
source={suggestion.sources[0]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -145,7 +145,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
<ColumnLink key='bookmark' icon='bookmarks' iconComponent={BookmarksIcon} text={intl.formatMessage(messages.bookmarks)} to='/bookmark_categories' />,
|
||||
<ColumnLink key='favourites' icon='star' iconComponent={StarIcon} text={intl.formatMessage(messages.favourites)} to='/favourites' />,
|
||||
<ColumnLink key='lists' icon='list-ul' iconComponent={ListAltIcon} text={intl.formatMessage(messages.lists)} to='/lists' />,
|
||||
<ColumnLink key='antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} to='/antennasw' />,
|
||||
<ColumnLink key='antennas' icon='wifi' iconComponent={AntennaIcon} text={intl.formatMessage(messages.antennas)} to='/antennas' />,
|
||||
<ColumnLink key='circles' icon='user-circle' iconComponent={CirclesIcon} text={intl.formatMessage(messages.circles)} to='/circles' />,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,217 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useEffect, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import { changeSetting } from 'mastodon/actions/settings';
|
||||
import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { FollowButton } from 'mastodon/components/follow_button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
|
||||
friendsOfFriendsHint: { id: 'follow_suggestions.hints.friends_of_friends', defaultMessage: 'This profile is popular among the people you follow.' },
|
||||
similarToRecentlyFollowedHint: { id: 'follow_suggestions.hints.similar_to_recently_followed', defaultMessage: 'This profile is similar to the profiles you have most recently followed.' },
|
||||
featuredHint: { id: 'follow_suggestions.hints.featured', defaultMessage: 'This profile has been hand-picked by the {domain} team.' },
|
||||
mostFollowedHint: { id: 'follow_suggestions.hints.most_followed', defaultMessage: 'This profile is one of the most followed on {domain}.'},
|
||||
mostInteractionsHint: { id: 'follow_suggestions.hints.most_interactions', defaultMessage: 'This profile has been recently getting a lot of attention on {domain}.' },
|
||||
});
|
||||
|
||||
const Source = ({ id }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
let label, hint;
|
||||
|
||||
switch (id) {
|
||||
case 'friends_of_friends':
|
||||
hint = intl.formatMessage(messages.friendsOfFriendsHint);
|
||||
label = <FormattedMessage id='follow_suggestions.personalized_suggestion' defaultMessage='Personalized suggestion' />;
|
||||
break;
|
||||
case 'similar_to_recently_followed':
|
||||
hint = intl.formatMessage(messages.similarToRecentlyFollowedHint);
|
||||
label = <FormattedMessage id='follow_suggestions.personalized_suggestion' defaultMessage='Personalized suggestion' />;
|
||||
break;
|
||||
case 'featured':
|
||||
hint = intl.formatMessage(messages.featuredHint, { domain });
|
||||
label = <FormattedMessage id='follow_suggestions.curated_suggestion' defaultMessage='Staff pick' />;
|
||||
break;
|
||||
case 'most_followed':
|
||||
hint = intl.formatMessage(messages.mostFollowedHint, { domain });
|
||||
label = <FormattedMessage id='follow_suggestions.popular_suggestion' defaultMessage='Popular suggestion' />;
|
||||
break;
|
||||
case 'most_interactions':
|
||||
hint = intl.formatMessage(messages.mostInteractionsHint, { domain });
|
||||
label = <FormattedMessage id='follow_suggestions.popular_suggestion' defaultMessage='Popular suggestion' />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inline-follow-suggestions__body__scrollable__card__text-stack__source' title={hint}>
|
||||
<Icon icon={InfoIcon} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Source.propTypes = {
|
||||
id: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
|
||||
};
|
||||
|
||||
const Card = ({ id, sources }) => {
|
||||
const intl = useIntl();
|
||||
const account = useSelector(state => state.getIn(['accounts', id]));
|
||||
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(dismissSuggestion(id));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<div className='inline-follow-suggestions__body__scrollable__card'>
|
||||
<IconButton iconComponent={CloseIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||
|
||||
<div className='inline-follow-suggestions__body__scrollable__card__avatar'>
|
||||
<Link to={`/@${account.get('acct')}`}><Avatar account={account} size={72} /></Link>
|
||||
</div>
|
||||
|
||||
<div className='inline-follow-suggestions__body__scrollable__card__text-stack'>
|
||||
<Link to={`/@${account.get('acct')}`}><DisplayName account={account} /></Link>
|
||||
{firstVerifiedField ? <VerifiedBadge link={firstVerifiedField.get('value')} /> : <Source id={sources.get(0)} />}
|
||||
</div>
|
||||
|
||||
<FollowButton accountId={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Card.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
sources: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
const DISMISSIBLE_ID = 'home/follow-suggestions';
|
||||
|
||||
export const InlineFollowSuggestions = ({ hidden }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const suggestions = useSelector(state => state.getIn(['suggestions', 'items']));
|
||||
const isLoading = useSelector(state => state.getIn(['suggestions', 'isLoading']));
|
||||
const dismissed = useSelector(state => state.getIn(['settings', 'dismissed_banners', DISMISSIBLE_ID]));
|
||||
const bodyRef = useRef();
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchSuggestions());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
||||
|
||||
const handleLeftNav = useCallback(() => {
|
||||
bodyRef.current.scrollLeft -= 200;
|
||||
}, [bodyRef]);
|
||||
|
||||
const handleRightNav = useCallback(() => {
|
||||
bodyRef.current.scrollLeft += 200;
|
||||
}, [bodyRef]);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true));
|
||||
}, [dispatch]);
|
||||
|
||||
if (dismissed || (!isLoading && suggestions.isEmpty())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return (
|
||||
<div className='inline-follow-suggestions' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inline-follow-suggestions'>
|
||||
<div className='inline-follow-suggestions__header'>
|
||||
<h3><FormattedMessage id='follow_suggestions.who_to_follow' defaultMessage='Who to follow' /></h3>
|
||||
|
||||
<div className='inline-follow-suggestions__header__actions'>
|
||||
<button className='link-button' onClick={handleDismiss}><FormattedMessage id='follow_suggestions.dismiss' defaultMessage="Don't show again" /></button>
|
||||
<Link to='/explore/suggestions' className='link-button'><FormattedMessage id='follow_suggestions.view_all' defaultMessage='View all' /></Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='inline-follow-suggestions__body'>
|
||||
<div className='inline-follow-suggestions__body__scrollable' ref={bodyRef} onScroll={handleScroll}>
|
||||
{suggestions.map(suggestion => (
|
||||
<Card
|
||||
key={suggestion.get('account')}
|
||||
id={suggestion.get('account')}
|
||||
sources={suggestion.get('sources')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canScrollLeft && (
|
||||
<button className='inline-follow-suggestions__body__scroll-button left' onClick={handleLeftNav} aria-label={intl.formatMessage(messages.previous)}>
|
||||
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronLeftIcon} /></div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canScrollRight && (
|
||||
<button className='inline-follow-suggestions__body__scroll-button right' onClick={handleRightNav} aria-label={intl.formatMessage(messages.next)}>
|
||||
<div className='inline-follow-suggestions__body__scroll-button__icon'><Icon icon={ChevronRightIcon} /></div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InlineFollowSuggestions.propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
};
|
|
@ -0,0 +1,326 @@
|
|||
import { useEffect, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import { changeSetting } from 'mastodon/actions/settings';
|
||||
import {
|
||||
fetchSuggestions,
|
||||
dismissSuggestion,
|
||||
} from 'mastodon/actions/suggestions';
|
||||
import type { ApiSuggestionSourceJSON } from 'mastodon/api_types/suggestions';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { FollowButton } from 'mastodon/components/follow_button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { domain } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||
dismiss: {
|
||||
id: 'follow_suggestions.dismiss',
|
||||
defaultMessage: "Don't show again",
|
||||
},
|
||||
friendsOfFriendsHint: {
|
||||
id: 'follow_suggestions.hints.friends_of_friends',
|
||||
defaultMessage: 'This profile is popular among the people you follow.',
|
||||
},
|
||||
similarToRecentlyFollowedHint: {
|
||||
id: 'follow_suggestions.hints.similar_to_recently_followed',
|
||||
defaultMessage:
|
||||
'This profile is similar to the profiles you have most recently followed.',
|
||||
},
|
||||
featuredHint: {
|
||||
id: 'follow_suggestions.hints.featured',
|
||||
defaultMessage: 'This profile has been hand-picked by the {domain} team.',
|
||||
},
|
||||
mostFollowedHint: {
|
||||
id: 'follow_suggestions.hints.most_followed',
|
||||
defaultMessage: 'This profile is one of the most followed on {domain}.',
|
||||
},
|
||||
mostInteractionsHint: {
|
||||
id: 'follow_suggestions.hints.most_interactions',
|
||||
defaultMessage:
|
||||
'This profile has been recently getting a lot of attention on {domain}.',
|
||||
},
|
||||
});
|
||||
|
||||
const Source: React.FC<{
|
||||
id: ApiSuggestionSourceJSON;
|
||||
}> = ({ id }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
let label, hint;
|
||||
|
||||
switch (id) {
|
||||
case 'friends_of_friends':
|
||||
hint = intl.formatMessage(messages.friendsOfFriendsHint);
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.personalized_suggestion'
|
||||
defaultMessage='Personalized suggestion'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'similar_to_recently_followed':
|
||||
hint = intl.formatMessage(messages.similarToRecentlyFollowedHint);
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.personalized_suggestion'
|
||||
defaultMessage='Personalized suggestion'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'featured':
|
||||
hint = intl.formatMessage(messages.featuredHint, { domain });
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.curated_suggestion'
|
||||
defaultMessage='Staff pick'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'most_followed':
|
||||
hint = intl.formatMessage(messages.mostFollowedHint, { domain });
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.popular_suggestion'
|
||||
defaultMessage='Popular suggestion'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'most_interactions':
|
||||
hint = intl.formatMessage(messages.mostInteractionsHint, { domain });
|
||||
label = (
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.popular_suggestion'
|
||||
defaultMessage='Popular suggestion'
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='inline-follow-suggestions__body__scrollable__card__text-stack__source'
|
||||
title={hint}
|
||||
>
|
||||
<Icon id='' icon={InfoIcon} />
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Card: React.FC<{
|
||||
id: string;
|
||||
sources: [ApiSuggestionSourceJSON, ...ApiSuggestionSourceJSON[]];
|
||||
}> = ({ id, sources }) => {
|
||||
const intl = useIntl();
|
||||
const account = useAppSelector((state) => state.accounts.get(id));
|
||||
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
void dispatch(dismissSuggestion({ accountId: id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<div className='inline-follow-suggestions__body__scrollable__card'>
|
||||
<IconButton
|
||||
icon=''
|
||||
iconComponent={CloseIcon}
|
||||
onClick={handleDismiss}
|
||||
title={intl.formatMessage(messages.dismiss)}
|
||||
/>
|
||||
|
||||
<div className='inline-follow-suggestions__body__scrollable__card__avatar'>
|
||||
<Link to={`/@${account?.acct}`}>
|
||||
<Avatar account={account} size={72} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='inline-follow-suggestions__body__scrollable__card__text-stack'>
|
||||
<Link to={`/@${account?.acct}`}>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
{firstVerifiedField ? (
|
||||
<VerifiedBadge link={firstVerifiedField.value} />
|
||||
) : (
|
||||
<Source id={sources[0]} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FollowButton accountId={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DISMISSIBLE_ID = 'home/follow-suggestions';
|
||||
|
||||
export const InlineFollowSuggestions: React.FC<{
|
||||
hidden?: boolean;
|
||||
}> = ({ hidden }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
||||
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
|
||||
const dismissed = useAppSelector(
|
||||
(state) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
|
||||
state.settings.getIn(['dismissed_banners', DISMISSIBLE_ID]) as boolean,
|
||||
);
|
||||
const bodyRef = useRef<HTMLDivElement>(null);
|
||||
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
||||
const [canScrollRight, setCanScrollRight] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
void dispatch(fetchSuggestions());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft(
|
||||
bodyRef.current.clientWidth - bodyRef.current.scrollLeft <
|
||||
bodyRef.current.scrollWidth,
|
||||
);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight(
|
||||
bodyRef.current.scrollLeft + bodyRef.current.clientWidth <
|
||||
bodyRef.current.scrollWidth,
|
||||
);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, suggestions]);
|
||||
|
||||
const handleLeftNav = useCallback(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
bodyRef.current.scrollLeft -= 200;
|
||||
}, []);
|
||||
|
||||
const handleRightNav = useCallback(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
bodyRef.current.scrollLeft += 200;
|
||||
}, []);
|
||||
|
||||
const handleScroll = useCallback(() => {
|
||||
if (!bodyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft(
|
||||
bodyRef.current.clientWidth - bodyRef.current.scrollLeft <
|
||||
bodyRef.current.scrollWidth,
|
||||
);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight(
|
||||
bodyRef.current.scrollLeft + bodyRef.current.clientWidth <
|
||||
bodyRef.current.scrollWidth,
|
||||
);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
dispatch(changeSetting(['dismissed_banners', DISMISSIBLE_ID], true));
|
||||
}, [dispatch]);
|
||||
|
||||
if (dismissed || (!isLoading && suggestions.length === 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
return <div className='inline-follow-suggestions' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inline-follow-suggestions'>
|
||||
<div className='inline-follow-suggestions__header'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.who_to_follow'
|
||||
defaultMessage='Who to follow'
|
||||
/>
|
||||
</h3>
|
||||
|
||||
<div className='inline-follow-suggestions__header__actions'>
|
||||
<button className='link-button' onClick={handleDismiss}>
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.dismiss'
|
||||
defaultMessage="Don't show again"
|
||||
/>
|
||||
</button>
|
||||
<Link to='/explore/suggestions' className='link-button'>
|
||||
<FormattedMessage
|
||||
id='follow_suggestions.view_all'
|
||||
defaultMessage='View all'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='inline-follow-suggestions__body'>
|
||||
<div
|
||||
className='inline-follow-suggestions__body__scrollable'
|
||||
ref={bodyRef}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{suggestions.map((suggestion) => (
|
||||
<Card
|
||||
key={suggestion.account_id}
|
||||
id={suggestion.account_id}
|
||||
sources={suggestion.sources}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{canScrollLeft && (
|
||||
<button
|
||||
className='inline-follow-suggestions__body__scroll-button left'
|
||||
onClick={handleLeftNav}
|
||||
aria-label={intl.formatMessage(messages.previous)}
|
||||
>
|
||||
<div className='inline-follow-suggestions__body__scroll-button__icon'>
|
||||
<Icon id='' icon={ChevronLeftIcon} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canScrollRight && (
|
||||
<button
|
||||
className='inline-follow-suggestions__body__scroll-button right'
|
||||
onClick={handleRightNav}
|
||||
aria-label={intl.formatMessage(messages.next)}
|
||||
>
|
||||
<div className='inline-follow-suggestions__body__scroll-button__icon'>
|
||||
<Icon id='' icon={ChevronRightIcon} />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_adder/account, circle_adder/account
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account } = this.props;
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(Account));
|
|
@ -1,82 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_adder/antenna, circle_adder/circle, bookmark_category_adder/bookmark_category
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromListAdder, addToListAdder } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
exclusive: { id: 'lists.exclusive', defaultMessage: 'Hide list or antenna account posts from home' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { listId, added }) => ({
|
||||
list: state.get('lists').get(listId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { listId }) => ({
|
||||
onRemove: () => dispatch(removeFromListAdder(listId)),
|
||||
onAdd: () => dispatch(addToListAdder(listId)),
|
||||
});
|
||||
|
||||
class List extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
list: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { list, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
const exclusiveIcon = list.get('exclusive') && <Icon id='eye-slash' icon={VisibilityOffIcon} title={intl.formatMessage(messages.exclusive)} className='column-link__icon' fixedWidth />;
|
||||
|
||||
return (
|
||||
<div className='list'>
|
||||
<div className='list__wrapper'>
|
||||
<div className='list__display-name'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} className='column-link__icon' />
|
||||
{exclusiveIcon}
|
||||
{list.get('title')}
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(MapStateToProps, mapDispatchToProps)(injectIntl(List));
|
|
@ -1,78 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_adder, circle_adder, bookmark_category_adder
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { setupListAdder, resetListAdder } from '../../actions/lists';
|
||||
import NewListForm from '../lists/components/new_list_form';
|
||||
|
||||
import Account from './components/account';
|
||||
import List from './components/list';
|
||||
// hack
|
||||
|
||||
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
listIds: getOrderedLists(state).map(list=>list.get('id')),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: accountId => dispatch(setupListAdder(accountId)),
|
||||
onReset: () => dispatch(resetListAdder()),
|
||||
});
|
||||
|
||||
class ListAdder extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
listIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, accountId } = this.props;
|
||||
onInitialize(accountId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountId, listIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-adder'>
|
||||
<div className='list-adder__account'>
|
||||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewListForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{listIds.map(ListId => <List key={ListId} listId={ListId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListAdder));
|
213
app/javascript/mastodon/features/list_adder/index.tsx
Normal file
213
app/javascript/mastodon/features/list_adder/index.tsx
Normal file
|
@ -0,0 +1,213 @@
|
|||
import { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
import { createList } from 'mastodon/actions/lists_typed';
|
||||
import {
|
||||
apiGetAccountLists,
|
||||
apiAddAccountToList,
|
||||
apiRemoveAccountFromList,
|
||||
} from 'mastodon/api/lists';
|
||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CheckBox } from 'mastodon/components/check_box';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
newList: {
|
||||
id: 'lists.new_list_name',
|
||||
defaultMessage: 'New list name',
|
||||
},
|
||||
createList: {
|
||||
id: 'lists.create',
|
||||
defaultMessage: 'Create',
|
||||
},
|
||||
close: {
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
const ListItem: React.FC<{
|
||||
id: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
onChange: (id: string, checked: boolean) => void;
|
||||
}> = ({ id, title, checked, onChange }) => {
|
||||
const handleChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(id, e.target.checked);
|
||||
},
|
||||
[id, onChange],
|
||||
);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
||||
<label className='lists__item'>
|
||||
<div className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
|
||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
const NewListItem: React.FC<{
|
||||
onCreate: (list: ApiListJSON) => void;
|
||||
}> = ({ onCreate }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(value);
|
||||
},
|
||||
[setTitle],
|
||||
);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (title.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
void dispatch(createList({ title })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
onCreate(result.payload);
|
||||
setTitle('');
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}, [setTitle, dispatch, onCreate, title]);
|
||||
|
||||
return (
|
||||
<form className='lists__item' onSubmit={handleSubmit}>
|
||||
<label className='lists__item__title'>
|
||||
<Icon id='list-ul' icon={ListAltIcon} />
|
||||
|
||||
<input
|
||||
type='text'
|
||||
value={title}
|
||||
onChange={handleChange}
|
||||
maxLength={30}
|
||||
required
|
||||
placeholder={intl.formatMessage(messages.newList)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button text={intl.formatMessage(messages.createList)} type='submit' />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ListAdder: React.FC<{
|
||||
accountId: string;
|
||||
onClose: () => void;
|
||||
}> = ({ accountId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||
const [listIds, setListIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchLists());
|
||||
|
||||
apiGetAccountLists(accountId)
|
||||
.then((data) => {
|
||||
setListIds(data.map((l) => l.id));
|
||||
return '';
|
||||
})
|
||||
.catch(() => {
|
||||
// Nothing
|
||||
});
|
||||
}, [dispatch, setListIds, accountId]);
|
||||
|
||||
const handleToggle = useCallback(
|
||||
(listId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== listId),
|
||||
);
|
||||
|
||||
apiRemoveAccountFromList(listId, accountId).catch(() => {
|
||||
setListIds((currentListIds) => [listId, ...currentListIds]);
|
||||
});
|
||||
}
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
const handleCreate = useCallback(
|
||||
(list: ApiListJSON) => {
|
||||
setListIds((currentListIds) => [list.id, ...currentListIds]);
|
||||
|
||||
apiAddAccountToList(list.id, accountId).catch(() => {
|
||||
setListIds((currentListIds) =>
|
||||
currentListIds.filter((id) => id !== list.id),
|
||||
);
|
||||
});
|
||||
},
|
||||
[setListIds, accountId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal dialog-modal'>
|
||||
<div className='dialog-modal__header'>
|
||||
<IconButton
|
||||
className='dialog-modal__header__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<span className='dialog-modal__header__title'>
|
||||
<FormattedMessage
|
||||
id='lists.add_to_lists'
|
||||
defaultMessage='Add {name} to lists'
|
||||
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='dialog-modal__content'>
|
||||
<div className='lists-scrollable'>
|
||||
<NewListItem onCreate={handleCreate} />
|
||||
|
||||
{lists.map((list) => (
|
||||
<ListItem
|
||||
key={list.id}
|
||||
id={list.id}
|
||||
title={list.title}
|
||||
checked={listIds.includes(list.id)}
|
||||
onChange={handleToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ListAdder;
|
|
@ -1,84 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_editor/account, circle_editor/account
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
|
||||
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { makeGetAccount } from '../../../selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId, added }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onRemove: () => dispatch(removeFromListEditor(accountId)),
|
||||
onAdd: () => dispatch(addToListEditor(accountId)),
|
||||
});
|
||||
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onAdd: PropTypes.func.isRequired,
|
||||
added: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, intl, onRemove, onAdd, added } = this.props;
|
||||
|
||||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton icon='times' iconComponent={CloseIcon} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' iconComponent={AddIcon} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(injectIntl(Account));
|
|
@ -1,78 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_editor/edit_antenna_form, circle_editor/edit_circle_form, bookmark_category_editor/edit_bookmark_category_form
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
|
||||
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'title']),
|
||||
disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onChange: value => dispatch(changeListEditorTitle(value)),
|
||||
onSubmit: () => dispatch(submitListEditor(false)),
|
||||
});
|
||||
|
||||
class ListForm extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onSubmit();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, disabled, intl } = this.props;
|
||||
|
||||
const title = intl.formatMessage(messages.title);
|
||||
|
||||
return (
|
||||
<form className='column-inline-form' onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
className='setting-text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon='check'
|
||||
iconComponent={CheckIcon}
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListForm));
|
|
@ -1,85 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_editor/search, circle_editor/search
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import CancelIcon from '@/material-icons/400-24px/cancel.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
|
||||
|
||||
const messages = defineMessages({
|
||||
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['listEditor', 'suggestions', 'value']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onSubmit: value => dispatch(fetchListSuggestions(value)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onChange: value => dispatch(changeListSuggestions(value)),
|
||||
});
|
||||
|
||||
class Search extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = e => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSubmit(this.props.value);
|
||||
}
|
||||
};
|
||||
|
||||
handleClear = () => {
|
||||
this.props.onClear();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, intl } = this.props;
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div className='list-editor__search search'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.search)}</span>
|
||||
|
||||
<input
|
||||
className='search__input'
|
||||
type='text'
|
||||
value={value}
|
||||
onChange={this.handleChange}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
|
||||
<Icon id='search' icon={SearchIcon} className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' icon={CancelIcon} aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
|
@ -1,85 +0,0 @@
|
|||
// Kmyblue tracking marker: copied antenna_editor, circle_editor, bookmark_category_statuses
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
|
||||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import Account from './components/account';
|
||||
import EditListForm from './components/edit_list_form';
|
||||
import Search from './components/search';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
|
||||
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onInitialize: listId => dispatch(setupListEditor(listId)),
|
||||
onClear: () => dispatch(clearListSuggestions()),
|
||||
onReset: () => dispatch(resetListEditor()),
|
||||
});
|
||||
|
||||
class ListEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
listId: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onInitialize: PropTypes.func.isRequired,
|
||||
onClear: PropTypes.func.isRequired,
|
||||
onReset: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list.isRequired,
|
||||
searchAccountIds: ImmutablePropTypes.list.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { onInitialize, listId } = this.props;
|
||||
onInitialize(listId);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
const { onReset } = this.props;
|
||||
onReset();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accountIds, searchAccountIds, onClear } = this.props;
|
||||
const showSearch = searchAccountIds.size > 0;
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal list-editor'>
|
||||
<EditListForm />
|
||||
|
||||
<Search />
|
||||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner list-editor__accounts'>
|
||||
{accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)}
|
||||
</div>
|
||||
|
||||
{showSearch && <div role='button' tabIndex={-1} className='drawer__backdrop' onClick={onClear} />}
|
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}>
|
||||
{({ x }) => (
|
||||
<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}>
|
||||
{searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ListEditor));
|
|
@ -3,21 +3,19 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { fetchList, updateList } from 'mastodon/actions/lists';
|
||||
import { fetchList } from 'mastodon/actions/lists';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { connectListStream } from 'mastodon/actions/streaming';
|
||||
import { expandListTimeline } from 'mastodon/actions/timelines';
|
||||
|
@ -25,17 +23,10 @@ import Column from 'mastodon/components/column';
|
|||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { RadioButton } from 'mastodon/components/radio_button';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
||||
const messages = defineMessages({
|
||||
followed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' },
|
||||
none: { id: 'lists.replies_policy.none', defaultMessage: 'No one' },
|
||||
list: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
list: state.getIn(['lists', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
|
||||
|
@ -117,13 +108,6 @@ class ListTimeline extends PureComponent {
|
|||
this.props.dispatch(expandListTimeline(id, { maxId }));
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'LIST_EDITOR',
|
||||
modalProps: { listId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
handleDeleteClick = () => {
|
||||
const { dispatch, columnId } = this.props;
|
||||
const { id } = this.props.params;
|
||||
|
@ -133,36 +117,15 @@ class ListTimeline extends PureComponent {
|
|||
|
||||
handleEditAntennaClick = (e) => {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
this.props.history.push(`/antennasw/${id}/edit`);
|
||||
};
|
||||
|
||||
handleRepliesPolicyChange = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, undefined, target.value, undefined));
|
||||
};
|
||||
|
||||
onExclusiveToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, target.checked, undefined, undefined));
|
||||
};
|
||||
|
||||
onNotifyToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateList(id, undefined, false, undefined, undefined, target.checked));
|
||||
this.props.history.push(`/antennas/${id}/edit`);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
|
||||
const { hasUnread, columnId, multiColumn, list } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = list ? list.get('title') : id;
|
||||
const replies_policy = list ? list.get('replies_policy') : undefined;
|
||||
const isExclusive = list ? list.get('exclusive') : undefined;
|
||||
const isNotify = list ? list.get('notify') : undefined;
|
||||
const antennas = list ? (list.get('antennas')?.toArray() || []) : [];
|
||||
const antennas = list ? (list.get('antennas') ?? []) : [];
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
return (
|
||||
|
@ -193,54 +156,24 @@ class ListTimeline extends PureComponent {
|
|||
>
|
||||
<div className='column-settings'>
|
||||
<section className='column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Link to={`/lists/${id}/edit`} className='text-btn column-header__setting-btn'>
|
||||
<Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' icon={DeleteIcon} /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-exclusive`} checked={isExclusive} onChange={this.onExclusiveToggle} />
|
||||
<label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.exclusive' defaultMessage='Hide list or antenna account posts from home' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className='similar-row'>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`list-${id}-notify`} checked={isNotify} onChange={this.onNotifyToggle} />
|
||||
<label htmlFor={`list-${id}-notify`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='lists.notify' defaultMessage='Notify these posts' />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{replies_policy !== undefined && (
|
||||
<section aria-labelledby={`list-${id}-replies-policy`}>
|
||||
<h3 id={`list-${id}-replies-policy`}><FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /></h3>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
{ ['none', 'list', 'followed'].map(policy => (
|
||||
<RadioButton name='order' key={policy} value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{ antennas.length > 0 && (
|
||||
<section aria-labelledby={`list-${id}-antenna`}>
|
||||
<h3><FormattedMessage id='lists.antennas' defaultMessage='Related antennas:' /></h3>
|
||||
|
||||
<ul className='column-settings__row'>
|
||||
{ antennas.map(antenna => (
|
||||
<li key={antenna.get('id')} className='column-settings__row__antenna'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' data-id={antenna.get('id')} onClick={this.handleEditAntennaClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> {antenna.get('title')}{antenna.get('stl') && ' [STL]'}
|
||||
<li key={antenna.id} className='column-settings__row__antenna'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' data-id={antenna.id} onClick={this.handleEditAntennaClick}>
|
||||
<Icon id='pencil' icon={EditIcon} /> {antenna.title}{antenna.stl && ' [STL]'}{antenna.ltl && ' [LTL]'}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
@ -269,4 +202,4 @@ class ListTimeline extends PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps)(injectIntl(ListTimeline)));
|
||||
export default withRouter(connect(mapStateToProps)(ListTimeline));
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue