Display list column (#5750)
This commit is contained in:
parent
269a445c0b
commit
31ac5f0e00
10 changed files with 166 additions and 1 deletions
28
app/javascript/mastodon/actions/lists.js
Normal file
28
app/javascript/mastodon/actions/lists.js
Normal 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,
|
||||||
|
});
|
|
@ -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}`);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
106
app/javascript/mastodon/features/list_timeline/index.js
Normal file
106
app/javascript/mastodon/features/list_timeline/index.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 })
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
15
app/javascript/mastodon/reducers/lists.js
Normal file
15
app/javascript/mastodon/reducers/lists.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue