diff --git a/app/javascript/mastodon/actions/circles.js b/app/javascript/mastodon/actions/circles.js
index 6a52e541c9a440..a497b27d5db592 100644
--- a/app/javascript/mastodon/actions/circles.js
+++ b/app/javascript/mastodon/actions/circles.js
@@ -1,7 +1,7 @@
-import api from '../api';
+import api, { getLinks } from '../api';
import { showAlertForError } from './alerts';
-import { importFetchedAccounts } from './importer';
+import { importFetchedAccounts, importFetchedStatuses } from './importer';
export const CIRCLE_FETCH_REQUEST = 'CIRCLE_FETCH_REQUEST';
export const CIRCLE_FETCH_SUCCESS = 'CIRCLE_FETCH_SUCCESS';
@@ -50,6 +50,14 @@ export const CIRCLE_ADDER_CIRCLES_FETCH_REQUEST = 'CIRCLE_ADDER_CIRCLES_FETCH_RE
export const CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS = 'CIRCLE_ADDER_CIRCLES_FETCH_SUCCESS';
export const CIRCLE_ADDER_CIRCLES_FETCH_FAIL = 'CIRCLE_ADDER_CIRCLES_FETCH_FAIL';
+export const CIRCLE_STATUSES_FETCH_REQUEST = 'CIRCLE_STATUSES_FETCH_REQUEST';
+export const CIRCLE_STATUSES_FETCH_SUCCESS = 'CIRCLE_STATUSES_FETCH_SUCCESS';
+export const CIRCLE_STATUSES_FETCH_FAIL = 'CIRCLE_STATUSES_FETCH_FAIL';
+
+export const CIRCLE_STATUSES_EXPAND_REQUEST = 'CIRCLE_STATUSES_EXPAND_REQUEST';
+export const CIRCLE_STATUSES_EXPAND_SUCCESS = 'CIRCLE_STATUSES_EXPAND_SUCCESS';
+export const CIRCLE_STATUSES_EXPAND_FAIL = 'CIRCLE_STATUSES_EXPAND_FAIL';
+
export const fetchCircle = id => (dispatch, getState) => {
if (getState().getIn(['circles', id])) {
return;
@@ -370,3 +378,89 @@ export const removeFromCircleAdder = circleId => (dispatch, getState) => {
dispatch(removeFromCircle(circleId, getState().getIn(['circleAdder', 'accountId'])));
};
+export function fetchCircleStatuses(circleId) {
+ return (dispatch, getState) => {
+ if (getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
+ return;
+ }
+
+ dispatch(fetchCircleStatusesRequest(circleId));
+
+ api(getState).get(`/api/v1/circles/${circleId}/statuses`).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(fetchCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(fetchCircleStatusesFail(circleId, error));
+ });
+ };
+}
+
+export function fetchCircleStatusesRequest(id) {
+ return {
+ type: CIRCLE_STATUSES_FETCH_REQUEST,
+ id,
+ };
+}
+
+export function fetchCircleStatusesSuccess(id, statuses, next) {
+ return {
+ type: CIRCLE_STATUSES_FETCH_SUCCESS,
+ id,
+ statuses,
+ next,
+ };
+}
+
+export function fetchCircleStatusesFail(id, error) {
+ return {
+ type: CIRCLE_STATUSES_FETCH_FAIL,
+ id,
+ error,
+ };
+}
+
+export function expandCircleStatuses(circleId) {
+ return (dispatch, getState) => {
+ const url = getState().getIn(['circles', circleId, 'statuses', 'next'], null);
+
+ if (url === null || getState().getIn(['circles', circleId, 'statuses', 'isLoading'])) {
+ return;
+ }
+
+ dispatch(expandCircleStatusesRequest(circleId));
+
+ api(getState).get(url).then(response => {
+ const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(expandCircleStatusesSuccess(circleId, response.data, next ? next.uri : null));
+ }).catch(error => {
+ dispatch(expandCircleStatusesFail(circleId, error));
+ });
+ };
+}
+
+export function expandCircleStatusesRequest(id) {
+ return {
+ type: CIRCLE_STATUSES_EXPAND_REQUEST,
+ id,
+ };
+}
+
+export function expandCircleStatusesSuccess(id, statuses, next) {
+ return {
+ type: CIRCLE_STATUSES_EXPAND_SUCCESS,
+ id,
+ statuses,
+ next,
+ };
+}
+
+export function expandCircleStatusesFail(id, error) {
+ return {
+ type: CIRCLE_STATUSES_EXPAND_FAIL,
+ id,
+ error,
+ };
+}
+
diff --git a/app/javascript/mastodon/features/circle_statuses/index.jsx b/app/javascript/mastodon/features/circle_statuses/index.jsx
new file mode 100644
index 00000000000000..4ec0b5da978521
--- /dev/null
+++ b/app/javascript/mastodon/features/circle_statuses/index.jsx
@@ -0,0 +1,179 @@
+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 { debounce } from 'lodash';
+
+import { deleteCircle, expandCircleStatuses, fetchCircle, fetchCircleStatuses , setupCircleEditor } from 'mastodon/actions/circles';
+import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
+import { openModal } from 'mastodon/actions/modal';
+import ColumnHeader from 'mastodon/components/column_header';
+import { Icon } from 'mastodon/components/icon';
+import { LoadingIndicator } from 'mastodon/components/loading_indicator';
+import StatusList from 'mastodon/components/status_list';
+import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
+import Column from 'mastodon/features/ui/components/column';
+import { getCircleStatusList } from 'mastodon/selectors';
+
+
+const messages = defineMessages({
+ deleteMessage: { id: 'confirmations.delete_circle.message', defaultMessage: 'Are you sure you want to permanently delete this circle?' },
+ deleteConfirm: { id: 'confirmations.delete_circle.confirm', defaultMessage: 'Delete' },
+ heading: { id: 'column.circles', defaultMessage: 'Circles' },
+});
+
+const mapStateToProps = (state, { params }) => ({
+ circle: state.getIn(['circles', params.id]),
+ statusIds: getCircleStatusList(state, params.id),
+ isLoading: state.getIn(['circles', params.id, 'isLoading'], true),
+ isEditing: state.getIn(['circleEditor', 'circleId']) === params.id,
+ hasMore: !!state.getIn(['circles', params.id, 'next']),
+});
+
+class CircleStatuses extends ImmutablePureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ statusIds: ImmutablePropTypes.list.isRequired,
+ circle: PropTypes.oneOfType([ImmutablePropTypes.map, PropTypes.bool]),
+ intl: PropTypes.object.isRequired,
+ columnId: PropTypes.string,
+ multiColumn: PropTypes.bool,
+ hasMore: PropTypes.bool,
+ isLoading: PropTypes.bool,
+ };
+
+ UNSAFE_componentWillMount () {
+ this.props.dispatch(fetchCircle(this.props.params.id));
+ this.props.dispatch(fetchCircleStatuses(this.props.params.id));
+ }
+
+ handlePin = () => {
+ const { columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('CIRCLE_STATUSES', { 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();
+ };
+
+ handleEditClick = () => {
+ this.props.dispatch(setupCircleEditor(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(deleteCircle(id));
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ this.context.router.history.push('/circles');
+ }
+ },
+ },
+ }));
+ };
+
+ setRef = c => {
+ this.column = c;
+ };
+
+ handleLoadMore = debounce(() => {
+ this.props.dispatch(expandCircleStatuses());
+ }, 300, { leading: true });
+
+ render () {
+ const { intl, circle, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
+ const pinned = !!columnId;
+
+ if (typeof circle === 'undefined') {
+ return (
+
+
+
+
+
+ );
+ } else if (circle === false) {
+ return (
+
+ );
+ }
+
+ const emptyMessage = ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {intl.formatMessage(messages.heading)}
+
+
+
+ );
+ }
+
+}
+
+export default connect(mapStateToProps)(injectIntl(CircleStatuses));
diff --git a/app/javascript/mastodon/reducers/circles.js b/app/javascript/mastodon/reducers/circles.js
index 805d7f186addf5..c07e9406b92203 100644
--- a/app/javascript/mastodon/reducers/circles.js
+++ b/app/javascript/mastodon/reducers/circles.js
@@ -1,4 +1,4 @@
-import { List as ImmutableList, fromJS } from 'immutable';
+import { List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
import {
CIRCLE_FETCH_SUCCESS,
@@ -7,6 +7,12 @@ import {
CIRCLE_CREATE_SUCCESS,
CIRCLE_UPDATE_SUCCESS,
CIRCLE_DELETE_SUCCESS,
+ CIRCLE_STATUSES_FETCH_REQUEST,
+ CIRCLE_STATUSES_FETCH_SUCCESS,
+ CIRCLE_STATUSES_FETCH_FAIL,
+ CIRCLE_STATUSES_EXPAND_REQUEST,
+ CIRCLE_STATUSES_EXPAND_SUCCESS,
+ CIRCLE_STATUSES_EXPAND_FAIL,
} from '../actions/circles';
const initialState = ImmutableList();
@@ -21,6 +27,31 @@ const normalizeLists = (state, circles) => {
return state;
};
+const normalizeCircleStatuses = (state, circleId, statuses, next) => {
+ return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
+ map.set('next', next);
+ map.set('loaded', true);
+ map.set('isLoading', false);
+ map.set('items', ImmutableOrderedSet(statuses.map(item => item.id)));
+ }));
+};
+
+const appendToCircleStatuses = (state, circleId, statuses, next) => {
+ return appendToCircleStatusesById(state, circleId, statuses.map(item => item.id), next);
+};
+
+const appendToCircleStatusesById = (state, circleId, statuses, next) => {
+ return state.updateIn([circleId, 'statuses'], listMap => listMap.withMutations(map => {
+ if (typeof next !== 'undefined') {
+ map.set('next', next);
+ }
+ map.set('isLoading', false);
+ if (map.get('items')) {
+ map.set('items', map.get('items').union(statuses));
+ }
+ }));
+};
+
export default function circles(state = initialState, action) {
switch(action.type) {
case CIRCLE_FETCH_SUCCESS:
@@ -32,6 +63,16 @@ export default function circles(state = initialState, action) {
case CIRCLE_DELETE_SUCCESS:
case CIRCLE_FETCH_FAIL:
return state.set(action.id, false);
+ case CIRCLE_STATUSES_FETCH_REQUEST:
+ case CIRCLE_STATUSES_EXPAND_REQUEST:
+ return state.setIn([action.id, 'statuses', 'isLoading'], true);
+ case CIRCLE_STATUSES_FETCH_FAIL:
+ case CIRCLE_STATUSES_EXPAND_FAIL:
+ return state.setIn([action.id, 'statuses', 'isLoading'], false);
+ case CIRCLE_STATUSES_FETCH_SUCCESS:
+ return normalizeCircleStatuses(state, action.id, action.statuses, action.next);
+ case CIRCLE_STATUSES_EXPAND_SUCCESS:
+ return appendToCircleStatuses(state, action.id, action.statuses, action.next);
default:
return state;
}
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index 6d5adbeb48f5a7..f6398e93240600 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -135,3 +135,7 @@ export const getStatusList = createSelector([
export const getBookmarkCategoryStatusList = createSelector([
(state, bookmarkCategoryId) => state.getIn(['bookmark_categories', bookmarkCategoryId, 'items']),
], (items) => items ? items.toList() : ImmutableList());
+
+export const getCircleStatusList = createSelector([
+ (state, circleId) => state.getIn(['circles', circleId, 'statuses', 'items']),
+], (items) => items ? items.toList() : ImmutableList());