Merge branch 'kb_development' into kb_migration
This commit is contained in:
commit
80d50f1656
61 changed files with 2307 additions and 164 deletions
|
@ -59,7 +59,7 @@ class AntennasController < ApplicationController
|
|||
end
|
||||
|
||||
def resource_params
|
||||
params.require(:antenna).permit(:title, :list, :available, :stl, :expires_in, :with_media_only, :ignore_reblog, :keywords_raw, :exclude_keywords_raw, :domains_raw, :exclude_domains_raw, :accounts_raw, :exclude_accounts_raw, :tags_raw, :exclude_tags_raw)
|
||||
params.require(:antenna).permit(:title, :list, :available, :insert_feeds, :stl, :expires_in, :with_media_only, :ignore_reblog, :keywords_raw, :exclude_keywords_raw, :domains_raw, :exclude_domains_raw, :accounts_raw, :exclude_accounts_raw, :tags_raw, :exclude_tags_raw)
|
||||
end
|
||||
|
||||
def thin_resource_params
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Antennas::AccountsController < Api::BaseController
|
||||
# before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:lists' } # , except: [:show]
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_antenna
|
||||
|
||||
# after_action :insert_pagination_headers, only: :show
|
||||
after_action :insert_pagination_headers, only: :show
|
||||
|
||||
def show
|
||||
@accounts = load_accounts
|
||||
render json: @accounts, each_serializer: REST::AccountSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
ApplicationRecord.transaction do
|
||||
antenna_accounts.each do |account|
|
||||
@antenna.antenna_accounts.create!(account: account, exclude: false)
|
||||
@antenna.update!(any_accounts: false)
|
||||
@antenna.update!(any_accounts: false) if @antenna.any_accounts
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -32,6 +37,14 @@ class Api::V1::Antennas::AccountsController < Api::BaseController
|
|||
@antenna = Antenna.where(account: current_account).find(params[:antenna_id])
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
if unlimited?
|
||||
@antenna.accounts.without_suspended.includes(:account_stat).all
|
||||
else
|
||||
@antenna.accounts.without_suspended.includes(:account_stat).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
|
||||
end
|
||||
end
|
||||
|
||||
def antenna_accounts
|
||||
Account.find(account_ids)
|
||||
end
|
||||
|
|
54
app/controllers/api/v1/antennas/domains_controller.rb
Normal file
54
app/controllers/api/v1/antennas/domains_controller.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Antennas::DomainsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_antenna
|
||||
|
||||
def show
|
||||
@domains = load_domains
|
||||
@exclude_domains = load_exclude_domains
|
||||
render json: { domains: @domains, exclude_domains: @exclude_domains }
|
||||
end
|
||||
|
||||
def create
|
||||
ApplicationRecord.transaction do
|
||||
domains.each do |domain|
|
||||
@antenna.antenna_domains.create!(name: domain, exclude: false)
|
||||
@antenna.update!(any_domains: false) if @antenna.any_domains
|
||||
end
|
||||
end
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
AntennaDomain.where(antenna: @antenna, name: domains).destroy_all
|
||||
@antenna.update!(any_domains: true) unless @antenna.antenna_domains.where(exclude: false).any?
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_antenna
|
||||
@antenna = Antenna.where(account: current_account).find(params[:antenna_id])
|
||||
end
|
||||
|
||||
def load_domains
|
||||
@antenna.antenna_domains.pluck(:name)
|
||||
end
|
||||
|
||||
def load_exclude_domains
|
||||
@antenna.exclude_domains || []
|
||||
end
|
||||
|
||||
def domains
|
||||
Array(resource_params[:domains])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.permit(domains: [])
|
||||
end
|
||||
end
|
59
app/controllers/api/v1/antennas/keywords_controller.rb
Normal file
59
app/controllers/api/v1/antennas/keywords_controller.rb
Normal file
|
@ -0,0 +1,59 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Antennas::KeywordsController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }, only: [:show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_antenna
|
||||
|
||||
def show
|
||||
@keywords = load_keywords
|
||||
@exclude_keywords = load_exclude_keywords
|
||||
render json: { keywords: @keywords, exclude_keywords: @exclude_keywords }
|
||||
end
|
||||
|
||||
def create
|
||||
new_keywords = @antenna.keywords || []
|
||||
keywords.each do |keyword|
|
||||
raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_keyword') if new_keywords.include?(keyword)
|
||||
|
||||
new_keywords << keyword
|
||||
end
|
||||
|
||||
@antenna.update!(keywords: new_keywords, any_keywords: new_keywords.empty?)
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
def destroy
|
||||
new_keywords = @antenna.keywords || []
|
||||
new_keywords -= keywords
|
||||
|
||||
@antenna.update!(keywords: new_keywords, any_keywords: new_keywords.empty?)
|
||||
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_antenna
|
||||
@antenna = Antenna.where(account: current_account).find(params[:antenna_id])
|
||||
end
|
||||
|
||||
def load_keywords
|
||||
@antenna.keywords || []
|
||||
end
|
||||
|
||||
def load_exclude_keywords
|
||||
@antenna.exclude_keywords || []
|
||||
end
|
||||
|
||||
def keywords
|
||||
Array(resource_params[:keywords])
|
||||
end
|
||||
|
||||
def resource_params
|
||||
params.permit(keywords: [])
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ class Api::V1::AntennasController < Api::BaseController
|
|||
before_action -> { doorkeeper_authorize! :write, :'write:lists' }, except: [:index, :show]
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_antenna, except: [:index]
|
||||
before_action :set_antenna, except: [:index, :create]
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
render json: { error: e.to_s }, status: 422
|
||||
|
@ -20,9 +20,28 @@ class Api::V1::AntennasController < Api::BaseController
|
|||
render json: @antenna, serializer: REST::AntennaSerializer
|
||||
end
|
||||
|
||||
def create
|
||||
@antenna = Antenna.create!(antenna_params.merge(account: current_account, list_id: 0))
|
||||
render json: @antenna, serializer: REST::AntennaSerializer
|
||||
end
|
||||
|
||||
def update
|
||||
@antenna.update!(antenna_params)
|
||||
render json: @antenna, serializer: REST::AntennaSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
@antenna.destroy!
|
||||
render_empty
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_antenna
|
||||
@antenna = Antenna.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def antenna_params
|
||||
params.permit(:title, :list_id, :insert_feeds, :stl, :with_media_only, :ignore_reblog)
|
||||
end
|
||||
end
|
||||
|
|
67
app/controllers/api/v1/timelines/antenna_controller.rb
Normal file
67
app/controllers/api/v1/timelines/antenna_controller.rb
Normal file
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::AntennaController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }
|
||||
before_action :require_user!
|
||||
before_action :set_antenna
|
||||
before_action :set_statuses
|
||||
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
render json: @statuses,
|
||||
each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_antenna
|
||||
@antenna = Antenna.where(account: current_account).find(params[:id])
|
||||
end
|
||||
|
||||
def set_statuses
|
||||
@statuses = cached_list_statuses
|
||||
end
|
||||
|
||||
def cached_list_statuses
|
||||
cache_collection list_statuses, Status
|
||||
end
|
||||
|
||||
def list_statuses
|
||||
list_feed.get(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params[:max_id],
|
||||
params[:since_id],
|
||||
params[:min_id]
|
||||
)
|
||||
end
|
||||
|
||||
def list_feed
|
||||
AntennaFeed.new(@antenna)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_timelines_antenna_url params[:id], pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_timelines_antenna_url params[:id], pagination_params(min_id: pagination_since_id)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
|
@ -1,29 +1,106 @@
|
|||
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';
|
||||
|
||||
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_ADD_ACCOUNT_REQUEST = 'ANTENNA_EDITOR_ADD_ACCOUNT_REQUEST';
|
||||
export const ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS = 'ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS';
|
||||
export const ANTENNA_EDITOR_ADD_ACCOUNT_FAIL = 'ANTENNA_EDITOR_ADD_ACCOUNT_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_REMOVE_ACCOUNT_REQUEST = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_REQUEST';
|
||||
export const ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS';
|
||||
export const ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL = 'ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL';
|
||||
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_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_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_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_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_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_RESET = 'ANTENNA_ADDER_RESET';
|
||||
export const ANTENNA_ADDER_SETUP = 'ANTENNA_ADDER_SETUP';
|
||||
export const fetchAntenna = id => (dispatch, getState) => {
|
||||
if (getState().getIn(['antennas', id])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchAntennaRequest(id));
|
||||
|
||||
api(getState).get(`/api/v1/antennas/${id}`)
|
||||
.then(({ data }) => dispatch(fetchAntennaSuccess(data)))
|
||||
.catch(err => dispatch(fetchAntennaFail(id, err)));
|
||||
};
|
||||
|
||||
export const fetchAntennaRequest = id => ({
|
||||
type: ANTENNA_FETCH_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const fetchAntennaSuccess = antenna => ({
|
||||
type: ANTENNA_FETCH_SUCCESS,
|
||||
antenna,
|
||||
});
|
||||
|
||||
export const fetchAntennaFail = (id, error) => ({
|
||||
type: ANTENNA_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennas = () => (dispatch, getState) => {
|
||||
dispatch(fetchAntennasRequest());
|
||||
|
@ -47,6 +124,113 @@ 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 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, with_media_only, ignore_reblog, insert_feeds) => (dispatch, getState) => {
|
||||
dispatch(updateAntennaRequest(id));
|
||||
|
||||
api(getState).put(`/api/v1/antennas/${id}`, { title, list_id, stl, 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));
|
||||
|
||||
api(getState).delete(`/api/v1/antennas/${id}`)
|
||||
.then(() => dispatch(deleteAntennaSuccess(id)))
|
||||
.catch(err => dispatch(deleteAntennaFail(id, err)));
|
||||
};
|
||||
|
||||
export const deleteAntennaRequest = id => ({
|
||||
type: ANTENNA_DELETE_REQUEST,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteAntennaSuccess = id => ({
|
||||
type: ANTENNA_DELETE_SUCCESS,
|
||||
id,
|
||||
});
|
||||
|
||||
export const deleteAntennaFail = (id, error) => ({
|
||||
type: ANTENNA_DELETE_FAIL,
|
||||
id,
|
||||
error,
|
||||
});
|
||||
|
||||
export const fetchAntennaAccounts = antennaId => (dispatch, getState) => {
|
||||
dispatch(fetchAntennaAccountsRequest(antennaId));
|
||||
|
||||
|
@ -74,95 +258,251 @@ export const fetchAntennaAccountsFail = (id, error) => ({
|
|||
error,
|
||||
});
|
||||
|
||||
export const addAccountToAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(addAccountToAntennaRequest(antennaId, accountId));
|
||||
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(addAccountToAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(addAccountToAntennaFail(antennaId, accountId, err)));
|
||||
.then(() => dispatch(addToAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(addToAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const addAccountToAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_ACCOUNT_REQUEST,
|
||||
export const addToAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addAccountToAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS,
|
||||
export const addToAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const addAccountToAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_ACCOUNT_FAIL,
|
||||
export const addToAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_ADD_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const removeAccountFromAntennaEditor = accountId => (dispatch, getState) => {
|
||||
dispatch(removeAccountFromAntenna(getState().getIn(['antennaEditor', 'antennaId']), accountId));
|
||||
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 removeAccountFromAntenna = (antennaId, accountId) => (dispatch, getState) => {
|
||||
dispatch(removeAccountFromAntennaRequest(antennaId, accountId));
|
||||
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 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 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(removeAccountFromAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(removeAccountFromAntennaFail(antennaId, accountId, err)));
|
||||
.then(() => dispatch(removeFromAntennaSuccess(antennaId, accountId)))
|
||||
.catch(err => dispatch(removeFromAntennaFail(antennaId, accountId, err)));
|
||||
};
|
||||
|
||||
export const removeAccountFromAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_ACCOUNT_REQUEST,
|
||||
export const removeFromAntennaRequest = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_REQUEST,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeAccountFromAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS,
|
||||
export const removeFromAntennaSuccess = (antennaId, accountId) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
antennaId,
|
||||
accountId,
|
||||
});
|
||||
|
||||
export const removeAccountFromAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_ACCOUNT_FAIL,
|
||||
export const removeFromAntennaFail = (antennaId, accountId, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_FAIL,
|
||||
antennaId,
|
||||
accountId,
|
||||
error,
|
||||
});
|
||||
|
||||
export const addToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addAccountToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
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 removeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(removeAccountFromAntenna(antennaId, getState().getIn(['antennaAdder', '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 removeDomainFromAntennaRequest = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_REQUEST,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const fetchAccountAntennasSuccess = (id, antennas) => ({
|
||||
type: ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
|
||||
id,
|
||||
antennas,
|
||||
export const removeDomainFromAntennaSuccess = (antennaId, domain) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS,
|
||||
antennaId,
|
||||
domain,
|
||||
});
|
||||
|
||||
export const fetchAccountAntennasFail = (id, err) => ({
|
||||
type: ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
|
||||
id,
|
||||
err,
|
||||
export const removeDomainFromAntennaFail = (antennaId, domain, error) => ({
|
||||
type: ANTENNA_EDITOR_REMOVE_DOMAIN_FAIL,
|
||||
antennaId,
|
||||
domain,
|
||||
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 resetAntennaAdder = () => ({
|
||||
|
@ -178,3 +518,36 @@ export const setupAntennaAdder = accountId => (dispatch, getState) => {
|
|||
dispatch(fetchAccountAntennas(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 addToAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(addToAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
export const removeFromAntennaAdder = antennaId => (dispatch, getState) => {
|
||||
dispatch(removeFromAntenna(antennaId, getState().getIn(['antennaAdder', 'accountId'])));
|
||||
};
|
||||
|
||||
|
|
|
@ -238,7 +238,6 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
|
|||
const params = {
|
||||
q,
|
||||
resolve: false,
|
||||
limit: 4,
|
||||
following: true,
|
||||
};
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
fillPublicTimelineGaps,
|
||||
fillCommunityTimelineGaps,
|
||||
fillListTimelineGaps,
|
||||
fillAntennaTimelineGaps,
|
||||
} from './timelines';
|
||||
|
||||
/**
|
||||
|
@ -185,3 +186,10 @@ export const connectDirectStream = () =>
|
|||
*/
|
||||
export const connectListStream = listId =>
|
||||
connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) });
|
||||
|
||||
/**
|
||||
* @param {string} antennaId
|
||||
* @returns {function(): void}
|
||||
*/
|
||||
export const connectAntennaStream = antennaId =>
|
||||
connectTimelineStream(`antenna:${antennaId}`, 'antenna', { antenna: antennaId }, { fillGaps: () => fillAntennaTimelineGaps(antennaId) });
|
||||
|
|
|
@ -149,6 +149,7 @@ export const expandAccountTimeline = (accountId, { maxId, withReplies, t
|
|||
export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
|
||||
export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
|
||||
export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
|
||||
export const expandAntennaTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
|
@ -163,6 +164,7 @@ export const fillHomeTimelineGaps = (done = noOp) => fillTimelineGaps('home
|
|||
export const fillPublicTimelineGaps = ({ onlyMedia, onlyRemote } = {}, done = noOp) => fillTimelineGaps(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, only_media: !!onlyMedia }, done);
|
||||
export const fillCommunityTimelineGaps = ({ onlyMedia } = {}, done = noOp) => fillTimelineGaps(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, only_media: !!onlyMedia }, done);
|
||||
export const fillListTimelineGaps = (id, done = noOp) => fillTimelineGaps(`list:${id}`, `/api/v1/timelines/list/${id}`, {}, done);
|
||||
export const fillAntennaTimelineGaps = (id, done = noOp) => fillTimelineGaps(`antenna:${id}`, `/api/v1/timelines/antenna/${id}`, {}, done);
|
||||
|
||||
export function expandTimelineRequest(timeline, isLoadingMore) {
|
||||
return {
|
||||
|
|
|
@ -334,7 +334,7 @@ class ScrollableList extends PureComponent {
|
|||
intersectionObserverWrapper={this.intersectionObserverWrapper}
|
||||
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
|
||||
>
|
||||
{cloneElement(child, {
|
||||
{cloneElement(child, child.type.name === 'ColumnLink' ? {} : {
|
||||
getScrollPosition: this.getScrollPosition,
|
||||
updateScrollBottom: this.updateScrollBottom,
|
||||
cachedMediaWidth: this.state.cachedMediaWidth,
|
||||
|
|
|
@ -558,7 +558,7 @@ class Status extends ImmutablePureComponent {
|
|||
onOpenMedia={this.handleOpenMedia}
|
||||
card={status.get('card')}
|
||||
compact
|
||||
sensitive={status.get('sensitive')}
|
||||
sensitive={status.get('sensitive') && !status.get('spoiler_text')}
|
||||
/>
|
||||
);
|
||||
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
|
||||
|
|
|
@ -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));
|
|
@ -6,14 +6,14 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { removeFromAntennaAdder, addToAntennaAdder } from '../../../actions/antennas';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
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' },
|
||||
remove: { id: 'antennas.account.remove', defaultMessage: 'Remove from antenna' },
|
||||
add: { id: 'antennas.account.add', defaultMessage: 'Add to antenna' },
|
||||
});
|
||||
|
||||
const MapStateToProps = (state, { antennaId, added }) => ({
|
||||
|
|
|
@ -8,8 +8,9 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
import { setupAntennaAdder, resetAntennaAdder } from '../../actions/antennas';
|
||||
import NewAntennaForm from '../antennas/components/new_antenna_form';
|
||||
import Account from '../list_adder/components/account';
|
||||
|
||||
import Account from './components/account';
|
||||
import Antenna from './components/antenna';
|
||||
// hack
|
||||
|
||||
|
@ -60,8 +61,11 @@ class AntennaAdder extends ImmutablePureComponent {
|
|||
<Account accountId={accountId} />
|
||||
</div>
|
||||
|
||||
<NewAntennaForm />
|
||||
|
||||
|
||||
<div className='list-adder__lists'>
|
||||
{antennaIds.map(AntennaId => <Antenna key={AntennaId} antennaId={AntennaId} />)}
|
||||
{antennaIds.map(antennaId => <Antenna key={antennaId} antennaId={antennaId} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
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 { removeFromAntennaEditor, addToAntennaEditor } 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)),
|
||||
});
|
||||
|
||||
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' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton icon='plus' 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));
|
|
@ -0,0 +1,73 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
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'
|
||||
title={title}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AntennaForm));
|
|
@ -0,0 +1,81 @@
|
|||
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 { 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' className={classNames({ active: !hasValue })} />
|
||||
<Icon id='times-circle' aria-label={intl.formatMessage(messages.search)} className={classNames({ active: hasValue })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(Search));
|
83
app/javascript/mastodon/features/antenna_editor/index.jsx
Normal file
83
app/javascript/mastodon/features/antenna_editor/index.jsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
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, 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 => ({
|
||||
onInitialize: antennaId => dispatch(setupAntennaEditor(antennaId)),
|
||||
onClear: () => dispatch(clearAntennaSuggestions()),
|
||||
onReset: () => dispatch(resetAntennaEditor()),
|
||||
});
|
||||
|
||||
class AntennaEditor extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
antennaId: 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, antennaId } = this.props;
|
||||
onInitialize(antennaId);
|
||||
}
|
||||
|
||||
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'>
|
||||
<EditAntennaForm />
|
||||
|
||||
<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(AntennaEditor));
|
|
@ -0,0 +1,96 @@
|
|||
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 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,
|
||||
value: PropTypes.string.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleRemove = () => {
|
||||
this.props.onRemove(this.props.value);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { icon, value } = this.props;
|
||||
|
||||
return (
|
||||
<div className='setting-text-list-item'>
|
||||
<Icon id={icon} />
|
||||
<span className='label'>{value}</span>
|
||||
<IconButton icon='trash' 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,
|
||||
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();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { icon, value, values, disabled, label, title } = this.props;
|
||||
|
||||
return (
|
||||
<div className='setting-text-list'>
|
||||
{values.map((val) => (
|
||||
<TextListItem key={val} value={val} icon={icon} onRemove={this.props.onRemove} />
|
||||
))}
|
||||
|
||||
<form className='add-text-form'>
|
||||
<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));
|
374
app/javascript/mastodon/features/antenna_setting/index.jsx
Normal file
374
app/javascript/mastodon/features/antenna_setting/index.jsx
Normal file
|
@ -0,0 +1,374 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { List as ImmutableList } 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 { fetchAntenna, deleteAntenna, updateAntenna, addDomainToAntenna, removeDomainFromAntenna, fetchAntennaDomains, fetchAntennaKeywords, removeKeywordFromAntenna, addKeywordToAntenna } 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 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' },
|
||||
addDomainTitle: { id: 'antennas.add_domain', defaultMessage: 'Add domain' },
|
||||
addKeywordTitle: { id: 'antennas.add_keyword', defaultMessage: 'Add keyword' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
antenna: state.getIn(['antennas', props.params.id]),
|
||||
lists: state.get('lists'),
|
||||
domains: state.getIn(['antennas', props.params.id, 'domains']) || ImmutableList(),
|
||||
keywords: state.getIn(['antennas', props.params.id, 'keywords']) || ImmutableList(),
|
||||
});
|
||||
|
||||
class AntennaSetting extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
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.list,
|
||||
domains: ImmutablePropTypes.list,
|
||||
keywords: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
domainName: '',
|
||||
keywordName: '',
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('ANTENNA', { id: this.props.params.id }));
|
||||
this.context.router.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(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(fetchLists());
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.props.dispatch(openModal({
|
||||
modalType: 'ANTENNA_EDITOR',
|
||||
modalProps: { antennaId: this.props.params.id },
|
||||
}));
|
||||
};
|
||||
|
||||
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.context.router.history.push('/antennasw');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
handleTimelineClick = () => {
|
||||
this.context.router.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));
|
||||
};
|
||||
|
||||
onMediaOnlyToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, 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, target.checked, undefined));
|
||||
};
|
||||
|
||||
onNoInsertFeedsToggle = ({ target }) => {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = this.props.params;
|
||||
dispatch(updateAntenna(id, undefined, false, 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));
|
||||
};
|
||||
|
||||
noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
|
||||
|
||||
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));
|
||||
|
||||
render () {
|
||||
const { columnId, multiColumn, antenna, lists, domains, keywords, 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 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) {
|
||||
columnSettings = (
|
||||
<>
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-mediaonly`} defaultChecked={isMediaOnly} onChange={this.onMediaOnlyToggle} />
|
||||
<label htmlFor={`antenna-${id}-mediaonly`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.media_only' defaultMessage='Media only' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-ignorereblog`} defaultChecked={isIgnoreReblog} onChange={this.onIgnoreReblogToggle} />
|
||||
<label htmlFor={`antenna-${id}-ignorereblog`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.ignore_reblog' defaultMessage='Exclude boosts' />
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
const listOptions = lists.toArray().map((list) => {
|
||||
return { value: list[1].get('id'), label: list[1].get('title') }
|
||||
});
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
|
||||
<ColumnHeader
|
||||
icon='wifi'
|
||||
title={title}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditAntennaClick}>
|
||||
<Icon id='pencil' /> <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' /> <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' /> <FormattedMessage id='antennas.go_timeline' defaultMessage='Go to antenna timeline' />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-stl`} defaultChecked={isStl} onChange={this.onStlToggle} />
|
||||
<label htmlFor={`antenna-${id}-stl`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.stl' defaultMessage='STL mode' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id={`antenna-${id}-noinsertfeeds`} defaultChecked={isInsertFeeds} onChange={this.onNoInsertFeedsToggle} />
|
||||
<label htmlFor={`antenna-${id}-noinsertfeeds`} className='setting-toggle__label'>
|
||||
<FormattedMessage id='antennas.insert_feeds' defaultMessage='Insert to feeds' />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{columnSettings}
|
||||
</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-select__container'
|
||||
classNamePrefix='column-select'
|
||||
name='lists'
|
||||
placeholder={this.props.intl.formatMessage(messages.placeholder)}
|
||||
defaultOptions
|
||||
/>
|
||||
</NonceProvider>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isStl && (
|
||||
<>
|
||||
<h3><FormattedMessage id='antennas.accounts' defaultMessage='{count} accounts' values={{ count: antenna.get('accounts_count') }} /></h3>
|
||||
<Button text={intl.formatMessage(messages.editAccounts)} onClick={this.handleEditClick} />
|
||||
|
||||
<h3><FormattedMessage id='antennas.domains' defaultMessage='{count} domains' values={{ count: antenna.get('domains_count') }} /></h3>
|
||||
<TextList
|
||||
onChange={this.onDomainNameChanged}
|
||||
onAdd={this.onDomainAdd}
|
||||
onRemove={this.onDomainRemove}
|
||||
value={this.state.domainName}
|
||||
values={domains.get('domains') || ImmutableList()}
|
||||
icon='sitemap'
|
||||
label={intl.formatMessage(messages.addDomainLabel)}
|
||||
title={intl.formatMessage(messages.addDomainTitle)}
|
||||
/>
|
||||
|
||||
<h3><FormattedMessage id='antennas.tags' defaultMessage='{count} tags' values={{ count: antenna.get('tags_count') }} /></h3>
|
||||
|
||||
<h3><FormattedMessage id='antennas.keywords' defaultMessage='{count} keywords' values={{ count: antenna.get('keywords_count') }} /></h3>
|
||||
<TextList
|
||||
onChange={this.onKeywordNameChanged}
|
||||
onAdd={this.onKeywordAdd}
|
||||
onRemove={this.onKeywordRemove}
|
||||
value={this.state.keywordName}
|
||||
values={keywords.get('keywords') || ImmutableList()}
|
||||
icon='paragraph'
|
||||
label={intl.formatMessage(messages.addKeywordLabel)}
|
||||
title={intl.formatMessage(messages.addKeywordTitle)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(AntennaSetting));
|
200
app/javascript/mastodon/features/antenna_timeline/index.jsx
Normal file
200
app/javascript/mastodon/features/antenna_timeline/index.jsx
Normal file
|
@ -0,0 +1,200 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchAntenna, deleteAntenna } from 'mastodon/actions/antennas';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { connectAntennaStream } from 'mastodon/actions/streaming';
|
||||
import { expandAntennaTimeline } from 'mastodon/actions/timelines';
|
||||
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 StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
|
||||
|
||||
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' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
antenna: state.getIn(['antennas', props.params.id]),
|
||||
hasUnread: state.getIn(['timelines', `antenna:${props.params.id}`, 'unread']) > 0,
|
||||
});
|
||||
|
||||
class AntennaTimeline extends PureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
hasUnread: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
antenna: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('ANTENNA_TIMELINE', { id: this.props.params.id }));
|
||||
this.context.router.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(expandAntennaTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectAntennaStream(id));
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
const { dispatch } = this.props;
|
||||
const { id } = nextProps.params;
|
||||
|
||||
if (id !== this.props.params.id) {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
|
||||
dispatch(fetchAntenna(id));
|
||||
dispatch(expandAntennaTimeline(id));
|
||||
|
||||
this.disconnect = dispatch(connectAntennaStream(id));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { id } = this.props.params;
|
||||
this.props.dispatch(expandAntennaTimeline(id, { maxId }));
|
||||
};
|
||||
|
||||
handleEditClick = () => {
|
||||
this.context.router.history.push(`/antennasw/${this.props.params.id}`);
|
||||
};
|
||||
|
||||
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.context.router.history.push('/antennasw');
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { hasUnread, columnId, multiColumn, antenna } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const title = antenna ? antenna.get('title') : id;
|
||||
|
||||
if (typeof antenna === 'undefined') {
|
||||
return (
|
||||
<Column>
|
||||
<div className='scrollable'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
} else if (antenna === false) {
|
||||
return (
|
||||
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
|
||||
<ColumnHeader
|
||||
icon='wifi'
|
||||
active={hasUnread}
|
||||
title={title}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
>
|
||||
<div className='column-settings__row column-header__links'>
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='antennas.edit' defaultMessage='Edit antenna' />
|
||||
</button>
|
||||
|
||||
<button type='button' className='text-btn column-header__setting-btn' tabIndex={0} onClick={this.handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='antennas.delete' defaultMessage='Delete antenna' />
|
||||
</button>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
|
||||
<StatusListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`antenna_timeline-${columnId}`}
|
||||
timelineId={`antenna:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.antenna' defaultMessage='There is nothing in this antenna yet. When members of this list post new statuses, they will appear here.' />}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(AntennaTimeline));
|
|
@ -0,0 +1,80 @@
|
|||
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));
|
101
app/javascript/mastodon/features/antennas/index.jsx
Normal file
101
app/javascript/mastodon/features/antennas/index.jsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchAntennas } from 'mastodon/actions/antennas';
|
||||
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 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' },
|
||||
});
|
||||
|
||||
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' 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' text={antenna.get('title')}>
|
||||
<span className='antenna-list-detail'>
|
||||
<span className='group'><Icon id='users' />{antenna.get('accounts_count')}</span>
|
||||
<span className='group'><Icon id='sitemap' />{antenna.get('domains_count')}</span>
|
||||
<span className='group'><Icon id='hashtag' />{antenna.get('tags_count')}</span>
|
||||
<span className='group'><Icon id='paragraph' />{antenna.get('keywords_count')}</span>
|
||||
</span>
|
||||
</ColumnLink>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Antennas));
|
|
@ -146,7 +146,7 @@ class ListTimeline extends PureComponent {
|
|||
|
||||
handleEditAntennaClick = (e) => {
|
||||
const id = e.currentTarget.getAttribute('data-id');
|
||||
window.open(`/antennas/${id}/edit`, '_blank');
|
||||
this.context.router.history.push(`/antennasw/${id}/edit`);
|
||||
}
|
||||
|
||||
handleRepliesPolicyChange = ({ target }) => {
|
||||
|
|
|
@ -232,7 +232,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
|||
);
|
||||
}
|
||||
} else if (status.get('card')) {
|
||||
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
||||
media = <Card sensitive={status.get('sensitive') && !status.get('spoiler_text')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
||||
isCardMediaWithSensitive = status.get('spoiler_text').length > 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ import { NavLink } from 'react-router-dom';
|
|||
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const ColumnLink = ({ icon, text, to, href, method, badge, transparent, ...other }) => {
|
||||
const ColumnLink = ({ icon, text, to, href, method, badge, transparent, children, ...other }) => {
|
||||
const className = classNames('column-link', { 'column-link--transparent': transparent });
|
||||
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null;
|
||||
const iconElement = typeof icon === 'string' ? <Icon id={icon} fixedWidth className='column-link__icon' /> : icon;
|
||||
const childElement = typeof children !== 'undefined' ? <p>{children}</p> : null;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
|
@ -24,6 +25,7 @@ const ColumnLink = ({ icon, text, to, href, method, badge, transparent, ...other
|
|||
{iconElement}
|
||||
<span>{text}</span>
|
||||
{badgeElement}
|
||||
{childElement}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
@ -37,6 +39,7 @@ ColumnLink.propTypes = {
|
|||
method: PropTypes.string,
|
||||
badge: PropTypes.node,
|
||||
transparent: PropTypes.bool,
|
||||
children: PropTypes.any,
|
||||
};
|
||||
|
||||
export default ColumnLink;
|
||||
|
|
|
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchAntennas } from 'mastodon/actions/antennas';
|
||||
import { fetchLists } from 'mastodon/actions/lists';
|
||||
|
||||
import ColumnLink from './column_link';
|
||||
|
@ -19,8 +20,17 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
|
|||
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(8);
|
||||
});
|
||||
|
||||
const getOrderedAntennas = createSelector([state => state.get('antennas')], antennas => {
|
||||
if (!antennas) {
|
||||
return antennas;
|
||||
}
|
||||
|
||||
return antennas.toList().filter(item => !!item && !item.get('insert_feeds')).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(8);
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
lists: getOrderedLists(state),
|
||||
antennas: getOrderedAntennas(state),
|
||||
});
|
||||
|
||||
class ListPanel extends ImmutablePureComponent {
|
||||
|
@ -28,17 +38,20 @@ class ListPanel extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
lists: ImmutablePropTypes.list,
|
||||
antennas: ImmutablePropTypes.list,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchLists());
|
||||
dispatch(fetchAntennas());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { lists } = this.props;
|
||||
const { lists, antennas } = this.props;
|
||||
const size = (lists ? lists.size : 0) + (antennas ? antennas.size : 0);
|
||||
|
||||
if (!lists || lists.isEmpty()) {
|
||||
if (size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -46,9 +59,12 @@ class ListPanel extends ImmutablePureComponent {
|
|||
<div className='list-panel'>
|
||||
<hr />
|
||||
|
||||
{lists.map(list => (
|
||||
{lists && lists.map(list => (
|
||||
<ColumnLink icon='list-ul' key={list.get('id')} strict text={list.get('title')} to={`/lists/${list.get('id')}`} transparent />
|
||||
))}
|
||||
{antennas && antennas.take(8 - (lists ? lists.size : 0)).map(antenna => (
|
||||
<ColumnLink icon='wifi' key={antenna.get('id')} strict text={antenna.get('title')} to={`/antennast/${antenna.get('id')}`} transparent />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
EmbedModal,
|
||||
ListEditor,
|
||||
ListAdder,
|
||||
AntennaEditor,
|
||||
AntennaAdder,
|
||||
CompareHistoryModal,
|
||||
FilterModal,
|
||||
|
@ -46,6 +47,7 @@ export const MODAL_COMPONENTS = {
|
|||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'LIST_EDITOR': ListEditor,
|
||||
'ANTENNA_EDITOR': AntennaEditor,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }),
|
||||
'LIST_ADDER': ListAdder,
|
||||
'ANTENNA_ADDER': AntennaAdder,
|
||||
|
|
|
@ -27,6 +27,7 @@ const messages = defineMessages({
|
|||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' },
|
||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
antennas: { id: 'navigation_bar.antennas', defaultMessage: 'Antennas' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||
|
@ -49,6 +50,10 @@ class NavigationPanel extends Component {
|
|||
return (match || location.pathname.startsWith('/public')) && !location.pathname.endsWith('/fixed');
|
||||
};
|
||||
|
||||
isAntennasActive = (match, location) => {
|
||||
return (match || location.pathname.startsWith('/antennast'));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
const { signedIn, disabledAccountId } = this.context.identity;
|
||||
|
@ -92,7 +97,6 @@ class NavigationPanel extends Component {
|
|||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
|
||||
<ListPanel />
|
||||
<hr />
|
||||
</>
|
||||
|
@ -100,6 +104,8 @@ class NavigationPanel extends Component {
|
|||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
|
||||
<ColumnLink transparent to='/antennasw' icon='wifi' text={intl.formatMessage(messages.antennas)} isActive={this.isAntennasActive} />
|
||||
<FollowRequestsColumnLink />
|
||||
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
|
||||
</>
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
StatusReferences,
|
||||
DirectTimeline,
|
||||
HashtagTimeline,
|
||||
AntennaTimeline,
|
||||
Notifications,
|
||||
FollowRequests,
|
||||
FavouritedStatuses,
|
||||
|
@ -60,6 +61,8 @@ import {
|
|||
Mutes,
|
||||
PinnedStatuses,
|
||||
Lists,
|
||||
Antennas,
|
||||
AntennaSetting,
|
||||
Directory,
|
||||
Explore,
|
||||
ReactionDeck,
|
||||
|
@ -207,6 +210,8 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
|
||||
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
|
||||
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/antennasw/:id' component={AntennaSetting} content={children} />
|
||||
<WrappedRoute path='/antennast/:id' component={AntennaTimeline} content={children} />
|
||||
<WrappedRoute path='/notifications' component={Notifications} content={children} />
|
||||
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
|
||||
<WrappedRoute path='/emoji_reactions' component={EmojiReactedStatuses} content={children} />
|
||||
|
@ -248,6 +253,7 @@ class SwitchingColumnsArea extends PureComponent {
|
|||
<WrappedRoute path='/followed_tags' component={FollowedTags} content={children} />
|
||||
<WrappedRoute path='/mutes' component={Mutes} content={children} />
|
||||
<WrappedRoute path='/lists' component={Lists} content={children} />
|
||||
<WrappedRoute path='/antennasw' component={Antennas} content={children} />
|
||||
|
||||
<Route component={BundleColumnError} />
|
||||
</WrappedSwitch>
|
||||
|
|
|
@ -34,6 +34,10 @@ export function DirectTimeline() {
|
|||
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
||||
}
|
||||
|
||||
export function AntennaTimeline () {
|
||||
return import(/* webpackChunkName: "features/antenna_timeline" */'../../antenna_timeline');
|
||||
}
|
||||
|
||||
export function ListTimeline () {
|
||||
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
|
||||
}
|
||||
|
@ -42,6 +46,10 @@ export function Lists () {
|
|||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
||||
}
|
||||
|
||||
export function Antennas () {
|
||||
return import(/* webpackChunkName: "features/antennas" */'../../antennas');
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
}
|
||||
|
@ -158,6 +166,14 @@ export function AntennaAdder () {
|
|||
return import(/*webpackChunkName: "features/antenna_adder" */'../../antenna_adder');
|
||||
}
|
||||
|
||||
export function AntennaEditor () {
|
||||
return import(/*webpackChunkName: "features/antenna_editor" */'../../antenna_editor');
|
||||
}
|
||||
|
||||
export function AntennaSetting () {
|
||||
return import(/*webpackChunkName: "features/antenna_setting" */'../../antenna_setting');
|
||||
}
|
||||
|
||||
export function Tesseract () {
|
||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||
}
|
||||
|
|
|
@ -88,6 +88,28 @@
|
|||
"alert.unexpected.message": "不明なエラーが発生しました。",
|
||||
"alert.unexpected.title": "エラー!",
|
||||
"announcement.announcement": "お知らせ",
|
||||
"antennas.accounts": "{count} のアカウント",
|
||||
"antennas.delete": "アンテナを削除",
|
||||
"antennas.domains": "{count} のドメイン",
|
||||
"antennas.edit": "アンテナを編集",
|
||||
"antennas.edit_static": "旧編集画面に移動",
|
||||
"antennas.edit_accounts": "アカウントを編集",
|
||||
"antennas.go_timeline": "タイムラインを見る",
|
||||
"antennas.ignore_reblog": "ブーストを除外",
|
||||
"antennas.in_stl_mode": "STLモードが有効になっています",
|
||||
"antennas.insert_feeds": "リストまたはホームに挿入",
|
||||
"antennas.keywords": "{count} のキーワード",
|
||||
"antennas.media_only": "メディアのみ",
|
||||
"antennas.new.create": "アンテナを作成",
|
||||
"antennas.new.title_placeholder": "新規アンテナ名",
|
||||
"antennas.not_related_list": "このアンテナはどのリストにも関連付けられていません。",
|
||||
"antennas.related_list": "このアンテナは {listTitle} に関連付けられています。",
|
||||
"antennas.search": "すべてのユーザーから検索",
|
||||
"antennas.select.no_options_message": "リストがありません",
|
||||
"antennas.select.placeholder": "リストを選択",
|
||||
"antennas.subheading": "あなたのアンテナ",
|
||||
"antennas.stl": "STLモード",
|
||||
"antennas.tags": "{count} のタグ",
|
||||
"attachments_list.unprocessed": "(未処理)",
|
||||
"audio.hide": "音声を閉じる",
|
||||
"autosuggest_hashtag.per_week": "{count} 回 / 週",
|
||||
|
@ -110,6 +132,7 @@
|
|||
"closed_registrations_modal.preamble": "Mastodonは分散型なのでどのサーバーでアカウントを作成してもこのサーバーのユーザーを誰でもフォローして交流することができます。また自分でホスティングすることもできます!",
|
||||
"closed_registrations_modal.title": "Mastodonでアカウントを作成",
|
||||
"column.about": "概要",
|
||||
"column.antennas": "アンテナ",
|
||||
"column.blocks": "ブロックしたユーザー",
|
||||
"column.bookmarks": "ブックマーク",
|
||||
"column.community": "ローカルタイムライン",
|
||||
|
@ -179,6 +202,8 @@
|
|||
"confirmations.delete.message": "本当に削除しますか?",
|
||||
"confirmations.delete_list.confirm": "削除",
|
||||
"confirmations.delete_list.message": "本当にこのリストを完全に削除しますか?",
|
||||
"confirmations.delete_antenna.confirm": "削除",
|
||||
"confirmations.delete_antenna.message": "本当にこのアンテナを完全に削除しますか?",
|
||||
"confirmations.discard_edit_media.confirm": "破棄",
|
||||
"confirmations.discard_edit_media.message": "メディアの説明またはプレビューに保存されていない変更があります。それでも破棄しますか?",
|
||||
"confirmations.domain_block.confirm": "ドメイン全体をブロック",
|
||||
|
@ -234,6 +259,8 @@
|
|||
"empty_column.account_suspended": "アカウントは停止されています",
|
||||
"empty_column.account_timeline": "投稿がありません!",
|
||||
"empty_column.account_unavailable": "プロフィールは利用できません",
|
||||
"empty_column.antenna": "このアンテナはまだ何も拾っていません。このアンテナの設定にマッチした投稿が検出されるとここに表示されます。",
|
||||
"empty_column.antennas": "まだアンテナがありません。アンテナを作るとここに表示されます。",
|
||||
"empty_column.blocks": "まだ誰もブロックしていません。",
|
||||
"empty_column.bookmarked_statuses": "まだ何もブックマーク登録していません。ブックマーク登録するとここに表示されます。",
|
||||
"empty_column.community": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
ANTENNA_ADDER_ANTENNAS_FETCH_REQUEST,
|
||||
ANTENNA_ADDER_ANTENNAS_FETCH_SUCCESS,
|
||||
ANTENNA_ADDER_ANTENNAS_FETCH_FAIL,
|
||||
ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS,
|
||||
ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
} from '../actions/antennas';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
|
@ -38,9 +38,9 @@ export default function antennaAdderReducer(state = initialState, action) {
|
|||
map.set('loaded', true);
|
||||
map.set('items', ImmutableList(action.antennas.map(item => item.id)));
|
||||
}));
|
||||
case ANTENNA_EDITOR_ADD_ACCOUNT_SUCCESS:
|
||||
case ANTENNA_EDITOR_ADD_SUCCESS:
|
||||
return state.updateIn(['antennas', 'items'], antenna => antenna.unshift(action.antennaId));
|
||||
case ANTENNA_EDITOR_REMOVE_ACCOUNT_SUCCESS:
|
||||
case ANTENNA_EDITOR_REMOVE_SUCCESS:
|
||||
return state.updateIn(['antennas', 'items'], antenna => antenna.filterNot(item => item === action.antennaId));
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import {
|
||||
ANTENNA_CREATE_REQUEST,
|
||||
ANTENNA_CREATE_FAIL,
|
||||
ANTENNA_CREATE_SUCCESS,
|
||||
ANTENNA_UPDATE_REQUEST,
|
||||
ANTENNA_UPDATE_FAIL,
|
||||
ANTENNA_UPDATE_SUCCESS,
|
||||
ANTENNA_EDITOR_RESET,
|
||||
ANTENNA_EDITOR_SETUP,
|
||||
ANTENNA_EDITOR_TITLE_CHANGE,
|
||||
ANTENNA_ACCOUNTS_FETCH_REQUEST,
|
||||
ANTENNA_ACCOUNTS_FETCH_SUCCESS,
|
||||
ANTENNA_ACCOUNTS_FETCH_FAIL,
|
||||
ANTENNA_EDITOR_SUGGESTIONS_READY,
|
||||
ANTENNA_EDITOR_SUGGESTIONS_CLEAR,
|
||||
ANTENNA_EDITOR_SUGGESTIONS_CHANGE,
|
||||
ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
} from '../actions/antennas';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
|
@ -11,16 +25,51 @@ const initialState = ImmutableMap({
|
|||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
title: '',
|
||||
accountsCount: 0,
|
||||
|
||||
accounts: ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
loaded: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
|
||||
suggestions: ImmutableMap({
|
||||
value: '',
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default function antennaEditorReducer(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ANTENNA_EDITOR_RESET:
|
||||
return initialState;
|
||||
case ANTENNA_EDITOR_SETUP:
|
||||
return state.withMutations(map => {
|
||||
map.set('antennaId', action.antenna.get('id'));
|
||||
map.set('title', action.antenna.get('title'));
|
||||
map.set('accountsCount', action.antenna.get('accounts_count'));
|
||||
map.set('isSubmitting', false);
|
||||
});
|
||||
case ANTENNA_EDITOR_TITLE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('title', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case ANTENNA_CREATE_REQUEST:
|
||||
case ANTENNA_UPDATE_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', true);
|
||||
map.set('isChanged', false);
|
||||
});
|
||||
case ANTENNA_CREATE_FAIL:
|
||||
case ANTENNA_UPDATE_FAIL:
|
||||
return state.set('isSubmitting', false);
|
||||
case ANTENNA_CREATE_SUCCESS:
|
||||
case ANTENNA_UPDATE_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', false);
|
||||
map.set('antennaId', action.antenna.id);
|
||||
});
|
||||
case ANTENNA_ACCOUNTS_FETCH_REQUEST:
|
||||
return state.setIn(['accounts', 'isLoading'], true);
|
||||
case ANTENNA_ACCOUNTS_FETCH_FAIL:
|
||||
|
@ -31,6 +80,19 @@ export default function antennaEditorReducer(state = initialState, action) {
|
|||
map.set('loaded', true);
|
||||
map.set('items', ImmutableList(action.accounts.map(item => item.id)));
|
||||
}));
|
||||
case ANTENNA_EDITOR_SUGGESTIONS_CHANGE:
|
||||
return state.setIn(['suggestions', 'value'], action.value);
|
||||
case ANTENNA_EDITOR_SUGGESTIONS_READY:
|
||||
return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
|
||||
case ANTENNA_EDITOR_SUGGESTIONS_CLEAR:
|
||||
return state.update('suggestions', suggestions => suggestions.withMutations(map => {
|
||||
map.set('items', ImmutableList());
|
||||
map.set('value', '');
|
||||
}));
|
||||
case ANTENNA_EDITOR_ADD_SUCCESS:
|
||||
return state.updateIn(['accounts', 'items'], antenna => antenna.unshift(action.accountId));
|
||||
case ANTENNA_EDITOR_REMOVE_SUCCESS:
|
||||
return state.updateIn(['accounts', 'items'], antenna => antenna.filterNot(item => item === action.accountId));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,36 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
ANTENNA_FETCH_SUCCESS,
|
||||
ANTENNA_FETCH_FAIL,
|
||||
ANTENNAS_FETCH_SUCCESS,
|
||||
ANTENNA_CREATE_SUCCESS,
|
||||
ANTENNA_UPDATE_SUCCESS,
|
||||
ANTENNA_DELETE_SUCCESS,
|
||||
ANTENNA_EDITOR_ADD_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_SUCCESS,
|
||||
ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS,
|
||||
ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS,
|
||||
ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS,
|
||||
ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS,
|
||||
ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS,
|
||||
} from '../actions/antennas';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
const normalizeAntenna = (state, antenna) => state.set(antenna.id, fromJS(antenna));
|
||||
const normalizeAntenna = (state, antenna) => {
|
||||
const old = state.get(antenna.id);
|
||||
let s = state.set(antenna.id, fromJS(antenna));
|
||||
if (old) {
|
||||
s = s.setIn([antenna.id, 'domains'], old.get('domains'));
|
||||
s = s.setIn([antenna.id, 'keywords'], old.get('keywords'));
|
||||
s = s.setIn([antenna.id, 'accounts_count'], old.get('accounts_count'));
|
||||
s = s.setIn([antenna.id, 'domains_count'], old.get('domains_count'));
|
||||
s = s.setIn([antenna.id, 'keywords_count'], old.get('keywords_count'));
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const normalizeAntennas = (state, antennas) => {
|
||||
antennas.forEach(antenna => {
|
||||
|
@ -18,8 +42,31 @@ const normalizeAntennas = (state, antennas) => {
|
|||
|
||||
export default function antennas(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ANTENNA_FETCH_SUCCESS:
|
||||
case ANTENNA_CREATE_SUCCESS:
|
||||
case ANTENNA_UPDATE_SUCCESS:
|
||||
return normalizeAntenna(state, action.antenna);
|
||||
case ANTENNAS_FETCH_SUCCESS:
|
||||
return normalizeAntennas(state, action.antennas);
|
||||
case ANTENNA_DELETE_SUCCESS:
|
||||
case ANTENNA_FETCH_FAIL:
|
||||
return state.set(action.id, false);
|
||||
case ANTENNA_EDITOR_ADD_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'accounts_count'], state.getIn([action.antennaId, 'accounts_count']) + 1);
|
||||
case ANTENNA_EDITOR_REMOVE_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'accounts_count'], state.getIn([action.antennaId, 'accounts_count']) - 1);
|
||||
case ANTENNA_EDITOR_ADD_DOMAIN_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'domains_count'], state.getIn([action.antennaId, 'domains_count']) + 1).updateIn([action.antennaId, 'domains', 'domains'], domains => (ImmutableList(domains || [])).push(action.domain));
|
||||
case ANTENNA_EDITOR_REMOVE_DOMAIN_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'domains_count'], state.getIn([action.antennaId, 'domains_count']) - 1).updateIn([action.antennaId, 'domains', 'domains'], domains => (ImmutableList(domains || [])).filterNot(domain => domain === action.domain));
|
||||
case ANTENNA_EDITOR_FETCH_DOMAINS_SUCCESS:
|
||||
return state.setIn([action.id, 'domains'], ImmutableMap(action.domains));
|
||||
case ANTENNA_EDITOR_ADD_KEYWORD_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'keywords_count'], state.getIn([action.antennaId, 'keywords_count']) + 1).updateIn([action.antennaId, 'keywords', 'keywords'], keywords => (ImmutableList(keywords || [])).push(action.keyword));
|
||||
case ANTENNA_EDITOR_REMOVE_KEYWORD_SUCCESS:
|
||||
return state.setIn([action.antennaId, 'keywords_count'], state.getIn([action.antennaId, 'keywords_count']) - 1).updateIn([action.antennaId, 'keywords', 'keywords'], keywords => (ImmutableList(keywords || [])).filterNot(keyword => keyword === action.keyword));
|
||||
case ANTENNA_EDITOR_FETCH_KEYWORDS_SUCCESS:
|
||||
return state.setIn([action.id, 'keywords'], ImmutableMap(action.keywords));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -107,6 +107,8 @@ const sharedCallbacks = {
|
|||
return channelName === streamChannelName && params.tag === streamIdentifier;
|
||||
} else if (channelName === 'list') {
|
||||
return channelName === streamChannelName && params.list === streamIdentifier;
|
||||
} else if (channelName === 'antenna') {
|
||||
return channelName === streamChannelName && params.antenna === streamIdentifier;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -7364,6 +7364,61 @@ noscript {
|
|||
}
|
||||
}
|
||||
|
||||
.antenna-list-detail {
|
||||
font-size: 12px;
|
||||
margin-left: 24px;
|
||||
|
||||
.group {
|
||||
margin-right: 16px;
|
||||
|
||||
i.fa {
|
||||
color: $light-text-color;
|
||||
margin-right: 4px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.antenna-setting {
|
||||
margin: 8px 16px 32px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
margin: 40px 0 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-text-list {
|
||||
.add-text-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
flex: 1 1 auto;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-text-list-item {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
margin: 8px 0;
|
||||
|
||||
i.fa {
|
||||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
margin: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.reaction_deck_container {
|
||||
&__row {
|
||||
display: flex;
|
||||
|
|
|
@ -90,6 +90,14 @@ class FeedManager
|
|||
true
|
||||
end
|
||||
|
||||
def push_to_antenna(antenna, status, update: false)
|
||||
return false unless add_to_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?)
|
||||
|
||||
trim(:antenna, antenna.id)
|
||||
PushUpdateWorker.perform_async(antenna.account_id, status.id, "timeline:antenna:#{antenna.id}", { 'update' => update }) if push_update_required?("timeline:antenna:#{antenna.id}")
|
||||
true
|
||||
end
|
||||
|
||||
# Remove a status from a list feed and send a streaming API update
|
||||
# @param [List] list
|
||||
# @param [Status] status
|
||||
|
@ -102,6 +110,13 @@ class FeedManager
|
|||
true
|
||||
end
|
||||
|
||||
def unpush_from_antenna(antenna, status, update: false)
|
||||
return false unless remove_from_feed(:antenna, antenna.id, status, aggregate_reblogs: antenna.account.user&.aggregates_reblogs?)
|
||||
|
||||
redis.publish("timeline:antenna:#{antenna.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
|
||||
true
|
||||
end
|
||||
|
||||
# Fill a home feed with an account's statuses
|
||||
# @param [Account] from_account
|
||||
# @param [Account] into_account
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
# exclude_tags :jsonb
|
||||
# stl :boolean default(FALSE), not null
|
||||
# ignore_reblog :boolean default(FALSE), not null
|
||||
# insert_feeds :boolean default(FALSE), not null
|
||||
#
|
||||
class Antenna < ApplicationRecord
|
||||
include Expireable
|
||||
|
@ -33,6 +34,7 @@ class Antenna < ApplicationRecord
|
|||
has_many :antenna_domains, inverse_of: :antenna, dependent: :destroy
|
||||
has_many :antenna_tags, inverse_of: :antenna, dependent: :destroy
|
||||
has_many :antenna_accounts, inverse_of: :antenna, dependent: :destroy
|
||||
has_many :accounts, through: :antenna_accounts
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :list, optional: true
|
||||
|
|
|
@ -13,4 +13,10 @@
|
|||
#
|
||||
class AntennaDomain < ApplicationRecord
|
||||
belongs_to :antenna
|
||||
|
||||
validate :duplicate_domain
|
||||
|
||||
def duplicate_domain
|
||||
raise Mastodon::ValidationError, I18n.t('antennas.errors.duplicate_domain') if AntennaDomain.exists?(antenna_id: antenna_id, name: name, exclude: exclude)
|
||||
end
|
||||
end
|
||||
|
|
7
app/models/antenna_feed.rb
Normal file
7
app/models/antenna_feed.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AntennaFeed < Feed
|
||||
def initialize(antenna)
|
||||
super(:antenna, antenna.id)
|
||||
end
|
||||
end
|
|
@ -1,9 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class REST::AntennaSerializer < ActiveModel::Serializer
|
||||
attributes :id, :title
|
||||
attributes :id, :title, :stl, :insert_feeds, :with_media_only, :ignore_reblog, :accounts_count, :domains_count, :tags_count, :keywords_count
|
||||
|
||||
class ListSerializer < ActiveModel::Serializer
|
||||
attributes :id, :title
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
end
|
||||
|
||||
has_one :list, serializer: ListSerializer, optional: true
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
|
||||
def accounts_count
|
||||
object.antenna_accounts.count
|
||||
end
|
||||
|
||||
def domains_count
|
||||
object.antenna_domains.count
|
||||
end
|
||||
|
||||
def tags_count
|
||||
object.antenna_tags.count
|
||||
end
|
||||
|
||||
def keywords_count
|
||||
object.keywords&.size || 0
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,4 +16,8 @@ class REST::ListSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
has_many :antennas, serializer: AntennaSerializer
|
||||
|
||||
def antennas
|
||||
object.antennas.where(insert_feeds: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,6 +47,7 @@ class BatchedRemoveStatusService < BaseService
|
|||
|
||||
unpush_from_home_timelines(account, account_statuses)
|
||||
unpush_from_list_timelines(account, account_statuses)
|
||||
unpush_from_antenna_timelines(account, account_statuses)
|
||||
end
|
||||
|
||||
# Cannot be batched
|
||||
|
@ -76,6 +77,14 @@ class BatchedRemoveStatusService < BaseService
|
|||
end
|
||||
end
|
||||
|
||||
def unpush_from_antenna_timelines(_account, statuses)
|
||||
Antenna.availables.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |antenna|
|
||||
statuses.each do |status|
|
||||
FeedManager.instance.unpush_from_antenna(antenna, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unpush_from_public_timelines(status, pipeline)
|
||||
return unless status.public_visibility? && status.id > @status_id_cutoff
|
||||
|
||||
|
|
|
@ -51,10 +51,11 @@ class FanOutOnWriteService < BaseService
|
|||
when :public, :unlisted, :public_unlisted, :login, :private
|
||||
deliver_to_all_followers!
|
||||
deliver_to_lists!
|
||||
deliver_to_antennas! if [:public, :public_unlisted, :login].include?(@status.visibility.to_sym) && !@account.dissubscribable
|
||||
deliver_to_antennas! unless @account.dissubscribable
|
||||
deliver_to_stl_antennas!
|
||||
when :limited
|
||||
deliver_to_lists_mentioned_accounts_only!
|
||||
deliver_to_antennas! unless @account.dissubscribable
|
||||
deliver_to_mentioned_followers!
|
||||
else
|
||||
deliver_to_mentioned_followers!
|
||||
|
@ -159,9 +160,6 @@ class FanOutOnWriteService < BaseService
|
|||
|
||||
antennas = Antenna.availables
|
||||
antennas = antennas.left_joins(:antenna_domains).where(any_domains: true).or(Antenna.left_joins(:antenna_domains).where(antenna_domains: { name: domain }))
|
||||
antennas = antennas.where(with_media_only: false) unless @status.with_media?
|
||||
antennas = antennas.where(ignore_reblog: false) unless @status.reblog?
|
||||
antennas = antennas.where(stl: false)
|
||||
|
||||
antennas = Antenna.where(id: antennas.select(:id))
|
||||
antennas = antennas.left_joins(:antenna_accounts).where(any_accounts: true).or(Antenna.left_joins(:antenna_accounts).where(antenna_accounts: { account: @account }))
|
||||
|
@ -171,13 +169,18 @@ class FanOutOnWriteService < BaseService
|
|||
antennas = antennas.left_joins(:antenna_tags).where(any_tags: true).or(Antenna.left_joins(:antenna_tags).where(antenna_tags: { tag_id: tag_ids }))
|
||||
|
||||
antennas = antennas.where(account_id: Account.without_suspended.joins(:user).select('accounts.id').where('users.current_sign_in_at > ?', User::ACTIVE_DURATION.ago))
|
||||
antennas = antennas.where(account: @status.account.followers) if [:public, :public_unlisted, :login, :limited].exclude?(@status.visibility.to_sym)
|
||||
antennas = antennas.where(account: @status.mentioned_accounts) if @status.visibility.to_sym == :limited
|
||||
antennas = antennas.where(with_media_only: false) unless @status.with_media?
|
||||
antennas = antennas.where(ignore_reblog: false) unless @status.reblog?
|
||||
antennas = antennas.where(stl: false)
|
||||
|
||||
collection = AntennaCollection.new(@status, @options[:update], false)
|
||||
|
||||
antennas.in_batches do |ans|
|
||||
ans.each do |antenna|
|
||||
next unless antenna.enabled?
|
||||
next if antenna.keywords.any? && antenna.keywords.none? { |keyword| @status.text.include?(keyword) }
|
||||
next if antenna.keywords&.any? && antenna.keywords&.none? { |keyword| @status.text.include?(keyword) }
|
||||
next if antenna.exclude_keywords&.any? { |keyword| @status.text.include?(keyword) }
|
||||
next if antenna.exclude_accounts&.include?(@status.account_id)
|
||||
next if antenna.exclude_domains&.include?(domain)
|
||||
|
@ -273,25 +276,25 @@ class FanOutOnWriteService < BaseService
|
|||
|
||||
def push(antenna)
|
||||
if antenna.list_id.zero?
|
||||
@home_account_ids << antenna.account_id
|
||||
else
|
||||
@list_ids << antenna.list_id
|
||||
@home_account_ids << { id: antenna.account_id, antenna_id: antenna.id } if @home_account_ids.none? { |id| id.id == antenna.account_id }
|
||||
elsif @list_ids.none? { |id| id.id == antenna.list_id }
|
||||
@list_ids << { id: antenna.list_id, antenna_id: antenna.id }
|
||||
end
|
||||
end
|
||||
|
||||
def deliver!
|
||||
lists = @list_ids.uniq
|
||||
homes = @home_account_ids.uniq
|
||||
lists = @list_ids
|
||||
homes = @home_account_ids
|
||||
|
||||
if lists.any?
|
||||
FeedInsertWorker.push_bulk(lists) do |list|
|
||||
[@status.id, list, 'list', { 'update' => @update, 'stl_home' => @stl_home || false }]
|
||||
[@status.id, list[:id], 'list', { 'update' => @update, 'stl_home' => @stl_home || false, 'antenna_id' => list[:antenna_id] }]
|
||||
end
|
||||
end
|
||||
|
||||
if homes.any?
|
||||
FeedInsertWorker.push_bulk(homes) do |home|
|
||||
[@status.id, home, 'home', { 'update' => @update }]
|
||||
[@status.id, home[:id], 'home', { 'update' => @update, 'antenna_id' => home[:antenna_id] }]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,6 +81,9 @@ class RemoveStatusService < BaseService
|
|||
Antenna.availables.where(list_id: 0).select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |antenna|
|
||||
FeedManager.instance.unpush_from_home(antenna.account, @status)
|
||||
end
|
||||
Antenna.availables.select(:id, :account_id).includes(account: :user).reorder(nil).find_each do |antenna|
|
||||
FeedManager.instance.unpush_from_antenna(antenna, @status)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_mentions
|
||||
|
|
|
@ -32,9 +32,6 @@
|
|||
.fields-group
|
||||
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
|
||||
|
||||
.fields-group
|
||||
= f.input :noai, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_noai'), hint: I18n.t('simple_form.hints.defaults.setting_noai')
|
||||
|
||||
%h4= t('admin.settings.discovery.publish_statistics')
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
.fields-group.fields-row__column.fields-row__column-6
|
||||
= f.input :available, wrapper: :with_label, label: t('antennas.edit.available'), hint: false
|
||||
|
||||
.fields-row
|
||||
= f.input :insert_feeds, wrapper: :with_label, label: t('antennas.edit.insert_feeds')
|
||||
.fields-row
|
||||
= f.input :stl, wrapper: :with_label, label: t('antennas.edit.stl'), hint: t('antennas.edit.stl_hint')
|
||||
|
||||
|
|
|
@ -40,9 +40,6 @@
|
|||
.fields-group
|
||||
= ff.input :'web.hide_recent_emojis', wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_hide_recent_emojis'), hint: false
|
||||
|
||||
.fields-group
|
||||
= ff.input :emoji_reaction_streaming_notify_impl2, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_emoji_reaction_streaming_notify_impl2'), hint: I18n.t('simple_form.hints.defaults.setting_emoji_reaction_streaming_notify_impl2')
|
||||
|
||||
%h4= t 'appearance.discovery'
|
||||
|
||||
.fields-group
|
||||
|
|
|
@ -39,16 +39,8 @@
|
|||
.fields-group
|
||||
= ff.input :default_sensitive, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), hint: I18n.t('simple_form.hints.defaults.setting_default_sensitive')
|
||||
|
||||
%h4= t 'preferences.stop_deliver'
|
||||
|
||||
.fields-group
|
||||
= ff.input :send_without_domain_blocks, kmyblue: true, recommended: :not_recommended, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_send_without_domain_blocks'), hint: I18n.t('simple_form.hints.defaults.setting_send_without_domain_blocks')
|
||||
|
||||
.fields-group
|
||||
= ff.input :reject_public_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_public_unlisted_subscription')
|
||||
|
||||
.fields-group
|
||||
= ff.input :reject_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_unlisted_subscription'), hint: I18n.t('simple_form.hints.defaults.setting_reject_unlisted_subscription')
|
||||
= ff.input :emoji_reaction_streaming_notify_impl2, as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_emoji_reaction_streaming_notify_impl2'), hint: I18n.t('simple_form.hints.defaults.setting_emoji_reaction_streaming_notify_impl2')
|
||||
|
||||
%h4= t 'preferences.public_timelines'
|
||||
|
||||
|
|
|
@ -51,5 +51,15 @@
|
|||
.fields-group
|
||||
= ff.input :show_application, wrapper: :with_label
|
||||
|
||||
%h4= t 'privacy.stop_deliver'
|
||||
|
||||
%p.lead= t('privacy.stop_deliver_hint_html')
|
||||
|
||||
.fields-group
|
||||
= ff.input :reject_public_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_public_unlisted_subscription')
|
||||
|
||||
.fields-group
|
||||
= ff.input :reject_unlisted_subscription, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_reject_unlisted_subscription'), hint: I18n.t('simple_form.hints.defaults.setting_reject_unlisted_subscription')
|
||||
|
||||
.actions
|
||||
= f.button :button, t('generic.save_changes'), type: :submit
|
||||
|
|
|
@ -9,6 +9,7 @@ class FeedInsertWorker
|
|||
@type = type.to_sym
|
||||
@status = Status.find(status_id)
|
||||
@options = options.symbolize_keys
|
||||
@antenna = Antenna.find(@options[:antenna_id]) if @options[:antenna_id].present?
|
||||
|
||||
case @type
|
||||
when :home, :tags
|
||||
|
@ -55,12 +56,18 @@ class FeedInsertWorker
|
|||
end
|
||||
|
||||
def perform_push
|
||||
case @type
|
||||
when :home, :tags
|
||||
FeedManager.instance.push_to_home(@follower, @status, update: update?)
|
||||
when :list
|
||||
FeedManager.instance.push_to_list(@list, @status, update: update?)
|
||||
if @antenna.nil? || @antenna.insert_feeds
|
||||
case @type
|
||||
when :home, :tags
|
||||
FeedManager.instance.push_to_home(@follower, @status, update: update?)
|
||||
when :list
|
||||
FeedManager.instance.push_to_list(@list, @status, update: update?)
|
||||
end
|
||||
end
|
||||
|
||||
return if @antenna.nil?
|
||||
|
||||
FeedManager.instance.push_to_antenna(@antenna, @status, update: update?)
|
||||
end
|
||||
|
||||
def perform_unpush
|
||||
|
@ -70,6 +77,10 @@ class FeedInsertWorker
|
|||
when :list
|
||||
FeedManager.instance.unpush_from_list(@list, @status, update: true)
|
||||
end
|
||||
|
||||
return if @antenna.nil?
|
||||
|
||||
FeedManager.instance.unpush_from_antenna(@antenna, @status, update: true)
|
||||
end
|
||||
|
||||
def perform_notify
|
||||
|
|
|
@ -1041,6 +1041,7 @@ en:
|
|||
exclude_tags_raw: Excluding hashtag list
|
||||
hint: 下のリストに、絞り込み条件・除外条件を入力します。条件は複数指定することができます。1行につき1つずつ入力してください。空行、コメント、重複を含めることはできません。
|
||||
ignore_reblog: Ignore BTs
|
||||
insert_feeds: Insert home/list timeline (Destination list setting is NOT available if this checkbox is NOT set)
|
||||
keywords_hint: キーワードは1つあたり最低2文字です。キーワードによる絞り込みを指定した場合、検索許可に対応しているサーバーからの投稿は、検索許可が「公開」以外のものは掲載されなくなります
|
||||
keywords_raw: Keyword list
|
||||
list: Destination list
|
||||
|
@ -1588,7 +1589,6 @@ en:
|
|||
other: Other
|
||||
posting_defaults: Posting defaults
|
||||
public_timelines: Public timelines
|
||||
stop_deliver: Stop delivering
|
||||
privacy:
|
||||
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case."
|
||||
privacy: Privacy
|
||||
|
@ -1597,6 +1597,7 @@ en:
|
|||
reach_hint_html: Control whether you want to be discovered and followed by new people. Do you want your posts to appear on the Explore screen? Do you want other people to see you in their follow recommendations? Do you want to accept all new followers automatically, or have granular control over each one?
|
||||
search: Search
|
||||
search_hint_html: Control how you want to be found. Do you want people to find you by what you've publicly posted about? Do you want people outside Mastodon to find your profile when searching the web? Please mind that total exclusion from all search engines cannot be guaranteed for public information.
|
||||
stop_deliver: Stop delivering
|
||||
title: Privacy and reach
|
||||
privacy_policy:
|
||||
title: Privacy Policy
|
||||
|
|
|
@ -1023,6 +1023,8 @@ ja:
|
|||
keyword: キーワード
|
||||
tag: ハッシュタグ
|
||||
errors:
|
||||
duplicate_domain: すでに同じドメインが登録されています
|
||||
duplicate_keyword: すでに同じキーワードが登録されています
|
||||
empty_contexts: 絞り込み条件が1つも指定されていないため無効です(除外条件はカウントされません)
|
||||
invalid_list_owner: これはあなたのリストではありません
|
||||
over_limit: 所持できるアンテナ数 %{limit}を超えています
|
||||
|
@ -1041,6 +1043,7 @@ ja:
|
|||
exclude_tags_raw: 除外するハッシュタグ
|
||||
hint: 下のリストに、絞り込み条件・除外条件を入力します。条件は複数指定することができます。1行につき1つずつ入力してください。空行、コメント、重複を含めることはできません。絞り込み条件(除外条件ではない)は最低1つ設定しなければいけません。
|
||||
ignore_reblog: ブーストを含めない
|
||||
insert_feeds: 投稿をホーム・リストに流す(投稿配置先リストの設定を有効にするには、この設定を有効にする必要があります)
|
||||
keywords_hint: キーワードは1つあたり最低2文字です。キーワードによる絞り込みを指定した場合、検索許可に対応しているサーバーからの投稿は、検索許可が「公開」以外のものは掲載されなくなります
|
||||
keywords_raw: 絞り込むキーワード
|
||||
list: 投稿配置先リスト
|
||||
|
@ -1579,7 +1582,17 @@ ja:
|
|||
other: その他
|
||||
posting_defaults: デフォルトの投稿設定
|
||||
public_timelines: 公開タイムライン
|
||||
privacy:
|
||||
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case."
|
||||
privacy: Privacy
|
||||
privacy_hint_html: Control how much you want to disclose for the benefit of others. People discover interesting profiles and cool apps by browsing other people's follows and seeing which apps they post from, but you may prefer to keep it hidden.
|
||||
reach: Reach
|
||||
reach_hint_html: Control whether you want to be discovered and followed by new people. Do you want your posts to appear on the Explore screen? Do you want other people to see you in their follow recommendations? Do you want to accept all new followers automatically, or have granular control over each one?
|
||||
search: Search
|
||||
search_hint_html: Control how you want to be found. Do you want people to find you by what you've publicly posted about? Do you want people outside Mastodon to find your profile when searching the web? Please mind that total exclusion from all search engines cannot be guaranteed for public information.
|
||||
stop_deliver: 配送停止
|
||||
stop_deliver_hint_html: Mastodonの投稿を、他のソフトウェアでは自由に検索することができます。Mastodon内で行ったプライバシーの設定は無視され、あなたの投稿が意図しない人に見つかるおそれがあります。ここでは、他のサーバーやソフトウェアであなたの投稿が見つからないようにする設定が可能です。ただしリスクは伴います。
|
||||
title: Privacy and reach
|
||||
privacy_policy:
|
||||
title: プライバシーポリシー
|
||||
reactions:
|
||||
|
|
|
@ -4,6 +4,8 @@ ja:
|
|||
account:
|
||||
display_name: フルネーム、ハンドルネームなど
|
||||
fields: ホームページ、代名詞、年齢など何でも構いません。
|
||||
hide_collections: People will not be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
|
||||
locked: People will request to follow you and you will be able to either accept or reject new followers.
|
||||
note: '自己紹介には #ハッシュタグ や、ほかのアカウントのユーザー名 (@user) を使用できます'
|
||||
account_alias:
|
||||
acct: 引っ越し元のユーザー名@ドメインを指定してください
|
||||
|
@ -68,7 +70,7 @@ ja:
|
|||
setting_display_media_expand: Misskeyなどは4個を超えて投稿可能です。その追加分を最大16個まで表示します。kmyblueからアップロードはできません
|
||||
setting_noai: AI学習への利用を禁止するメタタグをプロフィールページに追加します。ただし実効性があるとは限りません
|
||||
setting_public_post_to_unlisted: 未対応のサードパーティアプリからもローカル公開で投稿できますが、公開投稿はWeb以外できなくなります
|
||||
setting_reject_unlisted_subscription: Misskeyやそのフォーク(Calckeyなど)は、フォローしていないアカウントの「未収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーのうち管理人が指定したものに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに未収載として配信されること、ご理解ください
|
||||
setting_reject_unlisted_subscription: Misskeyやそのフォーク(Calckeyなど)は、フォローしていないアカウントの「未収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに未収載として配信されること、ご理解ください
|
||||
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
|
||||
setting_stop_emoji_reaction_streaming: 通信容量の節約に役立ちます
|
||||
setting_unsafe_limited_distribution: Mastodon 3.5、4.0、4.1のサーバーにも限定投稿(相互のみ)が届くようになりますが、安全でない方法で送信します
|
||||
|
@ -133,6 +135,9 @@ ja:
|
|||
sessions:
|
||||
otp: '携帯電話のアプリで生成された二要素認証コードを入力するか、リカバリーコードを使用してください:'
|
||||
webauthn: USBキーの場合は、必ず挿入し、必要に応じてタップしてください。
|
||||
settings:
|
||||
indexable: Your profile page may appear in search results on Google, Bing, and others.
|
||||
show_application: You will always be able to see which app published your post regardless.
|
||||
tag:
|
||||
name: 視認性向上などのためにアルファベット大文字小文字の変更のみ行うことができます
|
||||
user:
|
||||
|
@ -154,6 +159,8 @@ ja:
|
|||
fields:
|
||||
name: ラベル
|
||||
value: 内容
|
||||
hide_collections: (仮訳)フォロー・フォロワー一覧を隠す
|
||||
locked: (仮訳)新規フォローを承認制にする
|
||||
account_alias:
|
||||
acct: 引っ越し元のユーザー ID
|
||||
account_migration:
|
||||
|
@ -243,8 +250,8 @@ ja:
|
|||
setting_noai: 自分のコンテンツのAI学習利用に対して不快感を表明する
|
||||
setting_public_post_to_unlisted: サードパーティアプリから投稿するとき、公開投稿をローカル公開に変更する
|
||||
setting_reduce_motion: アニメーションの動きを減らす
|
||||
setting_reject_public_unlisted_subscription: 管理者の指定したサーバーに「ローカル公開」投稿を「フォロワーのみ」に変換して配送する
|
||||
setting_reject_unlisted_subscription: 管理者の指定したサーバーに「未収載」投稿を「フォロワーのみ」に変換して配送する
|
||||
setting_reject_public_unlisted_subscription: Misskey系サーバーに「ローカル公開」投稿を「フォロワーのみ」に変換して配送する
|
||||
setting_reject_unlisted_subscription: Misskey系サーバーに「未収載」投稿を「フォロワーのみ」に変換して配送する
|
||||
setting_send_without_domain_blocks: 管理人の設定した配送停止設定を拒否する (非推奨)
|
||||
setting_show_application: 送信したアプリを開示する
|
||||
setting_stop_emoji_reaction_streaming: スタンプのストリーミングを停止する
|
||||
|
@ -334,6 +341,9 @@ ja:
|
|||
trending_tag: 新しいトレンドのレビューをする必要がある時
|
||||
rule:
|
||||
text: ルール
|
||||
settings:
|
||||
indexable: (仮訳)プロフィールページを検索エンジンの収集対象に含める
|
||||
show_application: (仮訳)投稿に使用したアプリを開示する
|
||||
tag:
|
||||
listable: 検索とディレクトリへの使用を許可する
|
||||
name: ハッシュタグ
|
||||
|
|
|
@ -16,6 +16,8 @@ Rails.application.routes.draw do
|
|||
/public/remote
|
||||
/conversations
|
||||
/lists/(*any)
|
||||
/antennasw/(*any)
|
||||
/antennast/(*any)
|
||||
/notifications
|
||||
/favourites
|
||||
/emoji_reactions
|
||||
|
|
|
@ -49,6 +49,7 @@ namespace :api, format: false do
|
|||
resource :public, only: :show, controller: :public
|
||||
resources :tag, only: :show
|
||||
resources :list, only: :show
|
||||
resources :antenna, only: :show
|
||||
end
|
||||
|
||||
get '/streaming', to: 'streaming#index'
|
||||
|
@ -207,6 +208,8 @@ namespace :api, format: false do
|
|||
|
||||
resources :antennas, only: [:index, :create, :show, :update, :destroy] do
|
||||
resource :accounts, only: [:show, :create, :destroy], controller: 'antennas/accounts'
|
||||
resource :domains, only: [:show, :create, :destroy], controller: 'antennas/domains'
|
||||
resource :keywords, only: [:show, :create, :destroy], controller: 'antennas/keywords'
|
||||
end
|
||||
|
||||
namespace :featured_tags do
|
||||
|
|
23
db/migrate/20230819084858_add_no_insert_feeds_to_antennas.rb
Normal file
23
db/migrate/20230819084858_add_no_insert_feeds_to_antennas.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class AddNoInsertFeedsToAntennas < ActiveRecord::Migration[7.0]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class Antenna < ApplicationRecord
|
||||
end
|
||||
|
||||
def up
|
||||
safety_assured do
|
||||
add_column_with_default :antennas, :insert_feeds, :boolean, default: false, allow_null: false
|
||||
Antenna.where(insert_feeds: false).update_all(insert_feeds: true) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :antennas, :insert_feeds
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_08_14_223300) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_08_19_084858) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -308,6 +308,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_14_223300) do
|
|||
t.jsonb "exclude_tags"
|
||||
t.boolean "stl", default: false, null: false
|
||||
t.boolean "ignore_reblog", default: false, null: false
|
||||
t.boolean "insert_feeds", default: false, null: false
|
||||
t.index ["account_id"], name: "index_antennas_on_account_id"
|
||||
t.index ["any_accounts"], name: "index_antennas_on_any_accounts"
|
||||
t.index ["any_domains"], name: "index_antennas_on_any_domains"
|
||||
|
|
|
@ -486,6 +486,8 @@ const startServer = async () => {
|
|||
return 'direct';
|
||||
case '/api/v1/streaming/list':
|
||||
return 'list';
|
||||
case '/api/v1/streaming/antenna':
|
||||
return 'antenna';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
@ -701,6 +703,33 @@ const startServer = async () => {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} antennaId
|
||||
* @param {any} req
|
||||
* @returns {Promise.<void>}
|
||||
*/
|
||||
const authorizeAntennaAccess = (antennaId, req) => new Promise((resolve, reject) => {
|
||||
const { accountId } = req;
|
||||
|
||||
pgPool.connect((err, client, done) => {
|
||||
if (err) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
client.query('SELECT id, account_id FROM antennas WHERE id = $1 LIMIT 1', [antennaId], (err, result) => {
|
||||
done();
|
||||
|
||||
if (err || result.rows.length === 0 || result.rows[0].account_id !== accountId) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string[]} ids
|
||||
* @param {any} req
|
||||
|
@ -1214,6 +1243,17 @@ const startServer = async () => {
|
|||
reject('Not authorized to stream this list');
|
||||
});
|
||||
|
||||
break;
|
||||
case 'antenna':
|
||||
authorizeAntennaAccess(params.antenna, req).then(() => {
|
||||
resolve({
|
||||
channelIds: [`timeline:antenna:${params.antenna}`],
|
||||
options: { needsFiltering: false },
|
||||
});
|
||||
}).catch(() => {
|
||||
reject('Not authorized to stream this antenna');
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
reject('Unknown stream type');
|
||||
|
@ -1228,6 +1268,8 @@ const startServer = async () => {
|
|||
const streamNameFromChannelName = (channelName, params) => {
|
||||
if (channelName === 'list') {
|
||||
return [channelName, params.list];
|
||||
} else if (channelName === 'antenna') {
|
||||
return [channelName, params.antenna];
|
||||
} else if (['hashtag', 'hashtag:local'].includes(channelName)) {
|
||||
return [channelName, params.tag];
|
||||
} else {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue