about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2016-10-07 16:00:11 +0200
committerEugen Rochko <eugen@zeonfederated.com>2016-10-07 16:00:11 +0200
commit1f650d327d35bc48b897da99914c43750d8e5fd3 (patch)
tree6960b24e30c46c55ba9f3bf1af0a24b43cd9d248
parent06016453bd91882a53e91c11fc80f2c75fd474bb (diff)
Adding public timeline
-rw-r--r--app/assets/javascripts/components/components/status_content.jsx2
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx18
-rw-r--r--app/assets/javascripts/components/features/account/index.jsx21
-rw-r--r--app/assets/javascripts/components/features/getting_started/index.jsx18
-rw-r--r--app/assets/javascripts/components/features/public_timeline/index.jsx103
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx21
-rw-r--r--app/assets/javascripts/components/features/ui/components/column.jsx9
-rw-r--r--app/assets/javascripts/components/features/ui/components/columns_area.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/components/navigation_bar.jsx2
-rw-r--r--app/assets/javascripts/components/features/ui/index.jsx4
-rw-r--r--app/assets/javascripts/components/reducers/timelines.jsx17
-rw-r--r--app/channels/public_channel.rb19
-rw-r--r--app/controllers/api/v1/statuses_controller.rb7
-rw-r--r--app/helpers/home_helper.rb4
-rw-r--r--app/lib/feed_manager.rb34
-rw-r--r--app/services/fan_out_on_write_service.rb5
-rw-r--r--app/views/api/v1/statuses/index.rabl (renamed from app/views/api/v1/statuses/home.rabl)0
-rw-r--r--app/views/api/v1/statuses/mentions.rabl2
-rw-r--r--app/views/api/v1/statuses/show.rabl4
-rw-r--r--config/routes.rb1
-rw-r--r--spec/controllers/api/v1/statuses_controller_spec.rb7
21 files changed, 229 insertions, 71 deletions
diff --git a/app/assets/javascripts/components/components/status_content.jsx b/app/assets/javascripts/components/components/status_content.jsx
index 99d83b609..bf1ba54fc 100644
--- a/app/assets/javascripts/components/components/status_content.jsx
+++ b/app/assets/javascripts/components/components/status_content.jsx
@@ -26,7 +26,7 @@ const StatusContent = React.createClass({
       } else {
         link.setAttribute('target', '_blank');
         link.setAttribute('rel', 'noopener');
-        link.addEventListener('click', this.onNormalClick.bind(this));
+        link.addEventListener('click', this.onNormalClick);
       }
     }
   },
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 6c65c303b..24db6424a 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -18,6 +18,7 @@ import {
 import Account            from '../features/account';
 import Status             from '../features/status';
 import GettingStarted     from '../features/getting_started';
+import PublicTimeline     from '../features/public_timeline';
 import UI                 from '../features/ui';
 
 const store = configureStore();
@@ -43,14 +44,7 @@ const Mastodon = React.createClass({
     }
 
     if (typeof App !== 'undefined') {
-      App.timeline = App.cable.subscriptions.create("TimelineChannel", {
-        connected () {
-
-        },
-
-        disconnected () {
-
-        },
+      this.subscription = App.cable.subscriptions.create('TimelineChannel', {
 
         received (data) {
           switch(data.type) {
@@ -65,16 +59,24 @@ const Mastodon = React.createClass({
               return store.dispatch(refreshTimeline('mentions'));
           }
         }
+
       });
     }
   },
 
+  componentWillUnmount () {
+    if (typeof this.subscription !== 'undefined') {
+      this.subscription.unsubscribe();
+    }
+  },
+
   render () {
     return (
       <Provider store={store}>
         <Router history={hashHistory}>
           <Route path='/' component={UI}>
             <IndexRoute component={GettingStarted} />
+            <Route path='/statuses/all' component={PublicTimeline} />
             <Route path='/statuses/:statusId' component={Status} />
             <Route path='/accounts/:accountId' component={Account} />
           </Route>
diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx
index c9de1a848..9c77214d1 100644
--- a/app/assets/javascripts/components/features/account/index.jsx
+++ b/app/assets/javascripts/components/features/account/index.jsx
@@ -27,9 +27,10 @@ import StatusList            from '../../components/status_list';
 import LoadingIndicator      from '../../components/loading_indicator';
 import Immutable             from 'immutable';
 import ActionBar             from './components/action_bar';
+import Column                from '../ui/components/column';
 
 function selectStatuses(state, accountId) {
-  return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null);
+  return state.getIn(['timelines', 'accounts_timelines', accountId], Immutable.List([])).map(id => selectStatus(state, id)).filterNot(status => status === null);
 };
 
 const mapStateToProps = (state, props) => ({
@@ -109,15 +110,21 @@ const Account = React.createClass({
     const { account, statuses, me } = this.props;
 
     if (account === null) {
-      return <LoadingIndicator />;
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
     }
 
     return (
-      <div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
-        <Header account={account} />
-        <ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} />
-        <StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} />
-      </div>
+      <Column>
+        <div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
+          <Header account={account} />
+          <ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} />
+          <StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} onDelete={this.handleDelete} />
+        </div>
+      </Column>
     );
   }
 
diff --git a/app/assets/javascripts/components/features/getting_started/index.jsx b/app/assets/javascripts/components/features/getting_started/index.jsx
index d507cb46f..a4f1ac487 100644
--- a/app/assets/javascripts/components/features/getting_started/index.jsx
+++ b/app/assets/javascripts/components/features/getting_started/index.jsx
@@ -1,12 +1,16 @@
+import Column from '../ui/components/column';
+
 const GettingStarted = () => {
   return (
-    <div className='static-content'>
-      <h1>Getting started</h1>
-      <p>Mastodon is still in development and one of the lacking areas at the moment is user discovery.</p>
-      <p>You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form in the bottom of the sidebar.</p>
-      <p>If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.</p>
-      <p>The developer of this project can be followed as Gargron@mastodon.social</p>
-    </div>
+    <Column>
+      <div className='static-content'>
+        <h1>Getting started</h1>
+        <p>Mastodon is still in development and one of the lacking areas at the moment is user discovery.</p>
+        <p>You can follow people if you know their username and the domain they are on by entering an e-mail-esque address into the form in the bottom of the sidebar.</p>
+        <p>If the target user is on the same domain as you, just the username will work. The same rule applies to mentioning people in statuses.</p>
+        <p>The developer of this project can be followed as Gargron@mastodon.social</p>
+      </div>
+    </Column>
   );
 };
 
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
new file mode 100644
index 000000000..dd31dc115
--- /dev/null
+++ b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -0,0 +1,103 @@
+import { connect }        from 'react-redux';
+import PureRenderMixin    from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import StatusList         from '../../components/status_list';
+import Column             from '../ui/components/column';
+import Immutable          from 'immutable';
+import { selectStatus }   from '../../reducers/timelines';
+import {
+  updateTimeline,
+  refreshTimeline,
+  expandTimeline
+}                         from '../../actions/timelines';
+import { deleteStatus }   from '../../actions/statuses';
+import { replyCompose }   from '../../actions/compose';
+import {
+  favourite,
+  reblog,
+  unreblog,
+  unfavourite
+}                         from '../../actions/interactions';
+
+function selectStatuses(state) {
+  return state.getIn(['timelines', 'public'], Immutable.List()).map(id => selectStatus(state, id)).filterNot(status => status === null);
+};
+
+const mapStateToProps = (state) => ({
+  statuses: selectStatuses(state),
+  me: state.getIn(['timelines', 'me'])
+});
+
+const PublicTimeline = React.createClass({
+
+  propTypes: {
+    statuses: ImmutablePropTypes.list.isRequired,
+    me: React.PropTypes.number.isRequired,
+    dispatch: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  componentWillMount () {
+    const { dispatch } = this.props;
+
+    dispatch(refreshTimeline('public'));
+
+    if (typeof App !== 'undefined') {
+      this.subscription = App.cable.subscriptions.create('PublicChannel', {
+
+        received (data) {
+          dispatch(updateTimeline('public', JSON.parse(data.message)));
+        }
+
+      });
+    }
+  },
+
+  componentWillUnmount () {
+    if (typeof this.subscription !== 'undefined') {
+      this.subscription.unsubscribe();
+    }
+  },
+
+  handleReply (status) {
+    this.props.dispatch(replyCompose(status));
+  },
+
+  handleReblog (status) {
+    if (status.get('reblogged')) {
+      this.props.dispatch(unreblog(status));
+    } else {
+      this.props.dispatch(reblog(status));
+    }
+  },
+
+  handleFavourite (status) {
+    if (status.get('favourited')) {
+      this.props.dispatch(unfavourite(status));
+    } else {
+      this.props.dispatch(favourite(status));
+    }
+  },
+
+  handleDelete (status) {
+    this.props.dispatch(deleteStatus(status.get('id')));
+  },
+
+  handleScrollToBottom () {
+    this.props.dispatch(expandTimeline('public'));
+  },
+
+  render () {
+    const { statuses, me } = this.props;
+
+    return (
+      <Column icon='globe' heading='Public'>
+        <StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} onDelete={this.handleDelete} />
+      </Column>
+    );
+  },
+
+});
+
+export default connect(mapStateToProps)(PublicTimeline);
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index c294ac1d6..b282956b1 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -7,6 +7,7 @@ import EmbeddedStatus        from '../../components/status';
 import LoadingIndicator      from '../../components/loading_indicator';
 import DetailedStatus        from './components/detailed_status';
 import ActionBar             from './components/action_bar';
+import Column                from '../ui/components/column';
 import { favourite, reblog } from '../../actions/interactions';
 import { replyCompose }      from '../../actions/compose';
 import { selectStatus }      from '../../reducers/timelines';
@@ -64,20 +65,26 @@ const Status = React.createClass({
     const { status, ancestors, descendants, me } = this.props;
 
     if (status === null) {
-      return <LoadingIndicator />;
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
     }
 
     const account = status.get('account');
 
     return (
-      <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'>
-        <div>{this.renderChildren(ancestors)}</div>
+      <Column>
+        <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'>
+          <div>{this.renderChildren(ancestors)}</div>
 
-        <DetailedStatus status={status} me={me} />
-        <ActionBar status={status} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} />
+          <DetailedStatus status={status} me={me} />
+          <ActionBar status={status} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} />
 
-        <div>{this.renderChildren(descendants)}</div>
-      </div>
+          <div>{this.renderChildren(descendants)}</div>
+        </div>
+      </Column>
     );
   }
 
diff --git a/app/assets/javascripts/components/features/ui/components/column.jsx b/app/assets/javascripts/components/features/ui/components/column.jsx
index 499e5f4a5..bb9331267 100644
--- a/app/assets/javascripts/components/features/ui/components/column.jsx
+++ b/app/assets/javascripts/components/features/ui/components/column.jsx
@@ -29,7 +29,6 @@ const scrollTop = (node) => {
   };
 };
 
-
 const Column = React.createClass({
 
   propTypes: {
@@ -50,10 +49,6 @@ const Column = React.createClass({
     }
   },
 
-  handleScroll () {
-    // todo
-  },
-
   render () {
     let header = '';
 
@@ -61,10 +56,10 @@ const Column = React.createClass({
       header = <ColumnHeader icon={this.props.icon} type={this.props.heading} onClick={this.handleHeaderClick} />;
     }
 
-    const style = { width: '350px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', marginBottom: '0', display: 'flex', flexDirection: 'column' };
+    const style = { width: '330px', flex: '0 0 auto', background: '#282c37', margin: '10px', marginRight: '0', marginBottom: '0', display: 'flex', flexDirection: 'column' };
 
     return (
-      <div style={style} onWheel={this.handleWheel} onScroll={this.handleScroll}>
+      <div style={style} onWheel={this.handleWheel}>
         {header}
         {this.props.children}
       </div>
diff --git a/app/assets/javascripts/components/features/ui/components/columns_area.jsx b/app/assets/javascripts/components/features/ui/components/columns_area.jsx
index fa4c1a9c9..94433539b 100644
--- a/app/assets/javascripts/components/features/ui/components/columns_area.jsx
+++ b/app/assets/javascripts/components/features/ui/components/columns_area.jsx
@@ -6,7 +6,7 @@ const ColumnsArea = React.createClass({
 
   render () {
     return (
-      <div style={{ display: 'flex', flexDirection: 'row', flex: '1', marginRight: '10px', marginBottom: '10px', overflowX: 'auto' }}>
+      <div style={{ display: 'flex', flexDirection: 'row', flex: '1', justifyContent: 'flex-start', marginRight: '10px', marginBottom: '10px', overflowX: 'auto' }}>
         {this.props.children}
       </div>
     );
diff --git a/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx b/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx
index 9d9481d3e..a16852541 100644
--- a/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx
+++ b/app/assets/javascripts/components/features/ui/components/navigation_bar.jsx
@@ -19,7 +19,7 @@ const NavigationBar = React.createClass({
 
         <div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
           <strong style={{ fontWeight: '500', display: 'block', color: '#fff' }}>{this.props.account.get('acct')}</strong>
-          <a href='/settings' style={{ color: 'inherit', textDecoration: 'none' }}>Settings</a> · <a href='/auth/sign_out' data-method='delete' style={{ color: 'inherit', textDecoration: 'none' }}>Logout</a>
+          <a href='/settings' style={{ color: 'inherit', textDecoration: 'none' }}>Settings</a> · <Link to='/statuses/all' style={{ color: 'inherit', textDecoration: 'none' }}>Public timeline</Link> · <a href='/auth/sign_out' data-method='delete' style={{ color: 'inherit', textDecoration: 'none' }}>Logout</a>
         </div>
       </div>
     );
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
index 323729dd6..0bc235b53 100644
--- a/app/assets/javascripts/components/features/ui/index.jsx
+++ b/app/assets/javascripts/components/features/ui/index.jsx
@@ -40,9 +40,7 @@ const UI = React.createClass({
             <StatusListContainer type='mentions' />
           </Column>
 
-          <Column>
-            {this.props.children}
-          </Column>
+          {this.props.children}
         </ColumnsArea>
 
         <NotificationsContainer />
diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx
index 392683150..0b02ac181 100644
--- a/app/assets/javascripts/components/reducers/timelines.jsx
+++ b/app/assets/javascripts/components/reducers/timelines.jsx
@@ -30,6 +30,7 @@ import Immutable                 from 'immutable';
 const initialState = Immutable.Map({
   home: Immutable.List([]),
   mentions: Immutable.List([]),
+  public: Immutable.List([]),
   statuses: Immutable.Map(),
   accounts: Immutable.Map(),
   accounts_timelines: Immutable.Map(),
@@ -110,7 +111,7 @@ function normalizeTimeline(state, timeline, statuses) {
 };
 
 function appendNormalizedTimeline(state, timeline, statuses) {
-  let moreIds = Immutable.List();
+  let moreIds = Immutable.List([]);
 
   statuses.forEach((status, i) => {
     state   = normalizeStatus(state, status);
@@ -121,29 +122,33 @@ function appendNormalizedTimeline(state, timeline, statuses) {
 };
 
 function normalizeAccountTimeline(state, accountId, statuses) {
+  state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => {
+    return (list.size > 0) ? list.clear() : list;
+  });
+
   statuses.forEach((status, i) => {
     state = normalizeStatus(state, status);
-    state = state.updateIn(['accounts_timelines', accountId], Immutable.List(), list => list.set(i, status.get('id')));
+    state = state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.set(i, status.get('id')));
   });
 
   return state;
 };
 
 function appendNormalizedAccountTimeline(state, accountId, statuses) {
-  let moreIds = Immutable.List();
+  let moreIds = Immutable.List([]);
 
   statuses.forEach((status, i) => {
     state   = normalizeStatus(state, status);
     moreIds = moreIds.set(i, status.get('id'));
   });
 
-  return state.updateIn(['accounts_timelines', accountId], Immutable.List(), list => list.push(...moreIds));
+  return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.push(...moreIds));
 };
 
 function updateTimeline(state, timeline, status) {
   state = normalizeStatus(state, status);
   state = state.update(timeline, list => list.unshift(status.get('id')));
-  state = state.updateIn(['accounts_timelines', status.getIn(['account', 'id'])], Immutable.List(), list => list.unshift(status.get('id')));
+  state = state.updateIn(['accounts_timelines', status.getIn(['account', 'id'])], Immutable.List([]), list => (list.includes(status.get('id')) ? list : list.unshift(status.get('id'))));
 
   return state;
 };
@@ -161,7 +166,7 @@ function deleteStatus(state, id) {
   });
 
   // Remove references from account timelines
-  state = state.updateIn(['accounts_timelines', status.get('account')], Immutable.List(), list => list.filterNot(item => item === id));
+  state = state.updateIn(['accounts_timelines', status.get('account')], Immutable.List([]), list => list.filterNot(item => item === id));
 
   // Remove reblogs of deleted status
   const references = state.get('statuses').filter(item => item.get('reblog') === id);
diff --git a/app/channels/public_channel.rb b/app/channels/public_channel.rb
new file mode 100644
index 000000000..870b5cc2e
--- /dev/null
+++ b/app/channels/public_channel.rb
@@ -0,0 +1,19 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+class PublicChannel < ApplicationCable::Channel
+  def subscribed
+    stream_from 'timeline:public', -> (encoded_message) do
+      message = ActiveSupport::JSON.decode(encoded_message)
+
+      status = Status.find_by(id: message['id'])
+      next if status.nil?
+
+      message['message'] = FeedManager.instance.inline_render(current_user.account, status)
+
+      transmit message
+    end
+  end
+
+  def unsubscribed
+    # Any cleanup needed when channel is unsubscribed
+  end
+end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index ec1056a42..952ed641d 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -46,9 +46,16 @@ class Api::V1::StatusesController < ApiController
 
   def home
     @statuses = Feed.new(:home, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
+    render action: :index
   end
 
   def mentions
     @statuses = Feed.new(:mentions, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
+    render action: :index
+  end
+
+  def public
+    @statuses = Status.with_includes.with_counters.order('id desc').paginate_by_max_id(20, params[:max_id], params[:since_id]).to_a
+    render action: :index
   end
 end
diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb
index 61a4b4dd8..6b5151777 100644
--- a/app/helpers/home_helper.rb
+++ b/app/helpers/home_helper.rb
@@ -6,8 +6,8 @@ module HomeHelper
       account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json),
 
       timelines: {
-        home: render(file: 'api/v1/statuses/home', locals: { statuses: @home }, formats: :json),
-        mentions: render(file: 'api/v1/statuses/mentions', locals: { statuses: @mentions }, formats: :json)
+        home: render(file: 'api/v1/statuses/index', locals: { statuses: @home }, formats: :json),
+        mentions: render(file: 'api/v1/statuses/index', locals: { statuses: @mentions }, formats: :json)
       }
     }
   end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 58d6a005c..46a105124 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -33,22 +33,6 @@ class FeedManager
     redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
   end
 
-  private
-
-  def redis
-    $redis
-  end
-
-  # Filter status out of the home feed if it is a reply to someone the user doesn't follow
-  def filter_from_home?(status, receiver)
-    replied_to_user = status.reply? ? status.thread.account : nil
-    (status.reply? && !(receiver.id == replied_to_user.id || replied_to_user.id == status.account_id || receiver.following?(replied_to_user)))
-  end
-
-  def filter_from_mentions?(status, receiver)
-    receiver.blocking?(status.account) || (status.reblog? && receiver.blocking?(status.reblog.account))
-  end
-
   def inline_render(target_account, status)
     rabl_scope = Class.new do
       include RoutingHelper
@@ -58,7 +42,7 @@ class FeedManager
       end
 
       def current_user
-        @account.user
+        @account.try(:user)
       end
 
       def current_account
@@ -68,4 +52,20 @@ class FeedManager
 
     Rabl::Renderer.new('api/v1/statuses/show', status, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
   end
+
+  private
+
+  def redis
+    $redis
+  end
+
+  # Filter status out of the home feed if it is a reply to someone the user doesn't follow
+  def filter_from_home?(status, receiver)
+    replied_to_user = status.reply? ? status.thread.account : nil
+    (status.reply? && !(receiver.id == replied_to_user.id || replied_to_user.id == status.account_id || receiver.following?(replied_to_user))) || (status.reblog? && receiver.blocking?(status.reblog.account))
+  end
+
+  def filter_from_mentions?(status, receiver)
+    receiver.blocking?(status.account)
+  end
 end
diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb
index 29b093ef8..312994db5 100644
--- a/app/services/fan_out_on_write_service.rb
+++ b/app/services/fan_out_on_write_service.rb
@@ -5,6 +5,7 @@ class FanOutOnWriteService < BaseService
     deliver_to_self(status) if status.account.local?
     deliver_to_followers(status)
     deliver_to_mentioned(status)
+    deliver_to_public(status)
   end
 
   private
@@ -27,4 +28,8 @@ class FanOutOnWriteService < BaseService
       FeedManager.instance.push(:mentions, mentioned_account, status)
     end
   end
+
+  def deliver_to_public(status)
+    FeedManager.instance.broadcast(:public, id: status.id)
+  end
 end
diff --git a/app/views/api/v1/statuses/home.rabl b/app/views/api/v1/statuses/index.rabl
index 0a0ed13c5..0a0ed13c5 100644
--- a/app/views/api/v1/statuses/home.rabl
+++ b/app/views/api/v1/statuses/index.rabl
diff --git a/app/views/api/v1/statuses/mentions.rabl b/app/views/api/v1/statuses/mentions.rabl
deleted file mode 100644
index 0a0ed13c5..000000000
--- a/app/views/api/v1/statuses/mentions.rabl
+++ /dev/null
@@ -1,2 +0,0 @@
-collection @statuses
-extends('api/v1/statuses/show')
diff --git a/app/views/api/v1/statuses/show.rabl b/app/views/api/v1/statuses/show.rabl
index 3595bafb4..20cb65e29 100644
--- a/app/views/api/v1/statuses/show.rabl
+++ b/app/views/api/v1/statuses/show.rabl
@@ -6,8 +6,8 @@ node(:content)          { |status| Formatter.instance.format(status) }
 node(:url)              { |status| TagManager.instance.url_for(status) }
 node(:reblogs_count)    { |status| status.reblogs_count }
 node(:favourites_count) { |status| status.favourites_count }
-node(:favourited)       { |status| current_account.favourited?(status) }
-node(:reblogged)        { |status| current_account.reblogged?(status) }
+node(:favourited, if: proc { !current_account.nil? }) { |status| current_account.favourited?(status) }
+node(:reblogged,  if: proc { !current_account.nil? }) { |status| current_account.reblogged?(status) }
 
 child :reblog => :reblog do
   extends('api/v1/statuses/show')
diff --git a/config/routes.rb b/config/routes.rb
index 8f9e5fe14..94507d324 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -48,6 +48,7 @@ Rails.application.routes.draw do
         collection do
           get :home
           get :mentions
+          get :public
         end
 
         member do
diff --git a/spec/controllers/api/v1/statuses_controller_spec.rb b/spec/controllers/api/v1/statuses_controller_spec.rb
index 7af54299a..66060c57e 100644
--- a/spec/controllers/api/v1/statuses_controller_spec.rb
+++ b/spec/controllers/api/v1/statuses_controller_spec.rb
@@ -47,6 +47,13 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
     end
   end
 
+  describe 'GET #public' do
+    it 'returns http success' do
+      get :public
+      expect(response).to have_http_status(:success)
+    end
+  end
+
   describe 'POST #create' do
     before do
       post :create, params: { status: 'Hello world' }