Display list column (#5750)

This commit is contained in:
Eugen Rochko 2017-11-25 00:35:37 +01:00 committed by GitHub
parent 269a445c0b
commit 31ac5f0e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 166 additions and 1 deletions

View File

@ -0,0 +1,28 @@
import api from '../api';
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
export const fetchList = id => (dispatch, getState) => {
dispatch(fetchListRequest(id));
api(getState).get(`/api/v1/lists/${id}`)
.then(({ data }) => dispatch(fetchListSuccess(data)))
.catch(err => dispatch(fetchListFail(err)));
};
export const fetchListRequest = id => ({
type: LIST_FETCH_REQUEST,
id,
});
export const fetchListSuccess = list => ({
type: LIST_FETCH_SUCCESS,
list,
});
export const fetchListFail = error => ({
type: LIST_FETCH_FAIL,
error,
});

View File

@ -51,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
export const connectPublicStream = () => connectTimelineStream('public', 'public'); export const connectPublicStream = () => connectTimelineStream('public', 'public');
export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View File

@ -118,6 +118,7 @@ export const refreshCommunityTimeline = () => refreshTimeline('community', '/
export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function refreshTimelineFail(timeline, error, skipLoading) { export function refreshTimelineFail(timeline, error, skipLoading) {
return { return {
@ -158,6 +159,7 @@ export const expandCommunityTimeline = () => expandTimeline('community', '/ap
export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`);
export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
export function expandTimelineRequest(timeline) { export function expandTimelineRequest(timeline) {
return { return {

View File

@ -0,0 +1,106 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
import { connectListStream } from '../../actions/streaming';
import { refreshListTimeline, expandListTimeline } from '../../actions/timelines';
import { fetchList } from '../../actions/lists';
const mapStateToProps = (state, props) => ({
list: state.getIn(['lists', props.params.id]),
hasUnread: state.getIn(['timelines', `list:${props.params.id}`, 'unread']) > 0,
});
@connect(mapStateToProps)
export default class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
list: ImmutablePropTypes.map,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('LIST', { id: this.props.params.id }));
}
}
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(fetchList(id));
dispatch(refreshListTimeline(id));
this.disconnect = dispatch(connectListStream(id));
}
componentWillUnmount () {
if (this.disconnect) {
this.disconnect();
this.disconnect = null;
}
}
setRef = c => {
this.column = c;
}
handleLoadMore = () => {
const { id } = this.props.params;
this.props.dispatch(expandListTimeline(id));
}
render () {
const { hasUnread, columnId, multiColumn, list } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;
const title = list ? list.get('title') : id;
return (
<Column ref={this.setRef}>
<ColumnHeader
icon='bars'
active={hasUnread}
title={title}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
/>
<StatusListContainer
trackScroll={!pinned}
scrollKey={`list_timeline-${columnId}`}
timelineId={`list:${id}`}
loadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet.' />}
/>
</Column>
);
}
}

View File

@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading'; import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading'; import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error'; import BundleColumnError from './bundle_column_error';
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
import detectPassiveEvents from 'detect-passive-events'; import detectPassiveEvents from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
@ -24,6 +24,7 @@ const componentMap = {
'COMMUNITY': CommunityTimeline, 'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'FAVOURITES': FavouritedStatuses, 'FAVOURITES': FavouritedStatuses,
'LIST': ListTimeline,
}; };
@component => injectIntl(component, { withRef: true }) @component => injectIntl(component, { withRef: true })

View File

@ -33,6 +33,7 @@ import {
FollowRequests, FollowRequests,
GenericNotFound, GenericNotFound,
FavouritedStatuses, FavouritedStatuses,
ListTimeline,
Blocks, Blocks,
Mutes, Mutes,
PinnedStatuses, PinnedStatuses,
@ -372,6 +373,7 @@ export default class UI extends React.Component {
<WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} /> <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
<WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} /> <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
<WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} /> <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/timelines/list/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />

View File

@ -26,6 +26,10 @@ export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
} }
export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
}
export function Status () { export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status'); return import(/* webpackChunkName: "features/status" */'../../status');
} }

View File

@ -22,6 +22,7 @@ import media_attachments from './media_attachments';
import notifications from './notifications'; 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';
const reducers = { const reducers = {
timelines, timelines,
@ -47,6 +48,7 @@ const reducers = {
notifications, notifications,
height_cache, height_cache,
custom_emojis, custom_emojis,
lists,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

View File

@ -0,0 +1,15 @@
import { LIST_FETCH_SUCCESS } from '../actions/lists';
import { Map as ImmutableMap, fromJS } from 'immutable';
const initialState = ImmutableMap();
const normalizeList = (state, list) => state.set(list.id, fromJS(list));
export default function lists(state = initialState, action) {
switch(action.type) {
case LIST_FETCH_SUCCESS:
return normalizeList(state, action.list);
default:
return state;
}
};

View File

@ -2,4 +2,8 @@
class REST::ListSerializer < ActiveModel::Serializer class REST::ListSerializer < ActiveModel::Serializer
attributes :id, :title attributes :id, :title
def id
object.id.to_s
end
end end