about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/actions/lists.js28
-rw-r--r--app/javascript/mastodon/actions/streaming.js1
-rw-r--r--app/javascript/mastodon/actions/timelines.js2
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js106
-rw-r--r--app/javascript/mastodon/features/ui/components/columns_area.js3
-rw-r--r--app/javascript/mastodon/features/ui/index.js2
-rw-r--r--app/javascript/mastodon/features/ui/util/async-components.js4
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/lists.js15
-rw-r--r--app/serializers/rest/list_serializer.rb4
10 files changed, 166 insertions, 1 deletions
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
new file mode 100644
index 000000000..332e42166
--- /dev/null
+++ b/app/javascript/mastodon/actions/lists.js
@@ -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,
+});
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index dcce048ca..c22152edd 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -51,3 +51,4 @@ export const connectCommunityStream = () => connectTimelineStream('community', '
 export const connectMediaStream = () => connectTimelineStream('community', 'public:local');
 export const connectPublicStream = () => connectTimelineStream('public', 'public');
 export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`);
+export const connectListStream = (id) => connectTimelineStream(`list:${id}`, `list&list=${id}`);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 09abe2702..f8843d1d9 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -118,6 +118,7 @@ export const refreshCommunityTimeline    = () => refreshTimeline('community', '/
 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 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) {
   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 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 expandListTimeline         = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
 
 export function expandTimelineRequest(timeline) {
   return {
diff --git a/app/javascript/mastodon/features/list_timeline/index.js b/app/javascript/mastodon/features/list_timeline/index.js
new file mode 100644
index 000000000..71f6e36a8
--- /dev/null
+++ b/app/javascript/mastodon/features/list_timeline/index.js
@@ -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>
+    );
+  }
+
+}
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 5610095b9..93ed9e605 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -11,7 +11,7 @@ import BundleContainer from '../containers/bundle_container';
 import ColumnLoading from './column_loading';
 import DrawerLoading from './drawer_loading';
 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 { scrollRight } from '../../../scroll';
@@ -24,6 +24,7 @@ const componentMap = {
   'COMMUNITY': CommunityTimeline,
   'HASHTAG': HashtagTimeline,
   'FAVOURITES': FavouritedStatuses,
+  'LIST': ListTimeline,
 };
 
 @component => injectIntl(component, { withRef: true })
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index f28b37099..57289f519 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -33,6 +33,7 @@ import {
   FollowRequests,
   GenericNotFound,
   FavouritedStatuses,
+  ListTimeline,
   Blocks,
   Mutes,
   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/local' component={CommunityTimeline} 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='/favourites' component={FavouritedStatuses} content={children} />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 39663d5ca..ec1630ed6 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -26,6 +26,10 @@ export function HashtagTimeline () {
   return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
 }
 
+export function ListTimeline () {
+  return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
+}
+
 export function Status () {
   return import(/* webpackChunkName: "features/status" */'../../status');
 }
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 17c870351..425a2acdd 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -22,6 +22,7 @@ import media_attachments from './media_attachments';
 import notifications from './notifications';
 import height_cache from './height_cache';
 import custom_emojis from './custom_emojis';
+import lists from './lists';
 
 const reducers = {
   timelines,
@@ -47,6 +48,7 @@ const reducers = {
   notifications,
   height_cache,
   custom_emojis,
+  lists,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/lists.js b/app/javascript/mastodon/reducers/lists.js
new file mode 100644
index 000000000..3e3908869
--- /dev/null
+++ b/app/javascript/mastodon/reducers/lists.js
@@ -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;
+  }
+};
diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb
index c0150888e..977da7439 100644
--- a/app/serializers/rest/list_serializer.rb
+++ b/app/serializers/rest/list_serializer.rb
@@ -2,4 +2,8 @@
 
 class REST::ListSerializer < ActiveModel::Serializer
   attributes :id, :title
+
+  def id
+    object.id.to_s
+  end
 end