Browse Source

Add remote only to public timeline (#13504)

* Add remote only to public timeline

* Fix code style
Takeshi Umeda 2 months ago
parent
commit
26b08a3c54
No account linked to committer's email address

+ 2
- 2
app/controllers/api/v1/timelines/public_controller.rb View File

@@ -39,7 +39,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
39 39
   end
40 40
 
41 41
   def public_timeline_statuses
42
-    Status.as_public_timeline(current_account, truthy_param?(:local))
42
+    Status.as_public_timeline(current_account, truthy_param?(:remote) ? :remote : truthy_param?(:local))
43 43
   end
44 44
 
45 45
   def insert_pagination_headers
@@ -47,7 +47,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
47 47
   end
48 48
 
49 49
   def pagination_params(core_params)
50
-    params.slice(:local, :limit, :only_media).permit(:local, :limit, :only_media).merge(core_params)
50
+    params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params)
51 51
   end
52 52
 
53 53
   def next_path

+ 1
- 1
app/javascript/mastodon/actions/streaming.js View File

@@ -73,7 +73,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
73 73
 
74 74
 export const connectUserStream      = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
75 75
 export const connectCommunityStream = ({ onlyMedia } = {}) => connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`);
76
-export const connectPublicStream    = ({ onlyMedia } = {}) => connectTimelineStream(`public${onlyMedia ? ':media' : ''}`, `public${onlyMedia ? ':media' : ''}`);
76
+export const connectPublicStream    = ({ onlyMedia, onlyRemote } = {}) => connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`);
77 77
 export const connectHashtagStream   = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
78 78
 export const connectDirectStream    = () => connectTimelineStream('direct', 'direct');
79 79
 export const connectListStream      = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);

+ 1
- 1
app/javascript/mastodon/actions/timelines.js View File

@@ -107,7 +107,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
107 107
 };
108 108
 
109 109
 export const expandHomeTimeline            = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done);
110
-export const expandPublicTimeline          = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { max_id: maxId, only_media: !!onlyMedia }, done);
110
+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);
111 111
 export const expandCommunityTimeline       = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done);
112 112
 export const expandAccountTimeline         = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
113 113
 export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });

+ 30
- 0
app/javascript/mastodon/features/public_timeline/components/column_settings.js View File

@@ -0,0 +1,30 @@
1
+import React from 'react';
2
+import PropTypes from 'prop-types';
3
+import ImmutablePropTypes from 'react-immutable-proptypes';
4
+import { injectIntl, FormattedMessage } from 'react-intl';
5
+import SettingToggle from '../../notifications/components/setting_toggle';
6
+
7
+export default @injectIntl
8
+class ColumnSettings extends React.PureComponent {
9
+
10
+  static propTypes = {
11
+    settings: ImmutablePropTypes.map.isRequired,
12
+    onChange: PropTypes.func.isRequired,
13
+    intl: PropTypes.object.isRequired,
14
+    columnId: PropTypes.string,
15
+  };
16
+
17
+  render () {
18
+    const { settings, onChange } = this.props;
19
+
20
+    return (
21
+      <div>
22
+        <div className='column-settings__row'>
23
+          <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
24
+          <SettingToggle settings={settings} settingPath={['other', 'onlyRemote']} onChange={onChange} label={<FormattedMessage id='community.column_settings.remote_only' defaultMessage='Remote only' />} />
25
+        </div>
26
+      </div>
27
+    );
28
+  }
29
+
30
+}

+ 1
- 1
app/javascript/mastodon/features/public_timeline/containers/column_settings_container.js View File

@@ -1,5 +1,5 @@
1 1
 import { connect } from 'react-redux';
2
-import ColumnSettings from '../../community_timeline/components/column_settings';
2
+import ColumnSettings from '../components/column_settings';
3 3
 import { changeSetting } from '../../../actions/settings';
4 4
 import { changeColumnParams } from '../../../actions/columns';
5 5
 

+ 16
- 13
app/javascript/mastodon/features/public_timeline/index.js View File

@@ -19,11 +19,13 @@ const mapStateToProps = (state, { columnId }) => {
19 19
   const columns = state.getIn(['settings', 'columns']);
20 20
   const index = columns.findIndex(c => c.get('uuid') === uuid);
21 21
   const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
22
+  const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
22 23
   const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
23 24
 
24 25
   return {
25 26
     hasUnread: !!timelineState && timelineState.get('unread') > 0,
26 27
     onlyMedia,
28
+    onlyRemote,
27 29
   };
28 30
 };
29 31
 
@@ -47,15 +49,16 @@ class PublicTimeline extends React.PureComponent {
47 49
     multiColumn: PropTypes.bool,
48 50
     hasUnread: PropTypes.bool,
49 51
     onlyMedia: PropTypes.bool,
52
+    onlyRemote: PropTypes.bool,
50 53
   };
51 54
 
52 55
   handlePin = () => {
53
-    const { columnId, dispatch, onlyMedia } = this.props;
56
+    const { columnId, dispatch, onlyMedia, onlyRemote } = this.props;
54 57
 
55 58
     if (columnId) {
56 59
       dispatch(removeColumn(columnId));
57 60
     } else {
58
-      dispatch(addColumn('PUBLIC', { other: { onlyMedia } }));
61
+      dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
59 62
     }
60 63
   }
61 64
 
@@ -69,19 +72,19 @@ class PublicTimeline extends React.PureComponent {
69 72
   }
70 73
 
71 74
   componentDidMount () {
72
-    const { dispatch, onlyMedia } = this.props;
75
+    const { dispatch, onlyMedia, onlyRemote } = this.props;
73 76
 
74
-    dispatch(expandPublicTimeline({ onlyMedia }));
75
-    this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
77
+    dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
78
+    this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
76 79
   }
77 80
 
78 81
   componentDidUpdate (prevProps) {
79
-    if (prevProps.onlyMedia !== this.props.onlyMedia) {
80
-      const { dispatch, onlyMedia } = this.props;
82
+    if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.onlyRemote !== this.props.onlyRemote) {
83
+      const { dispatch, onlyMedia, onlyRemote } = this.props;
81 84
 
82 85
       this.disconnect();
83
-      dispatch(expandPublicTimeline({ onlyMedia }));
84
-      this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
86
+      dispatch(expandPublicTimeline({ onlyMedia, onlyRemote }));
87
+      this.disconnect = dispatch(connectPublicStream({ onlyMedia, onlyRemote }));
85 88
     }
86 89
   }
87 90
 
@@ -97,13 +100,13 @@ class PublicTimeline extends React.PureComponent {
97 100
   }
98 101
 
99 102
   handleLoadMore = maxId => {
100
-    const { dispatch, onlyMedia } = this.props;
103
+    const { dispatch, onlyMedia, onlyRemote } = this.props;
101 104
 
102
-    dispatch(expandPublicTimeline({ maxId, onlyMedia }));
105
+    dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
103 106
   }
104 107
 
105 108
   render () {
106
-    const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia } = this.props;
109
+    const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
107 110
     const pinned = !!columnId;
108 111
 
109 112
     return (
@@ -122,7 +125,7 @@ class PublicTimeline extends React.PureComponent {
122 125
         </ColumnHeader>
123 126
 
124 127
         <StatusListContainer
125
-          timelineId={`public${onlyMedia ? ':media' : ''}`}
128
+          timelineId={`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`}
126 129
           onLoadMore={this.handleLoadMore}
127 130
           trackScroll={!pinned}
128 131
           scrollKey={`public_timeline-${columnId}`}

+ 1
- 0
app/javascript/mastodon/features/ui/components/columns_area.js View File

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

+ 11
- 3
app/models/status.rb View File

@@ -284,7 +284,7 @@ class Status < ApplicationRecord
284 284
     def as_public_timeline(account = nil, local_only = false)
285 285
       query = timeline_scope(local_only).without_replies
286 286
 
287
-      apply_timeline_filters(query, account, local_only)
287
+      apply_timeline_filters(query, account, [:local, true].include?(local_only))
288 288
     end
289 289
 
290 290
     def as_tag_timeline(tag, account = nil, local_only = false)
@@ -376,8 +376,16 @@ class Status < ApplicationRecord
376 376
 
377 377
     private
378 378
 
379
-    def timeline_scope(local_only = false)
380
-      starting_scope = local_only ? Status.local : Status
379
+    def timeline_scope(scope = false)
380
+      starting_scope = case scope
381
+                       when :local, true
382
+                         Status.local
383
+                       when :remote
384
+                         Status.remote
385
+                       else
386
+                         Status
387
+                       end
388
+
381 389
       starting_scope
382 390
         .with_public_visibility
383 391
         .without_reblogs

+ 27
- 0
spec/models/status_spec.rb View File

@@ -374,6 +374,33 @@ RSpec.describe Status, type: :model do
374 374
       end
375 375
     end
376 376
 
377
+    context 'with a remote_only option set' do
378
+      let!(:local_account)  { Fabricate(:account, domain: nil) }
379
+      let!(:remote_account) { Fabricate(:account, domain: 'test.com') }
380
+      let!(:local_status)   { Fabricate(:status, account: local_account) }
381
+      let!(:remote_status)  { Fabricate(:status, account: remote_account) }
382
+
383
+      subject { Status.as_public_timeline(viewer, :remote) }
384
+
385
+      context 'without a viewer' do
386
+        let(:viewer) { nil }
387
+
388
+        it 'does not include local instances statuses' do
389
+          expect(subject).not_to include(local_status)
390
+          expect(subject).to include(remote_status)
391
+        end
392
+      end
393
+
394
+      context 'with a viewer' do
395
+        let(:viewer) { Fabricate(:account, username: 'viewer') }
396
+
397
+        it 'does not include local instances statuses' do
398
+          expect(subject).not_to include(local_status)
399
+          expect(subject).to include(remote_status)
400
+        end
401
+      end
402
+    end
403
+
377 404
     describe 'with an account passed in' do
378 405
       before do
379 406
         @account = Fabricate(:account)

+ 16
- 0
streaming/index.js View File

@@ -266,6 +266,8 @@ const startWorker = (workerId) => {
266 266
     'public:media',
267 267
     'public:local',
268 268
     'public:local:media',
269
+    'public:remote',
270
+    'public:remote:media',
269 271
     'hashtag',
270 272
     'hashtag:local',
271 273
   ];
@@ -297,6 +299,7 @@ const startWorker = (workerId) => {
297 299
   const PUBLIC_ENDPOINTS = [
298 300
     '/api/v1/streaming/public',
299 301
     '/api/v1/streaming/public/local',
302
+    '/api/v1/streaming/public/remote',
300 303
     '/api/v1/streaming/hashtag',
301 304
     '/api/v1/streaming/hashtag/local',
302 305
   ];
@@ -535,6 +538,13 @@ const startWorker = (workerId) => {
535 538
     streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true);
536 539
   });
537 540
 
541
+  app.get('/api/v1/streaming/public/remote', (req, res) => {
542
+    const onlyMedia = req.query.only_media === '1' || req.query.only_media === 'true';
543
+    const channel   = onlyMedia ? 'timeline:public:remote:media' : 'timeline:public:remote';
544
+
545
+    streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req), true);
546
+  });
547
+
538 548
   app.get('/api/v1/streaming/direct', (req, res) => {
539 549
     const channel = `timeline:direct:${req.accountId}`;
540 550
     streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)), true);
@@ -599,12 +609,18 @@ const startWorker = (workerId) => {
599 609
     case 'public:local':
600 610
       streamFrom('timeline:public:local', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
601 611
       break;
612
+    case 'public:remote':
613
+      streamFrom('timeline:public:remote', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
614
+      break;
602 615
     case 'public:media':
603 616
       streamFrom('timeline:public:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
604 617
       break;
605 618
     case 'public:local:media':
606 619
       streamFrom('timeline:public:local:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
607 620
       break;
621
+    case 'public:remote:media':
622
+      streamFrom('timeline:public:remote:media', req, streamToWs(req, ws), streamWsEnd(req, ws), true);
623
+      break;
608 624
     case 'direct':
609 625
       channel = `timeline:direct:${req.accountId}`;
610 626
       streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)), true);

Loading…
Cancel
Save