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' + /> ); }