Adding public timeline
This commit is contained in:
		
							parent
							
								
									06016453bd
								
							
						
					
					
						commit
						1f650d327d
					
				
					 21 changed files with 229 additions and 71 deletions
				
			
		| 
						 | 
				
			
			@ -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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,9 +40,7 @@ const UI = React.createClass({
 | 
			
		|||
            <StatusListContainer type='mentions' />
 | 
			
		||||
          </Column>
 | 
			
		||||
 | 
			
		||||
          <Column>
 | 
			
		||||
            {this.props.children}
 | 
			
		||||
          </Column>
 | 
			
		||||
          {this.props.children}
 | 
			
		||||
        </ColumnsArea>
 | 
			
		||||
 | 
			
		||||
        <NotificationsContainer />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										19
									
								
								app/channels/public_channel.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/channels/public_channel.rb
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
collection @statuses
 | 
			
		||||
extends('api/v1/statuses/show')
 | 
			
		||||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,7 @@ Rails.application.routes.draw do
 | 
			
		|||
        collection do
 | 
			
		||||
          get :home
 | 
			
		||||
          get :mentions
 | 
			
		||||
          get :public
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        member do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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' }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Reference in a new issue