Skip to content
Extraits de code Groupes Projets
Non vérifiée Valider e20895f2 rédigé par Eugen Rochko's avatar Eugen Rochko Validation de GitHub
Parcourir les fichiers

Add list of lists component to web UI (#5811)

* Add list of lists component to web UI

* Add list adding

* Add list removing

* List editor modal

* Add API account search limited by following=true relation

* Rework list editor modal

* Remove mandatory pagination of GET /api/v1/lists/:id/accounts

* Adjust search input placeholder

* Fix rspec (#5890)

* i18n: (zh-CN) Add missing translations for #5811 (#5891)

* i18n: (zh-CN) yarn manage:translations -- zh-CN

* i18n: (zh-CN) Add missing translations for #5811

* Fix some issues

- Display loading/missing state for list timelines
- Order lists alphabetically in overview
- Fix async list editor reset
- Redirect to /lists after deleting unpinned list
- Redirect to / after pinning a list

* Remove dead list columns when a list is deleted or fetch returns 404
parent 12cea766
Aucune branche associée trouvée
Aucune étiquette associée trouvée
Aucune requête de fusion associée trouvée
Affichage de
avec 925 ajouts et 13 suppressions
...@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController ...@@ -17,12 +17,13 @@ class Api::V1::Accounts::SearchController < Api::BaseController
AccountSearchService.new.call( AccountSearchService.new.call(
params[:q], params[:q],
limit_param(DEFAULT_ACCOUNTS_LIMIT), limit_param(DEFAULT_ACCOUNTS_LIMIT),
resolving_search?, current_account,
current_account resolve: truthy_param?(:resolve),
following: truthy_param?(:following)
) )
end end
def resolving_search? def truthy_param?(key)
params[:resolve] == 'true' params[key] == 'true'
end end
end end
...@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController ...@@ -10,7 +10,7 @@ class Api::V1::Lists::AccountsController < Api::BaseController
after_action :insert_pagination_headers, only: :show after_action :insert_pagination_headers, only: :show
def show def show
@accounts = @list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) @accounts = load_accounts
render json: @accounts, each_serializer: REST::AccountSerializer render json: @accounts, each_serializer: REST::AccountSerializer
end end
...@@ -35,6 +35,14 @@ class Api::V1::Lists::AccountsController < Api::BaseController ...@@ -35,6 +35,14 @@ class Api::V1::Lists::AccountsController < Api::BaseController
@list = List.where(account: current_account).find(params[:list_id]) @list = List.where(account: current_account).find(params[:list_id])
end end
def load_accounts
if unlimited?
@list.accounts.all
else
@list.accounts.paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id])
end
end
def list_accounts def list_accounts
Account.find(account_ids) Account.find(account_ids)
end end
...@@ -52,12 +60,16 @@ class Api::V1::Lists::AccountsController < Api::BaseController ...@@ -52,12 +60,16 @@ class Api::V1::Lists::AccountsController < Api::BaseController
end end
def next_path def next_path
return if unlimited?
if records_continue? if records_continue?
api_v1_list_accounts_url pagination_params(max_id: pagination_max_id) api_v1_list_accounts_url pagination_params(max_id: pagination_max_id)
end end
end end
def prev_path def prev_path
return if unlimited?
unless @accounts.empty? unless @accounts.empty?
api_v1_list_accounts_url pagination_params(since_id: pagination_since_id) api_v1_list_accounts_url pagination_params(since_id: pagination_since_id)
end end
...@@ -78,4 +90,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController ...@@ -78,4 +90,8 @@ class Api::V1::Lists::AccountsController < Api::BaseController
def pagination_params(core_params) def pagination_params(core_params)
params.permit(:limit).merge(core_params) params.permit(:limit).merge(core_params)
end end
def unlimited?
params[:limit] == '0'
end
end end
...@@ -4,12 +4,52 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST'; ...@@ -4,12 +4,52 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS'; export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL'; export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
export const LIST_EDITOR_TITLE_CHANGE = 'LIST_EDITOR_TITLE_CHANGE';
export const LIST_EDITOR_RESET = 'LIST_EDITOR_RESET';
export const LIST_EDITOR_SETUP = 'LIST_EDITOR_SETUP';
export const LIST_CREATE_REQUEST = 'LIST_CREATE_REQUEST';
export const LIST_CREATE_SUCCESS = 'LIST_CREATE_SUCCESS';
export const LIST_CREATE_FAIL = 'LIST_CREATE_FAIL';
export const LIST_UPDATE_REQUEST = 'LIST_UPDATE_REQUEST';
export const LIST_UPDATE_SUCCESS = 'LIST_UPDATE_SUCCESS';
export const LIST_UPDATE_FAIL = 'LIST_UPDATE_FAIL';
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
export const LIST_ACCOUNTS_FETCH_REQUEST = 'LIST_ACCOUNTS_FETCH_REQUEST';
export const LIST_ACCOUNTS_FETCH_SUCCESS = 'LIST_ACCOUNTS_FETCH_SUCCESS';
export const LIST_ACCOUNTS_FETCH_FAIL = 'LIST_ACCOUNTS_FETCH_FAIL';
export const LIST_EDITOR_SUGGESTIONS_CHANGE = 'LIST_EDITOR_SUGGESTIONS_CHANGE';
export const LIST_EDITOR_SUGGESTIONS_READY = 'LIST_EDITOR_SUGGESTIONS_READY';
export const LIST_EDITOR_SUGGESTIONS_CLEAR = 'LIST_EDITOR_SUGGESTIONS_CLEAR';
export const LIST_EDITOR_ADD_REQUEST = 'LIST_EDITOR_ADD_REQUEST';
export const LIST_EDITOR_ADD_SUCCESS = 'LIST_EDITOR_ADD_SUCCESS';
export const LIST_EDITOR_ADD_FAIL = 'LIST_EDITOR_ADD_FAIL';
export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST';
export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS';
export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL';
export const fetchList = id => (dispatch, getState) => { export const fetchList = id => (dispatch, getState) => {
if (getState().getIn(['lists', id])) {
return;
}
dispatch(fetchListRequest(id)); dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`) api(getState).get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data))) .then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(err))); .catch(err => dispatch(fetchListFail(id, err)));
}; };
export const fetchListRequest = id => ({ export const fetchListRequest = id => ({
...@@ -22,7 +62,252 @@ export const fetchListSuccess = list => ({ ...@@ -22,7 +62,252 @@ export const fetchListSuccess = list => ({
list, list,
}); });
export const fetchListFail = error => ({ export const fetchListFail = (id, error) => ({
type: LIST_FETCH_FAIL, type: LIST_FETCH_FAIL,
id,
error,
});
export const fetchLists = () => (dispatch, getState) => {
dispatch(fetchListsRequest());
api(getState).get('/api/v1/lists')
.then(({ data }) => dispatch(fetchListsSuccess(data)))
.catch(err => dispatch(fetchListsFail(err)));
};
export const fetchListsRequest = () => ({
type: LISTS_FETCH_REQUEST,
});
export const fetchListsSuccess = lists => ({
type: LISTS_FETCH_SUCCESS,
lists,
});
export const fetchListsFail = error => ({
type: LISTS_FETCH_FAIL,
error,
});
export const submitListEditor = shouldReset => (dispatch, getState) => {
const listId = getState().getIn(['listEditor', 'listId']);
const title = getState().getIn(['listEditor', 'title']);
if (listId === null) {
dispatch(createList(title, shouldReset));
} else {
dispatch(updateList(listId, title, shouldReset));
}
};
export const setupListEditor = listId => (dispatch, getState) => {
dispatch({
type: LIST_EDITOR_SETUP,
list: getState().getIn(['lists', listId]),
});
dispatch(fetchListAccounts(listId));
};
export const changeListEditorTitle = value => ({
type: LIST_EDITOR_TITLE_CHANGE,
value,
});
export const createList = (title, shouldReset) => (dispatch, getState) => {
dispatch(createListRequest());
api(getState).post('/api/v1/lists', { title }).then(({ data }) => {
dispatch(createListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(createListFail(err)));
};
export const createListRequest = () => ({
type: LIST_CREATE_REQUEST,
});
export const createListSuccess = list => ({
type: LIST_CREATE_SUCCESS,
list,
});
export const createListFail = error => ({
type: LIST_CREATE_FAIL,
error,
});
export const updateList = (id, title, shouldReset) => (dispatch, getState) => {
dispatch(updateListRequest(id));
api(getState).put(`/api/v1/lists/${id}`, { title }).then(({ data }) => {
dispatch(updateListSuccess(data));
if (shouldReset) {
dispatch(resetListEditor());
}
}).catch(err => dispatch(updateListFail(id, err)));
};
export const updateListRequest = id => ({
type: LIST_UPDATE_REQUEST,
id,
});
export const updateListSuccess = list => ({
type: LIST_UPDATE_SUCCESS,
list,
});
export const updateListFail = (id, error) => ({
type: LIST_UPDATE_FAIL,
id,
error,
});
export const resetListEditor = () => ({
type: LIST_EDITOR_RESET,
});
export const deleteList = id => (dispatch, getState) => {
dispatch(deleteListRequest(id));
api(getState).delete(`/api/v1/lists/${id}`)
.then(() => dispatch(deleteListSuccess(id)))
.catch(err => dispatch(deleteListFail(id, err)));
};
export const deleteListRequest = id => ({
type: LIST_DELETE_REQUEST,
id,
});
export const deleteListSuccess = id => ({
type: LIST_DELETE_SUCCESS,
id,
});
export const deleteListFail = (id, error) => ({
type: LIST_DELETE_FAIL,
id,
error,
});
export const fetchListAccounts = listId => (dispatch, getState) => {
dispatch(fetchListAccountsRequest(listId));
api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
.then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
.catch(err => dispatch(fetchListAccountsFail(listId, err)));
};
export const fetchListAccountsRequest = id => ({
type: LIST_ACCOUNTS_FETCH_REQUEST,
id,
});
export const fetchListAccountsSuccess = (id, accounts, next) => ({
type: LIST_ACCOUNTS_FETCH_SUCCESS,
id,
accounts,
next,
});
export const fetchListAccountsFail = (id, error) => ({
type: LIST_ACCOUNTS_FETCH_FAIL,
id,
error,
});
export const fetchListSuggestions = q => (dispatch, getState) => {
const params = {
q,
resolve: false,
limit: 4,
following: true,
};
api(getState).get('/api/v1/accounts/search', { params })
.then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
};
export const fetchListSuggestionsReady = (query, accounts) => ({
type: LIST_EDITOR_SUGGESTIONS_READY,
query,
accounts,
});
export const clearListSuggestions = () => ({
type: LIST_EDITOR_SUGGESTIONS_CLEAR,
});
export const changeListSuggestions = value => ({
type: LIST_EDITOR_SUGGESTIONS_CHANGE,
value,
});
export const addToListEditor = accountId => (dispatch, getState) => {
dispatch(addToList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const addToList = (listId, accountId) => (dispatch, getState) => {
dispatch(addToListRequest(listId, accountId));
api(getState).post(`/api/v1/lists/${listId}/accounts`, { account_ids: [accountId] })
.then(() => dispatch(addToListSuccess(listId, accountId)))
.catch(err => dispatch(addToListFail(listId, accountId, err)));
};
export const addToListRequest = (listId, accountId) => ({
type: LIST_EDITOR_ADD_REQUEST,
listId,
accountId,
});
export const addToListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_ADD_SUCCESS,
listId,
accountId,
});
export const addToListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_ADD_FAIL,
listId,
accountId,
error,
});
export const removeFromListEditor = accountId => (dispatch, getState) => {
dispatch(removeFromList(getState().getIn(['listEditor', 'listId']), accountId));
};
export const removeFromList = (listId, accountId) => (dispatch, getState) => {
dispatch(removeFromListRequest(listId, accountId));
api(getState).delete(`/api/v1/lists/${listId}/accounts`, { params: { account_ids: [accountId] } })
.then(() => dispatch(removeFromListSuccess(listId, accountId)))
.catch(err => dispatch(removeFromListFail(listId, accountId, err)));
};
export const removeFromListRequest = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_REQUEST,
listId,
accountId,
});
export const removeFromListSuccess = (listId, accountId) => ({
type: LIST_EDITOR_REMOVE_SUCCESS,
listId,
accountId,
});
export const removeFromListFail = (listId, accountId, error) => ({
type: LIST_EDITOR_REMOVE_FAIL,
listId,
accountId,
error, error,
}); });
...@@ -25,6 +25,7 @@ const messages = defineMessages({ ...@@ -25,6 +25,7 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' },
}); });
...@@ -70,6 +71,7 @@ export default class GettingStarted extends ImmutablePureComponent { ...@@ -70,6 +71,7 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />, <ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
<ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />,
]); ]);
if (myAccount.get('locked')) { if (myAccount.get('locked')) {
...@@ -79,7 +81,7 @@ export default class GettingStarted extends ImmutablePureComponent { ...@@ -79,7 +81,7 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
<ColumnLink key='9' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />, <ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' hideOnMobile />,
]); ]);
return ( return (
......
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { makeGetAccount } from '../../../selectors';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import { removeFromListEditor, addToListEditor } from '../../../actions/lists';
const messages = defineMessages({
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
add: { id: 'lists.account.add', defaultMessage: 'Add to list' },
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId, added }) => ({
account: getAccount(state, accountId),
added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added,
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { accountId }) => ({
onRemove: () => dispatch(removeFromListEditor(accountId)),
onAdd: () => dispatch(addToListEditor(accountId)),
});
@connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
export default 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>
);
}
}
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { defineMessages, injectIntl } from 'react-intl';
import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists';
import classNames from 'classnames';
const messages = defineMessages({
search: { id: 'lists.search', defaultMessage: 'Search among people you follow' },
});
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'suggestions', 'value']),
});
const mapDispatchToProps = dispatch => ({
onSubmit: value => dispatch(fetchListSuggestions(value)),
onClear: () => dispatch(clearListSuggestions()),
onChange: value => dispatch(changeListSuggestions(value)),
});
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class Search extends React.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}>
<i className={classNames('fa fa-search', { active: !hasValue })} />
<i aria-label={intl.formatMessage(messages.search)} className={classNames('fa fa-times-circle', { active: hasValue })} />
</div>
</div>
);
}
}
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { injectIntl } from 'react-intl';
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
import Account from './components/account';
import Search from './components/search';
import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
const mapStateToProps = state => ({
title: state.getIn(['listEditor', 'title']),
accountIds: state.getIn(['listEditor', 'accounts', 'items']),
searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
});
const mapDispatchToProps = dispatch => ({
onInitialize: listId => dispatch(setupListEditor(listId)),
onClear: () => dispatch(clearListSuggestions()),
onReset: () => dispatch(resetListEditor()),
});
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class ListEditor extends ImmutablePureComponent {
static propTypes = {
listId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
onInitialize: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
accountIds: ImmutablePropTypes.list.isRequired,
searchAccountIds: ImmutablePropTypes.list.isRequired,
};
componentDidMount () {
const { onInitialize, listId } = this.props;
onInitialize(listId);
}
componentWillUnmount () {
const { onReset } = this.props;
onReset();
}
render () {
const { title, accountIds, searchAccountIds, onClear } = this.props;
const showSearch = searchAccountIds.size > 0;
return (
<div className='modal-root__modal list-editor'>
<h4>{title}</h4>
<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>
);
}
}
...@@ -6,10 +6,18 @@ import StatusListContainer from '../ui/containers/status_list_container'; ...@@ -6,10 +6,18 @@ import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column'; import Column from '../../components/column';
import ColumnHeader from '../../components/column_header'; import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { connectListStream } from '../../actions/streaming'; import { connectListStream } from '../../actions/streaming';
import { refreshListTimeline, expandListTimeline } from '../../actions/timelines'; import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
import { fetchList } from '../../actions/lists'; import { fetchList, deleteList } from '../../actions/lists';
import { openModal } from '../../actions/modal';
import MissingIndicator from '../../components/missing_indicator';
import LoadingIndicator from '../../components/loading_indicator';
const messages = defineMessages({
deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
});
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
list: state.getIn(['lists', props.params.id]), list: state.getIn(['lists', props.params.id]),
...@@ -17,15 +25,21 @@ const mapStateToProps = (state, props) => ({ ...@@ -17,15 +25,21 @@ const mapStateToProps = (state, props) => ({
}); });
@connect(mapStateToProps) @connect(mapStateToProps)
@injectIntl
export default class ListTimeline extends React.PureComponent { export default class ListTimeline extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
params: PropTypes.object.isRequired, params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string, columnId: PropTypes.string,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
list: ImmutablePropTypes.map, list: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
intl: PropTypes.object.isRequired,
}; };
handlePin = () => { handlePin = () => {
...@@ -35,6 +49,7 @@ export default class ListTimeline extends React.PureComponent { ...@@ -35,6 +49,7 @@ export default class ListTimeline extends React.PureComponent {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('LIST', { id: this.props.params.id })); dispatch(addColumn('LIST', { id: this.props.params.id }));
this.context.router.history.push('/');
} }
} }
...@@ -73,12 +88,49 @@ export default class ListTimeline extends React.PureComponent { ...@@ -73,12 +88,49 @@ export default class ListTimeline extends React.PureComponent {
this.props.dispatch(expandListTimeline(id)); this.props.dispatch(expandListTimeline(id));
} }
handleEditClick = () => {
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
}
handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props;
const { id } = this.props.params;
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => {
dispatch(deleteList(id));
if (!!columnId) {
dispatch(removeColumn(columnId));
} else {
this.context.router.history.push('/lists');
}
},
}));
}
render () { render () {
const { hasUnread, columnId, multiColumn, list } = this.props; const { hasUnread, columnId, multiColumn, list } = this.props;
const { id } = this.props.params; const { id } = this.props.params;
const pinned = !!columnId; const pinned = !!columnId;
const title = list ? list.get('title') : id; const title = list ? list.get('title') : id;
if (typeof list === 'undefined') {
return (
<Column>
<LoadingIndicator />
</Column>
);
} else if (list === false) {
return (
<Column>
<MissingIndicator />
</Column>
);
}
return ( return (
<Column ref={this.setRef}> <Column ref={this.setRef}>
<ColumnHeader <ColumnHeader
...@@ -90,7 +142,19 @@ export default class ListTimeline extends React.PureComponent { ...@@ -90,7 +142,19 @@ export default class ListTimeline extends React.PureComponent {
onClick={this.handleHeaderClick} onClick={this.handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
/> >
<div className='column-header__links'>
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleEditClick}>
<i className='fa fa-pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
</button>
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={this.handleDeleteClick}>
<i className='fa fa-trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
</button>
</div>
<hr />
</ColumnHeader>
<StatusListContainer <StatusListContainer
trackScroll={!pinned} trackScroll={!pinned}
......
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
const messages = defineMessages({
label: { id: 'lists.new.title_placeholder', defaultMessage: 'New list title' },
title: { id: 'lists.new.create', defaultMessage: 'Add list' },
});
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'title']),
disabled: state.getIn(['listEditor', 'isSubmitting']),
});
const mapDispatchToProps = dispatch => ({
onChange: value => dispatch(changeListEditorTitle(value)),
onSubmit: () => dispatch(submitListEditor(true)),
});
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class NewListForm extends React.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);
}
handleKeyUp = e => {
if (e.keyCode === 13) {
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 (
<div className='column-inline-form'>
<label>
<span style={{ display: 'none' }}>{label}</span>
<input
className='setting-text'
value={value}
disabled={disabled}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
placeholder={label}
/>
</label>
<IconButton
disabled={disabled}
icon='plus'
title={title}
onClick={this.handleClick}
/>
</div>
);
}
}
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import { fetchLists } from '../../actions/lists';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import NewListForm from './components/new_list_form';
import { createSelector } from 'reselect';
const messages = defineMessages({
heading: { id: 'column.lists', defaultMessage: 'Lists' },
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
});
const getOrderedLists = createSelector([state => state.get('lists')], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title')));
});
const mapStateToProps = state => ({
lists: getOrderedLists(state),
});
@connect(mapStateToProps)
@injectIntl
export default class Lists extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
lists: ImmutablePropTypes.list,
intl: PropTypes.object.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchLists());
}
render () {
const { intl, lists } = this.props;
if (!lists) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
return (
<Column icon='bars' heading={intl.formatMessage(messages.heading)}>
<ColumnBackButtonSlim />
<NewListForm />
<div className='scrollable'>
<ColumnSubheading text={intl.formatMessage(messages.subheading)} />
{lists.map(list =>
<ColumnLink key={list.get('id')} to={`/timelines/list/${list.get('id')}`} icon='bars' text={list.get('title')} />
)}
</div>
</Column>
);
}
}
...@@ -13,6 +13,7 @@ import { ...@@ -13,6 +13,7 @@ import {
MuteModal, MuteModal,
ReportModal, ReportModal,
EmbedModal, EmbedModal,
ListEditor,
} from '../../../features/ui/util/async-components'; } from '../../../features/ui/util/async-components';
const MODAL_COMPONENTS = { const MODAL_COMPONENTS = {
...@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = { ...@@ -25,6 +26,7 @@ const MODAL_COMPONENTS = {
'REPORT': ReportModal, 'REPORT': ReportModal,
'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
'EMBED': EmbedModal, 'EMBED': EmbedModal,
'LIST_EDITOR': ListEditor,
}; };
export default class ModalRoot extends React.PureComponent { export default class ModalRoot extends React.PureComponent {
......
...@@ -38,6 +38,7 @@ import { ...@@ -38,6 +38,7 @@ import {
Blocks, Blocks,
Mutes, Mutes,
PinnedStatuses, PinnedStatuses,
Lists,
} from './util/async-components'; } from './util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { me } from '../../initial_state'; import { me } from '../../initial_state';
...@@ -404,6 +405,7 @@ export default class UI extends React.Component { ...@@ -404,6 +405,7 @@ export default class UI extends React.Component {
<WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
<WrappedRoute path='/blocks' component={Blocks} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} />
<WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} />
<WrappedRoute path='/lists' component={Lists} content={children} />
<WrappedRoute component={GenericNotFound} content={children} /> <WrappedRoute component={GenericNotFound} content={children} />
</WrappedSwitch> </WrappedSwitch>
......
...@@ -30,6 +30,10 @@ export function ListTimeline () { ...@@ -30,6 +30,10 @@ export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline'); return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
} }
export function Lists () {
return import(/* webpackChunkName: "features/lists" */'../../lists');
}
export function Status () { export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status'); return import(/* webpackChunkName: "features/status" */'../../status');
} }
...@@ -113,3 +117,7 @@ export function Video () { ...@@ -113,3 +117,7 @@ export function Video () {
export function EmbedModal () { export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal'); return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
} }
export function ListEditor () {
return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
}
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
"column.favourites": "收藏过的嘟文", "column.favourites": "收藏过的嘟文",
"column.follow_requests": "关注请求", "column.follow_requests": "关注请求",
"column.home": "主页", "column.home": "主页",
"column.lists": "列表",
"column.mutes": "被隐藏的用户", "column.mutes": "被隐藏的用户",
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "置顶嘟文", "column.pins": "置顶嘟文",
...@@ -62,6 +63,8 @@ ...@@ -62,6 +63,8 @@
"confirmations.block.message": "想好了,真的要屏蔽 {name}?", "confirmations.block.message": "想好了,真的要屏蔽 {name}?",
"confirmations.delete.confirm": "删除", "confirmations.delete.confirm": "删除",
"confirmations.delete.message": "想好了,真的要删除这条嘟文?", "confirmations.delete.message": "想好了,真的要删除这条嘟文?",
"confirmations.delete_list.confirm": "删除",
"confirmations.delete_list.message": "你确定要永久删除这个列表吗?",
"confirmations.domain_block.confirm": "隐藏整个网站的内容", "confirmations.domain_block.confirm": "隐藏整个网站的内容",
"confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。", "confirmations.domain_block.message": "你真的真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就应该能满足你的需要了。",
"confirmations.mute.confirm": "隐藏", "confirmations.mute.confirm": "隐藏",
...@@ -124,6 +127,14 @@ ...@@ -124,6 +127,14 @@
"lightbox.close": "关闭", "lightbox.close": "关闭",
"lightbox.next": "下一步", "lightbox.next": "下一步",
"lightbox.previous": "上一步", "lightbox.previous": "上一步",
"lists.account.add": "添加到列表",
"lists.account.remove": "从列表中删除",
"lists.delete": "删除列表",
"lists.edit": "编辑列表",
"lists.new.create": "新建列表",
"lists.new.title_placeholder": "新列表的标题",
"lists.search": "搜索你关注的人",
"lists.subheading": "你的列表",
"loading_indicator.label": "加载中……", "loading_indicator.label": "加载中……",
"media_gallery.toggle_visible": "切换显示/隐藏", "media_gallery.toggle_visible": "切换显示/隐藏",
"missing_indicator.label": "找不到内容", "missing_indicator.label": "找不到内容",
...@@ -134,6 +145,7 @@ ...@@ -134,6 +145,7 @@
"navigation_bar.favourites": "收藏的内容", "navigation_bar.favourites": "收藏的内容",
"navigation_bar.follow_requests": "关注请求", "navigation_bar.follow_requests": "关注请求",
"navigation_bar.info": "关于本站", "navigation_bar.info": "关于本站",
"navigation_bar.lists": "列表",
"navigation_bar.keyboard_shortcuts": "快捷键列表", "navigation_bar.keyboard_shortcuts": "快捷键列表",
"navigation_bar.logout": "注销", "navigation_bar.logout": "注销",
"navigation_bar.mutes": "被隐藏的用户", "navigation_bar.mutes": "被隐藏的用户",
......
...@@ -43,6 +43,10 @@ import { ...@@ -43,6 +43,10 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
LIST_ACCOUNTS_FETCH_SUCCESS,
LIST_EDITOR_SUGGESTIONS_READY,
} from '../actions/lists';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
import emojify from '../features/emoji/emoji'; import emojify from '../features/emoji/emoji';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
...@@ -115,6 +119,8 @@ export default function accounts(state = initialState, action) { ...@@ -115,6 +119,8 @@ export default function accounts(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS: case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS: case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS: case MUTES_EXPAND_SUCCESS:
case LIST_ACCOUNTS_FETCH_SUCCESS:
case LIST_EDITOR_SUGGESTIONS_READY:
return action.accounts ? normalizeAccounts(state, action.accounts) : state; return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
......
...@@ -45,6 +45,10 @@ import { ...@@ -45,6 +45,10 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
LIST_ACCOUNTS_FETCH_SUCCESS,
LIST_EDITOR_SUGGESTIONS_READY,
} from '../actions/lists';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
...@@ -106,6 +110,8 @@ export default function accountsCounters(state = initialState, action) { ...@@ -106,6 +110,8 @@ export default function accountsCounters(state = initialState, action) {
case BLOCKS_EXPAND_SUCCESS: case BLOCKS_EXPAND_SUCCESS:
case MUTES_FETCH_SUCCESS: case MUTES_FETCH_SUCCESS:
case MUTES_EXPAND_SUCCESS: case MUTES_EXPAND_SUCCESS:
case LIST_ACCOUNTS_FETCH_SUCCESS:
case LIST_EDITOR_SUGGESTIONS_READY:
return action.accounts ? normalizeAccounts(state, action.accounts) : state; return action.accounts ? normalizeAccounts(state, action.accounts) : state;
case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_REFRESH_SUCCESS:
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
......
...@@ -23,6 +23,7 @@ import notifications from './notifications'; ...@@ -23,6 +23,7 @@ import notifications from './notifications';
import height_cache from './height_cache'; import height_cache from './height_cache';
import custom_emojis from './custom_emojis'; import custom_emojis from './custom_emojis';
import lists from './lists'; import lists from './lists';
import listEditor from './list_editor';
const reducers = { const reducers = {
timelines, timelines,
...@@ -49,6 +50,7 @@ const reducers = { ...@@ -49,6 +50,7 @@ const reducers = {
height_cache, height_cache,
custom_emojis, custom_emojis,
lists, lists,
listEditor,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import {
LIST_CREATE_REQUEST,
LIST_CREATE_FAIL,
LIST_CREATE_SUCCESS,
LIST_UPDATE_REQUEST,
LIST_UPDATE_FAIL,
LIST_UPDATE_SUCCESS,
LIST_EDITOR_RESET,
LIST_EDITOR_SETUP,
LIST_EDITOR_TITLE_CHANGE,
LIST_ACCOUNTS_FETCH_REQUEST,
LIST_ACCOUNTS_FETCH_SUCCESS,
LIST_ACCOUNTS_FETCH_FAIL,
LIST_EDITOR_SUGGESTIONS_READY,
LIST_EDITOR_SUGGESTIONS_CLEAR,
LIST_EDITOR_SUGGESTIONS_CHANGE,
LIST_EDITOR_ADD_SUCCESS,
LIST_EDITOR_REMOVE_SUCCESS,
} from '../actions/lists';
const initialState = ImmutableMap({
listId: null,
isSubmitting: false,
title: '',
accounts: ImmutableMap({
items: ImmutableList(),
loaded: false,
isLoading: false,
}),
suggestions: ImmutableMap({
value: '',
items: ImmutableList(),
}),
});
export default function listEditorReducer(state = initialState, action) {
switch(action.type) {
case LIST_EDITOR_RESET:
return initialState;
case LIST_EDITOR_SETUP:
return state.withMutations(map => {
map.set('listId', action.list.get('id'));
map.set('title', action.list.get('title'));
map.set('isSubmitting', false);
});
case LIST_EDITOR_TITLE_CHANGE:
return state.set('title', action.value);
case LIST_CREATE_REQUEST:
case LIST_UPDATE_REQUEST:
return state.set('isSubmitting', true);
case LIST_CREATE_FAIL:
case LIST_UPDATE_FAIL:
return state.set('isSubmitting', false);
case LIST_CREATE_SUCCESS:
case LIST_UPDATE_SUCCESS:
return state.withMutations(map => {
map.set('isSubmitting', false);
map.set('listId', action.list.id);
});
case LIST_ACCOUNTS_FETCH_REQUEST:
return state.setIn(['accounts', 'isLoading'], true);
case LIST_ACCOUNTS_FETCH_FAIL:
return state.setIn(['accounts', 'isLoading'], false);
case LIST_ACCOUNTS_FETCH_SUCCESS:
return state.update('accounts', accounts => accounts.withMutations(map => {
map.set('isLoading', false);
map.set('loaded', true);
map.set('items', ImmutableList(action.accounts.map(item => item.id)));
}));
case LIST_EDITOR_SUGGESTIONS_CHANGE:
return state.setIn(['suggestions', 'value'], action.value);
case LIST_EDITOR_SUGGESTIONS_READY:
return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id)));
case LIST_EDITOR_SUGGESTIONS_CLEAR:
return state.update('suggestions', suggestions => suggestions.withMutations(map => {
map.set('items', ImmutableList());
map.set('value', '');
}));
case LIST_EDITOR_ADD_SUCCESS:
return state.updateIn(['accounts', 'items'], list => list.unshift(action.accountId));
case LIST_EDITOR_REMOVE_SUCCESS:
return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.accountId));
default:
return state;
}
};
import { LIST_FETCH_SUCCESS } from '../actions/lists'; import {
LIST_FETCH_SUCCESS,
LIST_FETCH_FAIL,
LISTS_FETCH_SUCCESS,
LIST_CREATE_SUCCESS,
LIST_UPDATE_SUCCESS,
LIST_DELETE_SUCCESS,
} from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
const initialState = ImmutableMap(); const initialState = ImmutableMap();
const normalizeList = (state, list) => state.set(list.id, fromJS(list)); const normalizeList = (state, list) => state.set(list.id, fromJS(list));
const normalizeLists = (state, lists) => {
lists.forEach(list => {
state = normalizeList(state, list);
});
return state;
};
export default function lists(state = initialState, action) { export default function lists(state = initialState, action) {
switch(action.type) { switch(action.type) {
case LIST_FETCH_SUCCESS: case LIST_FETCH_SUCCESS:
case LIST_CREATE_SUCCESS:
case LIST_UPDATE_SUCCESS:
return normalizeList(state, action.list); return normalizeList(state, action.list);
case LISTS_FETCH_SUCCESS:
return normalizeLists(state, action.lists);
case LIST_DELETE_SUCCESS:
case LIST_FETCH_FAIL:
return state.set(action.id, false);
default: default:
return state; return state;
} }
......
...@@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings'; ...@@ -2,6 +2,7 @@ import { SETTING_CHANGE, SETTING_SAVE } from '../actions/settings';
import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns';
import { STORE_HYDRATE } from '../actions/store'; import { STORE_HYDRATE } from '../actions/store';
import { EMOJI_USE } from '../actions/emojis'; import { EMOJI_USE } from '../actions/emojis';
import { LIST_DELETE_SUCCESS, LIST_FETCH_FAIL } from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
import uuid from '../uuid'; import uuid from '../uuid';
...@@ -84,6 +85,8 @@ const moveColumn = (state, uuid, direction) => { ...@@ -84,6 +85,8 @@ const moveColumn = (state, uuid, direction) => {
const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false); const updateFrequentEmojis = (state, emoji) => state.update('frequentlyUsedEmojis', ImmutableMap(), map => map.update(emoji.id, 0, count => count + 1)).set('saved', false);
const filterDeadListColumns = (state, listId) => state.update('columns', columns => columns.filterNot(column => column.get('id') === 'LIST' && column.get('params').get('id') === listId));
export default function settings(state = initialState, action) { export default function settings(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
...@@ -106,6 +109,10 @@ export default function settings(state = initialState, action) { ...@@ -106,6 +109,10 @@ export default function settings(state = initialState, action) {
return updateFrequentEmojis(state, action.emoji); return updateFrequentEmojis(state, action.emoji);
case SETTING_SAVE: case SETTING_SAVE:
return state.set('saved', true); return state.set('saved', true);
case LIST_FETCH_FAIL:
return action.error.response.status === 404 ? filterDeadListColumns(state, action.id) : state;
case LIST_DELETE_SUCCESS:
return filterDeadListColumns(state, action.id);
default: default:
return state; return state;
} }
......
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Terminez d'abord l'édition de ce message.
Veuillez vous inscrire ou vous pour commenter