diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 581befef13..c6e7854d93 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def public_timeline_statuses - Status.as_public_timeline(current_account, truthy_param?(:local)) + Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local)) end def insert_pagination_headers @@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params) + params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params) end def next_path diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 79b08bddac..080d665f4e 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => { export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); -export const connectPublicStream = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`); +export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`); export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept); export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); 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 861827d338..01f0fb0151 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -107,7 +107,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); -export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); diff --git a/app/javascript/mastodon/features/public_timeline/components/column_settings.js b/app/javascript/mastodon/features/public_timeline/components/column_settings.js new file mode 100644 index 0000000000..756b6fe06d --- /dev/null +++ b/app/javascript/mastodon/features/public_timeline/components/column_settings.js @@ -0,0 +1,30 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import SettingToggle from '../../notifications/components/setting_toggle'; + +export default @injectIntl +class ColumnSettings extends React.PureComponent { + + static propTypes = { + settings: ImmutablePropTypes.map.isRequired, + onChange: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + columnId: PropTypes.string, + }; + + render () { + const { settings, onChange } = this.props; + + return ( +
+
+ } /> + } /> +
+
+ ); + } + +} diff --git a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js index c56caa59e7..8c9e8aef41 100644 --- a/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import ColumnSettings from '../../community_timeline/components/column_settings'; +import ColumnSettings from '../components/column_settings'; import { changeSetting } from '../../../actions/settings'; import { changeColumnParams } from '../../../actions/columns'; diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 7aabd7f6e9..988b1b070b 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => { const columns = state.getIn(['settings', 'columns']); const index = columns.findIndex(c => c.get('uuid') === uuid); const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']); + const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]); return { hasUnread: !!timelineState && timelineState.get('unread') > 0, onlyMedia, + onlyRemote, }; }; @@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent { multiColumn: PropTypes.bool, hasUnread: PropTypes.bool, onlyMedia: PropTypes.bool, + onlyRemote: PropTypes.bool, }; handlePin = () => { - const { columnId, dispatch, onlyMedia } = this.props; + const { columnId, dispatch, onlyMedia, onlyRemote } = this.props; if (columnId) { dispatch(removeColumn(columnId)); } else { - dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); + dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } })); } } @@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent { } componentDidMount () { - const { dispatch, onlyMedia } = this.props; + const { dispatch, onlyMedia, onlyRemote } = this.props; - dispatch(expandPublicTimeline({ onlyMedia })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); } componentDidUpdate (prevProps) { - if (prevProps.onlyMedia !== this.props.onlyMedia) { - const { dispatch, onlyMedia } = this.props; + if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) { + const { dispatch, onlyMedia, onlyRemote } = this.props; this.disconnect(); - dispatch(expandPublicTimeline({ onlyMedia })); - this.disconnect = dispatch(connectPublicStream({ onlyMedia })); + dispatch(expandPublicTimeline({ onlyMedia, onlyRemote })); + this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote })); } } @@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent { } handleLoadMore = maxId => { - const { dispatch, onlyMedia } = this.props; + const { dispatch, onlyMedia, onlyRemote } = this.props; - dispatch(expandPublicTimeline({ maxId, onlyMedia })); + dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote })); } render () { - const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props; + const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props; const pinned = !!columnId; return ( @@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent { { 'public:media', 'public:local', 'public:local:media', + 'public:remote', + 'public:remote:media', 'hashtag', 'hashtag:local', ]; @@ -297,6 +299,7 @@ const startWorker = (workerId) => { const PUBLIC_ENDPOINTS = [ '/api/v1/streaming/public', '/api/v1/streaming/public/local', + '/api/v1/streaming/public/remote', '/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag/local', ]; @@ -535,6 +538,13 @@ const startWorker = (workerId) => { streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); }); + app.get('/api/v1/streaming/public/remote', (req, res) => { + const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true'; + const channel = onlyMedia ? 'timeline:public:remote:media' : 'timeline:public:remote'; + + streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); + }); + app.get('/api/v1/streaming/direct', (req, res) => { const channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); @@ -599,12 +609,18 @@ const startWorker = (workerId) => { case 'public:local': streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote': + streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'public:media': streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; case 'public:local:media': streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); break; + case 'public:remote:media': + streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); + break; case 'direct': channel = `timeline:direct:${req.accountId}`; streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true);