Merge branch 'kb_development' into kb_migration

This commit is contained in:
KMY 2023-08-20 08:35:42 +09:00
commit 80d50f1656
61 changed files with 2307 additions and 164 deletions

View file

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

View file

@ -238,7 +238,6 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
limit: 4,
following: true,
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 }) => ({

View file

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

View file

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

View file

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

View file

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

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

View file

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

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

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

View file

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

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

View file

@ -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 }) => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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": "ローカルタイムラインはまだ使われていません。何か書いてみましょう!",

View file

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

View file

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

View file

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

View file

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