diff --git a/app/javascript/mastodon/components/timeline.js b/app/javascript/mastodon/components/timeline.js
new file mode 100644
index 0000000000..192f246f5c
--- /dev/null
+++ b/app/javascript/mastodon/components/timeline.js
@@ -0,0 +1,179 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import StatusListContainer from '../features/ui/containers/status_list_container';
+import Column from './column';
+import ColumnHeader from './column_header';
+import {
+ updateTimeline,
+ deleteFromTimelines,
+ connectTimeline,
+ disconnectTimeline,
+} from '../actions/timelines';
+import { addColumn, removeColumn, moveColumn } from '../actions/columns';
+import createStream from '../stream';
+
+const mapStateToProps = (state, ownprops) => ({
+ streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
+ accessToken: state.getIn(['meta', 'access_token']),
+ hasUnread: state.getIn(['timelines', ownprops.timelineId, 'unread']) > 0,
+});
+
+@connect(mapStateToProps)
+export default class Timeline extends React.PureComponent {
+
+ static propTypes = {
+ dispatch: PropTypes.func.isRequired,
+ streamingAPIBaseURL: PropTypes.string.isRequired,
+ accessToken: PropTypes.string.isRequired,
+ expand: PropTypes.func.isRequired,
+ refresh: PropTypes.func,
+ streamId: PropTypes.string,
+ hasUnread: PropTypes.bool,
+ columnName: PropTypes.string.isRequired,
+ columnProps: PropTypes.object,
+ columnId: PropTypes.string,
+ multiColumn: PropTypes.bool,
+ emptyMessage: PropTypes.oneOfType([
+ PropTypes.element,
+ PropTypes.string,
+ ]),
+ icon: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ settings: PropTypes.element,
+ scrollName: PropTypes.string.isRequired,
+ timelineId: PropTypes.string.isRequired,
+ };
+
+ handlePin = () => {
+ const { columnName, columnProps, columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn(columnName, columnProps || {}));
+ }
+ }
+
+ handleMove = (dir) => {
+ const { columnId, dispatch } = this.props;
+ dispatch(moveColumn(columnId, dir));
+ }
+
+ handleHeaderClick = () => {
+ this.column.scrollTop();
+ }
+
+ setRef = c => {
+ this.column = c;
+ }
+
+ handleLoadMore = () => {
+ this.props.dispatch(this.props.expand());
+ }
+
+ _subscribe (dispatch, streamId, timelineId) {
+ const { streamingAPIBaseURL, accessToken } = this.props;
+
+ if (!streamId || !timelineId) return;
+
+ this.subscription = createStream(streamingAPIBaseURL, accessToken, streamId, {
+
+ connected () {
+ dispatch(connectTimeline(timelineId));
+ },
+
+ reconnected () {
+ dispatch(connectTimeline(timelineId));
+ },
+
+ disconnected () {
+ dispatch(disconnectTimeline(timelineId));
+ },
+
+ received (data) {
+ switch(data.event) {
+ case 'update':
+ dispatch(updateTimeline(timelineId, JSON.parse(data.payload)));
+ break;
+ case 'delete':
+ dispatch(deleteFromTimelines(data.payload));
+ break;
+ }
+ },
+
+ });
+ }
+
+ _unsubscribe () {
+ if (typeof this.subscription !== 'undefined') {
+ this.subscription.close();
+ this.subscription = null;
+ }
+ }
+
+ componentDidMount () {
+ const { dispatch, refresh, streamId, timelineId } = this.props;
+
+ if (typeof refresh !== 'function') return;
+
+ dispatch(refresh());
+ this._subscribe(dispatch, streamId, timelineId);
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.streamId !== this.props.streamId || nextProps.timelineId !== this.props.timelineId) {
+
+ if (typeof refresh !== 'function') return;
+
+ this.props.dispatch(this.props.refresh());
+ this._unsubscribe();
+ this._subscribe(this.props.dispatch, nextProps.streamId, nextProps.timelineId);
+ }
+ }
+
+ componentWillUnmount () {
+ this._unsubscribe();
+ }
+
+ render () {
+ const {
+ hasUnread,
+ columnId,
+ multiColumn,
+ emptyMessage,
+ icon,
+ title,
+ settings,
+ scrollName,
+ timelineId,
+ } = this.props;
+ const pinned = !!columnId;
+
+ return (
+
+
+ {settings}
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js
index 0e2300f8ce..8e2f44ffb5 100644
--- a/app/javascript/mastodon/features/community_timeline/index.js
+++ b/app/javascript/mastodon/features/community_timeline/index.js
@@ -1,144 +1,45 @@
import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
import {
refreshCommunityTimeline,
expandCommunityTimeline,
- updateTimeline,
- deleteFromTimelines,
- connectTimeline,
- disconnectTimeline,
} from '../../actions/timelines';
-import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
-import createStream from '../../stream';
+import Timeline from '../../components/timeline';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
});
-const mapStateToProps = state => ({
- hasUnread: state.getIn(['timelines', 'community', 'unread']) > 0,
- streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
- accessToken: state.getIn(['meta', 'access_token']),
-});
-
-@connect(mapStateToProps)
@injectIntl
export default class CommunityTimeline extends React.PureComponent {
static propTypes = {
- dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
- streamingAPIBaseURL: PropTypes.string.isRequired,
- accessToken: PropTypes.string.isRequired,
- hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('COMMUNITY', {}));
- }
- }
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- }
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- }
-
- componentDidMount () {
- const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
-
- dispatch(refreshCommunityTimeline());
-
- if (typeof this._subscription !== 'undefined') {
- return;
- }
-
- this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public:local', {
-
- connected () {
- dispatch(connectTimeline('community'));
- },
-
- reconnected () {
- dispatch(connectTimeline('community'));
- },
-
- disconnected () {
- dispatch(disconnectTimeline('community'));
- },
-
- received (data) {
- switch(data.event) {
- case 'update':
- dispatch(updateTimeline('community', JSON.parse(data.payload)));
- break;
- case 'delete':
- dispatch(deleteFromTimelines(data.payload));
- break;
- }
- },
-
- });
- }
-
- componentWillUnmount () {
- if (typeof this._subscription !== 'undefined') {
- this._subscription.close();
- this._subscription = null;
- }
- }
-
- setRef = c => {
- this.column = c;
- }
-
- handleLoadMore = () => {
- this.props.dispatch(expandCommunityTimeline());
- }
-
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
- const pinned = !!columnId;
return (
-
-
-
-
-
- }
- />
-
+ }
+ icon='users'
+ title={intl.formatMessage(messages.title)}
+ settings={}
+ scrollName='community_timeline'
+ timelineId='community'
+ />
);
}
diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js
index b17e8e1a5b..be2d5934b8 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/index.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/index.js
@@ -1,138 +1,54 @@
import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
import {
refreshHashtagTimeline,
expandHashtagTimeline,
- updateTimeline,
- deleteFromTimelines,
} from '../../actions/timelines';
-import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { FormattedMessage } from 'react-intl';
-import createStream from '../../stream';
+import Timeline from '../../components/timeline';
-const mapStateToProps = state => ({
- hasUnread: state.getIn(['timelines', 'tag', 'unread']) > 0,
- streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
- accessToken: state.getIn(['meta', 'access_token']),
-});
-
-@connect(mapStateToProps)
export default class HashtagTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
- dispatch: PropTypes.func.isRequired,
- streamingAPIBaseURL: PropTypes.string.isRequired,
- accessToken: PropTypes.string.isRequired,
- hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
- }
- }
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- }
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- }
-
- _subscribe (dispatch, id) {
- const { streamingAPIBaseURL, accessToken } = this.props;
-
- this.subscription = createStream(streamingAPIBaseURL, accessToken, `hashtag&tag=${id}`, {
-
- received (data) {
- switch(data.event) {
- case 'update':
- dispatch(updateTimeline(`hashtag:${id}`, JSON.parse(data.payload)));
- break;
- case 'delete':
- dispatch(deleteFromTimelines(data.payload));
- break;
- }
- },
-
- });
- }
-
- _unsubscribe () {
- if (typeof this.subscription !== 'undefined') {
- this.subscription.close();
- this.subscription = null;
- }
- }
-
- componentDidMount () {
- const { dispatch } = this.props;
- const { id } = this.props.params;
-
- dispatch(refreshHashtagTimeline(id));
- this._subscribe(dispatch, id);
+ componentWillMount () {
+ const id = this.props.params.id;
+ this.expand = () => expandHashtagTimeline(id);
+ this.refresh = () => refreshHashtagTimeline(id);
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.id !== this.props.params.id) {
- this.props.dispatch(refreshHashtagTimeline(nextProps.params.id));
- this._unsubscribe();
- this._subscribe(this.props.dispatch, nextProps.params.id);
+ const id = nextProps.params.id;
+ this.expand = () => expandHashtagTimeline(id);
+ this.refresh = () => refreshHashtagTimeline(id);
}
}
- componentWillUnmount () {
- this._unsubscribe();
- }
-
- setRef = c => {
- this.column = c;
- }
-
- handleLoadMore = () => {
- this.props.dispatch(expandHashtagTimeline(this.props.params.id));
- }
-
render () {
const { hasUnread, columnId, multiColumn } = this.props;
const { id } = this.props.params;
- const pinned = !!columnId;
return (
-
-
-
- }
- />
-
+ }
+ icon='hashtag'
+ title={id}
+ scrollName='hashtag_timeline'
+ timelineId={`hashtag:${id}`}
+ />
);
}
diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js
index 6021299d64..3d34f75544 100644
--- a/app/javascript/mastodon/features/home_timeline/index.js
+++ b/app/javascript/mastodon/features/home_timeline/index.js
@@ -2,12 +2,9 @@ import React from 'react';
import { connect } from 'react-redux';
import { expandHomeTimeline } from '../../actions/timelines';
import PropTypes from 'prop-types';
-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 { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
+import Timeline from '../../components/timeline';
import Link from 'react-router-dom/Link';
const messages = defineMessages({
@@ -15,7 +12,6 @@ const messages = defineMessages({
});
const mapStateToProps = state => ({
- hasUnread: state.getIn(['timelines', 'home', 'unread']) > 0,
hasFollows: state.getIn(['accounts_counters', state.getIn(['meta', 'me']), 'following_count']) > 0,
});
@@ -24,44 +20,14 @@ const mapStateToProps = state => ({
export default class HomeTimeline extends React.PureComponent {
static propTypes = {
- dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
- hasUnread: PropTypes.bool,
hasFollows: PropTypes.bool,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
};
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('HOME', {}));
- }
- }
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- }
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- }
-
- setRef = c => {
- this.column = c;
- }
-
- handleLoadMore = () => {
- this.props.dispatch(expandHomeTimeline());
- }
-
render () {
const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props;
- const pinned = !!columnId;
let emptyMessage;
@@ -72,28 +38,19 @@ export default class HomeTimeline extends React.PureComponent {
}
return (
-
-
-
-
-
-
-
+ }
+ scrollName='home_timeline'
+ timelineId='home'
+ />
);
}
diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js
index c6cad02d6d..23794d80b9 100644
--- a/app/javascript/mastodon/features/public_timeline/index.js
+++ b/app/javascript/mastodon/features/public_timeline/index.js
@@ -1,144 +1,45 @@
import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import StatusListContainer from '../ui/containers/status_list_container';
-import Column from '../../components/column';
-import ColumnHeader from '../../components/column_header';
import {
refreshPublicTimeline,
expandPublicTimeline,
- updateTimeline,
- deleteFromTimelines,
- connectTimeline,
- disconnectTimeline,
} from '../../actions/timelines';
-import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ColumnSettingsContainer from './containers/column_settings_container';
-import createStream from '../../stream';
+import Timeline from '../../components/timeline';
const messages = defineMessages({
title: { id: 'column.public', defaultMessage: 'Federated timeline' },
});
-const mapStateToProps = state => ({
- hasUnread: state.getIn(['timelines', 'public', 'unread']) > 0,
- streamingAPIBaseURL: state.getIn(['meta', 'streaming_api_base_url']),
- accessToken: state.getIn(['meta', 'access_token']),
-});
-
-@connect(mapStateToProps)
@injectIntl
export default class PublicTimeline extends React.PureComponent {
static propTypes = {
- dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
- streamingAPIBaseURL: PropTypes.string.isRequired,
- accessToken: PropTypes.string.isRequired,
- hasUnread: PropTypes.bool,
};
- handlePin = () => {
- const { columnId, dispatch } = this.props;
-
- if (columnId) {
- dispatch(removeColumn(columnId));
- } else {
- dispatch(addColumn('PUBLIC', {}));
- }
- }
-
- handleMove = (dir) => {
- const { columnId, dispatch } = this.props;
- dispatch(moveColumn(columnId, dir));
- }
-
- handleHeaderClick = () => {
- this.column.scrollTop();
- }
-
- componentDidMount () {
- const { dispatch, streamingAPIBaseURL, accessToken } = this.props;
-
- dispatch(refreshPublicTimeline());
-
- if (typeof this._subscription !== 'undefined') {
- return;
- }
-
- this._subscription = createStream(streamingAPIBaseURL, accessToken, 'public', {
-
- connected () {
- dispatch(connectTimeline('public'));
- },
-
- reconnected () {
- dispatch(connectTimeline('public'));
- },
-
- disconnected () {
- dispatch(disconnectTimeline('public'));
- },
-
- received (data) {
- switch(data.event) {
- case 'update':
- dispatch(updateTimeline('public', JSON.parse(data.payload)));
- break;
- case 'delete':
- dispatch(deleteFromTimelines(data.payload));
- break;
- }
- },
-
- });
- }
-
- componentWillUnmount () {
- if (typeof this._subscription !== 'undefined') {
- this._subscription.close();
- this._subscription = null;
- }
- }
-
- setRef = c => {
- this.column = c;
- }
-
- handleLoadMore = () => {
- this.props.dispatch(expandPublicTimeline());
- }
-
render () {
const { intl, columnId, hasUnread, multiColumn } = this.props;
- const pinned = !!columnId;
return (
-
-
-
-
-
- }
- />
-
+ }
+ icon='globe'
+ title={intl.formatMessage(messages.title)}
+ settings={}
+ scrollName='public_timeline'
+ timelineId='public'
+ />
);
}