Add remote only to public timeline (#13504)

* Add remote only to public timeline

* Fix code style
shrike
Takeshi Umeda 2020-05-10 17:36:18 +09:00 committed by GitHub
parent e9ecbca70d
commit 26b08a3c54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 21 deletions

View File

@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def public_timeline_statuses 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 end
def insert_pagination_headers def insert_pagination_headers
@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
end end
def pagination_params(core_params) 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 end
def next_path def next_path

View File

@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`); 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 connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

View File

@ -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 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 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 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 }); export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });

View File

@ -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 (
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
<SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
</div>
</div>
);
}
}

View File

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; 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 { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns'; import { changeColumnParams } from '../../../actions/columns';

View File

@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
const columns = state.getIn(['settings', 'columns']); const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid); 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 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' : ''}`]); const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
return { return {
hasUnread: !!timelineState && timelineState.get('unread') > 0, hasUnread: !!timelineState && timelineState.get('unread') > 0,
onlyMedia, onlyMedia,
onlyRemote,
}; };
}; };
@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent {
multiColumn: PropTypes.bool, multiColumn: PropTypes.bool,
hasUnread: PropTypes.bool, hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool, onlyMedia: PropTypes.bool,
onlyRemote: PropTypes.bool,
}; };
handlePin = () => { handlePin = () => {
const { columnId, dispatch, onlyMedia } = this.props; const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('PUBLIC', { other: { onlyMedia } })); dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
} }
} }
@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent {
} }
componentDidMount () { componentDidMount () {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) { if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
this.disconnect(); this.disconnect();
dispatch(expandPublicTimeline({ onlyMedia })); dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia })); this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
} }
} }
@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent {
} }
handleLoadMore = maxId => { handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props; const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia })); dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
} }
render () { render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props; const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId; const pinned = !!columnId;
return ( return (
@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent {
</ColumnHeader> </ColumnHeader>
<StatusListContainer <StatusListContainer
timelineId={`public${onlyMedia ? ':media' : ''}`} timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore} onLoadMore={this.handleLoadMore}
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`} scrollKey={`public_timeline-${columnId}`}

View File

@ -37,6 +37,7 @@ const componentMap = {
'HOME': HomeTimeline, 'HOME': HomeTimeline,
'NOTIFICATIONS': Notifications, 'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline, 'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline, 'COMMUNITY': CommunityTimeline,
'HASHTAG': HashtagTimeline, 'HASHTAG': HashtagTimeline,
'DIRECT': DirectTimeline, 'DIRECT': DirectTimeline,

View File

@ -284,7 +284,7 @@ class Status < ApplicationRecord
def as_public_timeline(account = nil, local_only = false) def as_public_timeline(account = nil, local_only = false)
query = timeline_scope(local_only).without_replies query = timeline_scope(local_only).without_replies
apply_timeline_filters(query, account, local_only) apply_timeline_filters(query, account, [:local, true].include?(local_only))
end end
def as_tag_timeline(tag, account = nil, local_only = false) def as_tag_timeline(tag, account = nil, local_only = false)
@ -376,8 +376,16 @@ class Status < ApplicationRecord
private private
def timeline_scope(local_only = false) def timeline_scope(scope = false)
starting_scope = local_only ? Status.local : Status starting_scope = case scope
when :local, true
Status.local
when :remote
Status.remote
else
Status
end
starting_scope starting_scope
.with_public_visibility .with_public_visibility
.without_reblogs .without_reblogs

View File

@ -374,6 +374,33 @@ RSpec.describe Status, type: :model do
end end
end end
context 'with a remote_only option set' do
let!(:local_account) { Fabricate(:account, domain: nil) }
let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
let!(:local_status) { Fabricate(:status, account: local_account) }
let!(:remote_status) { Fabricate(:status, account: remote_account) }
subject { Status.as_public_timeline(viewer, :remote) }
context 'without a viewer' do
let(:viewer) { nil }
it 'does not include local instances statuses' do
expect(subject).not_to include(local_status)
expect(subject).to include(remote_status)
end
end
context 'with a viewer' do
let(:viewer) { Fabricate(:account, username: 'viewer') }
it 'does not include local instances statuses' do
expect(subject).not_to include(local_status)
expect(subject).to include(remote_status)
end
end
end
describe 'with an account passed in' do describe 'with an account passed in' do
before do before do
@account = Fabricate(:account) @account = Fabricate(:account)

View File

@ -266,6 +266,8 @@ const startWorker = (workerId) => {
'public:media', 'public:media',
'public:local', 'public:local',
'public:local:media', 'public:local:media',
'public:remote',
'public:remote:media',
'hashtag', 'hashtag',
'hashtag:local', 'hashtag:local',
]; ];
@ -297,6 +299,7 @@ const startWorker = (workerId) => {
const PUBLIC_ENDPOINTS = [ const PUBLIC_ENDPOINTS = [
'/api/v1/streaming/public', '/api/v1/streaming/public',
'/api/v1/streaming/public/local', '/api/v1/streaming/public/local',
'/api/v1/streaming/public/remote',
'/api/v1/streaming/hashtag', '/api/v1/streaming/hashtag',
'/api/v1/streaming/hashtag/local', '/api/v1/streaming/hashtag/local',
]; ];
@ -535,6 +538,13 @@ const startWorker = (workerId) => {
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true); 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) => { app.get('/api/v1/streaming/direct', (req, res) => {
const channel = `timeline:direct:${req.accountId}`; const channel = `timeline:direct:${req.accountId}`;
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true); streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true);
@ -599,12 +609,18 @@ const startWorker = (workerId) => {
case 'public:local': case 'public:local':
streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:remote':
streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'public:media': case 'public:media':
streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:local:media': case 'public:local:media':
streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true); streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break; break;
case 'public:remote:media':
streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
break;
case 'direct': case 'direct':
channel = `timeline:direct:${req.accountId}`; channel = `timeline:direct:${req.accountId}`;
streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true); streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true);