forked from treehouse/mastodon
Adding public timeline
parent
06016453bd
commit
1f650d327d
|
@ -26,7 +26,7 @@ const StatusContent = React.createClass({
|
||||||
} else {
|
} else {
|
||||||
link.setAttribute('target', '_blank');
|
link.setAttribute('target', '_blank');
|
||||||
link.setAttribute('rel', 'noopener');
|
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 Account from '../features/account';
|
||||||
import Status from '../features/status';
|
import Status from '../features/status';
|
||||||
import GettingStarted from '../features/getting_started';
|
import GettingStarted from '../features/getting_started';
|
||||||
|
import PublicTimeline from '../features/public_timeline';
|
||||||
import UI from '../features/ui';
|
import UI from '../features/ui';
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
@ -43,14 +44,7 @@ const Mastodon = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof App !== 'undefined') {
|
if (typeof App !== 'undefined') {
|
||||||
App.timeline = App.cable.subscriptions.create("TimelineChannel", {
|
this.subscription = App.cable.subscriptions.create('TimelineChannel', {
|
||||||
connected () {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
disconnected () {
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
received (data) {
|
received (data) {
|
||||||
switch(data.type) {
|
switch(data.type) {
|
||||||
|
@ -65,16 +59,24 @@ const Mastodon = React.createClass({
|
||||||
return store.dispatch(refreshTimeline('mentions'));
|
return store.dispatch(refreshTimeline('mentions'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (typeof this.subscription !== 'undefined') {
|
||||||
|
this.subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={hashHistory}>
|
<Router history={hashHistory}>
|
||||||
<Route path='/' component={UI}>
|
<Route path='/' component={UI}>
|
||||||
<IndexRoute component={GettingStarted} />
|
<IndexRoute component={GettingStarted} />
|
||||||
|
<Route path='/statuses/all' component={PublicTimeline} />
|
||||||
<Route path='/statuses/:statusId' component={Status} />
|
<Route path='/statuses/:statusId' component={Status} />
|
||||||
<Route path='/accounts/:accountId' component={Account} />
|
<Route path='/accounts/:accountId' component={Account} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
|
@ -27,9 +27,10 @@ import StatusList from '../../components/status_list';
|
||||||
import LoadingIndicator from '../../components/loading_indicator';
|
import LoadingIndicator from '../../components/loading_indicator';
|
||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
import ActionBar from './components/action_bar';
|
import ActionBar from './components/action_bar';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
function selectStatuses(state, accountId) {
|
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) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
|
@ -109,15 +110,21 @@ const Account = React.createClass({
|
||||||
const { account, statuses, me } = this.props;
|
const { account, statuses, me } = this.props;
|
||||||
|
|
||||||
if (account === null) {
|
if (account === null) {
|
||||||
return <LoadingIndicator />;
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
|
<Column>
|
||||||
<Header account={account} />
|
<div style={{ display: 'flex', flexDirection: 'column', 'flex': '0 0 auto', height: '100%' }}>
|
||||||
<ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} />
|
<Header account={account} />
|
||||||
<StatusList statuses={statuses} me={me} onScrollToBottom={this.handleScrollToBottom} onReply={this.handleReply} onReblog={this.handleReblog} onFavourite={this.handleFavourite} />
|
<ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} />
|
||||||
</div>
|
<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 = () => {
|
const GettingStarted = () => {
|
||||||
return (
|
return (
|
||||||
<div className='static-content'>
|
<Column>
|
||||||
<h1>Getting started</h1>
|
<div className='static-content'>
|
||||||
<p>Mastodon is still in development and one of the lacking areas at the moment is user discovery.</p>
|
<h1>Getting started</h1>
|
||||||
<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>Mastodon is still in development and one of the lacking areas at the moment is user discovery.</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>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>The developer of this project can be followed as Gargron@mastodon.social</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>
|
||||||
</div>
|
<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 LoadingIndicator from '../../components/loading_indicator';
|
||||||
import DetailedStatus from './components/detailed_status';
|
import DetailedStatus from './components/detailed_status';
|
||||||
import ActionBar from './components/action_bar';
|
import ActionBar from './components/action_bar';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
import { favourite, reblog } from '../../actions/interactions';
|
import { favourite, reblog } from '../../actions/interactions';
|
||||||
import { replyCompose } from '../../actions/compose';
|
import { replyCompose } from '../../actions/compose';
|
||||||
import { selectStatus } from '../../reducers/timelines';
|
import { selectStatus } from '../../reducers/timelines';
|
||||||
|
@ -64,20 +65,26 @@ const Status = React.createClass({
|
||||||
const { status, ancestors, descendants, me } = this.props;
|
const { status, ancestors, descendants, me } = this.props;
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return <LoadingIndicator />;
|
return (
|
||||||
|
<Column>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = status.get('account');
|
const account = status.get('account');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'>
|
<Column>
|
||||||
<div>{this.renderChildren(ancestors)}</div>
|
<div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'>
|
||||||
|
<div>{this.renderChildren(ancestors)}</div>
|
||||||
|
|
||||||
<DetailedStatus status={status} me={me} />
|
<DetailedStatus status={status} me={me} />
|
||||||
<ActionBar status={status} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} />
|
<ActionBar status={status} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} />
|
||||||
|
|
||||||
<div>{this.renderChildren(descendants)}</div>
|
<div>{this.renderChildren(descendants)}</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ const scrollTop = (node) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const Column = React.createClass({
|
const Column = React.createClass({
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -50,10 +49,6 @@ const Column = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleScroll () {
|
|
||||||
// todo
|
|
||||||
},
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let header = '';
|
let header = '';
|
||||||
|
|
||||||
|
@ -61,10 +56,10 @@ const Column = React.createClass({
|
||||||
header = <ColumnHeader icon={this.props.icon} type={this.props.heading} onClick={this.handleHeaderClick} />;
|
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 (
|
return (
|
||||||
<div style={style} onWheel={this.handleWheel} onScroll={this.handleScroll}>
|
<div style={style} onWheel={this.handleWheel}>
|
||||||
{header}
|
{header}
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ const ColumnsArea = React.createClass({
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
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}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ const NavigationBar = React.createClass({
|
||||||
|
|
||||||
<div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
|
<div style={{ flex: '1 1 auto', marginLeft: '8px', color: '#9baec8' }}>
|
||||||
<strong style={{ fontWeight: '500', display: 'block', color: '#fff' }}>{this.props.account.get('acct')}</strong>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -40,9 +40,7 @@ const UI = React.createClass({
|
||||||
<StatusListContainer type='mentions' />
|
<StatusListContainer type='mentions' />
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column>
|
{this.props.children}
|
||||||
{this.props.children}
|
|
||||||
</Column>
|
|
||||||
</ColumnsArea>
|
</ColumnsArea>
|
||||||
|
|
||||||
<NotificationsContainer />
|
<NotificationsContainer />
|
||||||
|
|
|
@ -30,6 +30,7 @@ import Immutable from 'immutable';
|
||||||
const initialState = Immutable.Map({
|
const initialState = Immutable.Map({
|
||||||
home: Immutable.List([]),
|
home: Immutable.List([]),
|
||||||
mentions: Immutable.List([]),
|
mentions: Immutable.List([]),
|
||||||
|
public: Immutable.List([]),
|
||||||
statuses: Immutable.Map(),
|
statuses: Immutable.Map(),
|
||||||
accounts: Immutable.Map(),
|
accounts: Immutable.Map(),
|
||||||
accounts_timelines: Immutable.Map(),
|
accounts_timelines: Immutable.Map(),
|
||||||
|
@ -110,7 +111,7 @@ function normalizeTimeline(state, timeline, statuses) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function appendNormalizedTimeline(state, timeline, statuses) {
|
function appendNormalizedTimeline(state, timeline, statuses) {
|
||||||
let moreIds = Immutable.List();
|
let moreIds = Immutable.List([]);
|
||||||
|
|
||||||
statuses.forEach((status, i) => {
|
statuses.forEach((status, i) => {
|
||||||
state = normalizeStatus(state, status);
|
state = normalizeStatus(state, status);
|
||||||
|
@ -121,29 +122,33 @@ function appendNormalizedTimeline(state, timeline, statuses) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeAccountTimeline(state, accountId, 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) => {
|
statuses.forEach((status, i) => {
|
||||||
state = normalizeStatus(state, status);
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
function appendNormalizedAccountTimeline(state, accountId, statuses) {
|
function appendNormalizedAccountTimeline(state, accountId, statuses) {
|
||||||
let moreIds = Immutable.List();
|
let moreIds = Immutable.List([]);
|
||||||
|
|
||||||
statuses.forEach((status, i) => {
|
statuses.forEach((status, i) => {
|
||||||
state = normalizeStatus(state, status);
|
state = normalizeStatus(state, status);
|
||||||
moreIds = moreIds.set(i, status.get('id'));
|
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) {
|
function updateTimeline(state, timeline, status) {
|
||||||
state = normalizeStatus(state, status);
|
state = normalizeStatus(state, status);
|
||||||
state = state.update(timeline, list => list.unshift(status.get('id')));
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -161,7 +166,7 @@ function deleteStatus(state, id) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove references from account timelines
|
// 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
|
// Remove reblogs of deleted status
|
||||||
const references = state.get('statuses').filter(item => item.get('reblog') === id);
|
const references = state.get('statuses').filter(item => item.get('reblog') === id);
|
||||||
|
|
|
@ -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
|
def home
|
||||||
@statuses = Feed.new(:home, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
|
@statuses = Feed.new(:home, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
|
||||||
|
render action: :index
|
||||||
end
|
end
|
||||||
|
|
||||||
def mentions
|
def mentions
|
||||||
@statuses = Feed.new(:mentions, current_user.account).get(20, params[:max_id], params[:since_id]).to_a
|
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,8 +6,8 @@ module HomeHelper
|
||||||
account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json),
|
account: render(file: 'api/v1/accounts/show', locals: { account: current_user.account }, formats: :json),
|
||||||
|
|
||||||
timelines: {
|
timelines: {
|
||||||
home: render(file: 'api/v1/statuses/home', locals: { statuses: @home }, formats: :json),
|
home: render(file: 'api/v1/statuses/index', locals: { statuses: @home }, formats: :json),
|
||||||
mentions: render(file: 'api/v1/statuses/mentions', locals: { statuses: @mentions }, formats: :json)
|
mentions: render(file: 'api/v1/statuses/index', locals: { statuses: @mentions }, formats: :json)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,22 +33,6 @@ class FeedManager
|
||||||
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
|
redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}")
|
||||||
end
|
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)
|
def inline_render(target_account, status)
|
||||||
rabl_scope = Class.new do
|
rabl_scope = Class.new do
|
||||||
include RoutingHelper
|
include RoutingHelper
|
||||||
|
@ -58,7 +42,7 @@ class FeedManager
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_user
|
def current_user
|
||||||
@account.user
|
@account.try(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
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
|
Rabl::Renderer.new('api/v1/statuses/show', status, view_path: 'app/views', format: :json, scope: rabl_scope.new(target_account)).render
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ class FanOutOnWriteService < BaseService
|
||||||
deliver_to_self(status) if status.account.local?
|
deliver_to_self(status) if status.account.local?
|
||||||
deliver_to_followers(status)
|
deliver_to_followers(status)
|
||||||
deliver_to_mentioned(status)
|
deliver_to_mentioned(status)
|
||||||
|
deliver_to_public(status)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -27,4 +28,8 @@ class FanOutOnWriteService < BaseService
|
||||||
FeedManager.instance.push(:mentions, mentioned_account, status)
|
FeedManager.instance.push(:mentions, mentioned_account, status)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def deliver_to_public(status)
|
||||||
|
FeedManager.instance.broadcast(:public, id: status.id)
|
||||||
|
end
|
||||||
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(:url) { |status| TagManager.instance.url_for(status) }
|
||||||
node(:reblogs_count) { |status| status.reblogs_count }
|
node(:reblogs_count) { |status| status.reblogs_count }
|
||||||
node(:favourites_count) { |status| status.favourites_count }
|
node(:favourites_count) { |status| status.favourites_count }
|
||||||
node(:favourited) { |status| current_account.favourited?(status) }
|
node(:favourited, if: proc { !current_account.nil? }) { |status| current_account.favourited?(status) }
|
||||||
node(:reblogged) { |status| current_account.reblogged?(status) }
|
node(:reblogged, if: proc { !current_account.nil? }) { |status| current_account.reblogged?(status) }
|
||||||
|
|
||||||
child :reblog => :reblog do
|
child :reblog => :reblog do
|
||||||
extends('api/v1/statuses/show')
|
extends('api/v1/statuses/show')
|
||||||
|
|
|
@ -48,6 +48,7 @@ Rails.application.routes.draw do
|
||||||
collection do
|
collection do
|
||||||
get :home
|
get :home
|
||||||
get :mentions
|
get :mentions
|
||||||
|
get :public
|
||||||
end
|
end
|
||||||
|
|
||||||
member do
|
member do
|
||||||
|
|
|
@ -47,6 +47,13 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
||||||
end
|
end
|
||||||
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
|
describe 'POST #create' do
|
||||||
before do
|
before do
|
||||||
post :create, params: { status: 'Hello world' }
|
post :create, params: { status: 'Hello world' }
|
||||||
|
|
Loading…
Reference in New Issue