Adding public timeline
This commit is contained in:
		
							parent
							
								
									d51efdd1dc
								
							
						
					
					
						commit
						c3f5dfeabb
					
				
					 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' } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue