Display follow suggestions
This commit is contained in:
		
					parent
					
						
							
								e21a3fe0cd
							
						
					
				
			
			
				commit
				
					
						20f581f796
					
				
			
		
					 8 changed files with 146 additions and 6 deletions
				
			
		|  | @ -6,10 +6,32 @@ export const SUGGESTIONS_FETCH_FAIL    = 'SUGGESTIONS_FETCH_FAIL'; | |||
| 
 | ||||
| export function fetchSuggestions() { | ||||
|   return (dispatch, getState) => { | ||||
|     dispatch(fetchSuggestionsRequest()); | ||||
| 
 | ||||
|     api(getState).get('/api/v1/accounts/suggestions').then(response => { | ||||
|       console.log(response.data); | ||||
|       dispatch(fetchSuggestionsSuccess(response.data)); | ||||
|     }).catch(error => { | ||||
|       console.error(error); | ||||
|       dispatch(fetchSuggestionsFail(error)); | ||||
|     }); | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchSuggestionsRequest() { | ||||
|   return { | ||||
|     type: SUGGESTIONS_FETCH_REQUEST | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchSuggestionsSuccess(suggestions) { | ||||
|   return { | ||||
|     type: SUGGESTIONS_FETCH_SUCCESS, | ||||
|     suggestions: suggestions | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function fetchSuggestionsFail(error) { | ||||
|   return { | ||||
|     type: SUGGESTIONS_FETCH_FAIL, | ||||
|     error: error | ||||
|   }; | ||||
| }; | ||||
|  |  | |||
|  | @ -0,0 +1,76 @@ | |||
| import PureRenderMixin    from 'react-addons-pure-render-mixin'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import Avatar             from '../../../components/avatar'; | ||||
| import DisplayName        from '../../../components/display_name'; | ||||
| import { Link }           from 'react-router'; | ||||
| 
 | ||||
| const outerStyle = { | ||||
|   marginBottom: '10px', | ||||
|   borderTop: '1px solid #616b86' | ||||
| }; | ||||
| 
 | ||||
| const headerStyle = { | ||||
|   fontSize: '14px', | ||||
|   fontWeight: '500', | ||||
|   display: 'block', | ||||
|   padding: '10px', | ||||
|   color: '#9baec8', | ||||
|   background: '#454b5e', | ||||
|   width: '120px', | ||||
|   marginTop: '-18px' | ||||
| }; | ||||
| 
 | ||||
| const itemStyle = { | ||||
|   display: 'block', | ||||
|   padding: '10px', | ||||
|   color: '#9baec8', | ||||
|   overflow: 'hidden', | ||||
|   textDecoration: 'none' | ||||
| }; | ||||
| 
 | ||||
| const displayNameStyle = { | ||||
|   display: 'block', | ||||
|   fontWeight: '500' | ||||
| }; | ||||
| 
 | ||||
| const acctStyle = { | ||||
|   display: 'block' | ||||
| }; | ||||
| 
 | ||||
| const SuggestionsBox = React.createClass({ | ||||
| 
 | ||||
|   propTypes: { | ||||
|     accounts: ImmutablePropTypes.list.isRequired | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
| 
 | ||||
|   render () { | ||||
|     const accounts = this.props.accounts.take(2); | ||||
| 
 | ||||
|     return ( | ||||
|       <div style={outerStyle}> | ||||
|         <strong style={headerStyle}>Who to follow</strong> | ||||
| 
 | ||||
|         {accounts.map(account => { | ||||
|           let displayName = account.get('display_name'); | ||||
| 
 | ||||
|           if (displayName.length === 0) { | ||||
|             displayName = account.get('username'); | ||||
|           } | ||||
| 
 | ||||
|           return ( | ||||
|             <Link key={account.get('id')} style={itemStyle} to={`/accounts/${account.get('id')}`}> | ||||
|               <div style={{ float: 'left', marginRight: '10px' }}><Avatar src={account.get('avatar')} size={36} /></div> | ||||
|               <strong style={displayNameStyle}>{displayName}</strong> | ||||
|               <span style={acctStyle}>{account.get('acct')}</span> | ||||
|             </Link> | ||||
|           ) | ||||
|         })} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default SuggestionsBox; | ||||
|  | @ -0,0 +1,9 @@ | |||
| import { connect }           from 'react-redux'; | ||||
| import { getSuggestions }    from '../../../selectors'; | ||||
| import SuggestionsBox        from '../components/suggestions_box'; | ||||
| 
 | ||||
| const mapStateToProps = (state) => ({ | ||||
|   accounts: getSuggestions(state) | ||||
| }); | ||||
| 
 | ||||
| export default connect(mapStateToProps)(SuggestionsBox); | ||||
|  | @ -4,11 +4,22 @@ import FollowFormContainer  from '../ui/containers/follow_form_container'; | |||
| import UploadFormContainer  from '../ui/containers/upload_form_container'; | ||||
| import NavigationContainer  from '../ui/containers/navigation_container'; | ||||
| import PureRenderMixin      from 'react-addons-pure-render-mixin'; | ||||
| import SuggestionsContainer from './containers/suggestions_container'; | ||||
| import { fetchSuggestions } from '../../actions/suggestions'; | ||||
| import { connect }          from 'react-redux'; | ||||
| 
 | ||||
| const Compose = React.createClass({ | ||||
| 
 | ||||
|   propTypes: { | ||||
|     dispatch: React.PropTypes.func.isRequired | ||||
|   }, | ||||
| 
 | ||||
|   mixins: [PureRenderMixin], | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.props.dispatch(fetchSuggestions()); | ||||
|   }, | ||||
| 
 | ||||
|   render () { | ||||
|     return ( | ||||
|       <Drawer> | ||||
|  | @ -18,6 +29,7 @@ const Compose = React.createClass({ | |||
|           <UploadFormContainer /> | ||||
|         </div> | ||||
| 
 | ||||
|         <SuggestionsContainer /> | ||||
|         <FollowFormContainer /> | ||||
|       </Drawer> | ||||
|     ); | ||||
|  | @ -25,4 +37,4 @@ const Compose = React.createClass({ | |||
| 
 | ||||
| }); | ||||
| 
 | ||||
| export default Compose; | ||||
| export default connect()(Compose); | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import { | |||
|   STATUS_DELETE_SUCCESS | ||||
| }                                from '../actions/statuses'; | ||||
| import { FOLLOW_SUBMIT_SUCCESS } from '../actions/follow'; | ||||
| import { SUGGESTIONS_FETCH_SUCCESS } from '../actions/suggestions'; | ||||
| import Immutable                 from 'immutable'; | ||||
| 
 | ||||
| const initialState = Immutable.Map({ | ||||
|  | @ -37,7 +38,8 @@ const initialState = Immutable.Map({ | |||
|   me: null, | ||||
|   ancestors: Immutable.Map(), | ||||
|   descendants: Immutable.Map(), | ||||
|   relationships: Immutable.Map() | ||||
|   relationships: Immutable.Map(), | ||||
|   suggestions: Immutable.List([]) | ||||
| }); | ||||
| 
 | ||||
| function normalizeStatus(state, status) { | ||||
|  | @ -189,6 +191,14 @@ function normalizeContext(state, status, ancestors, descendants) { | |||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function normalizeSuggestions(state, accounts) { | ||||
|   accounts.forEach(account => { | ||||
|     state = state.setIn(['accounts', account.get('id')], account); | ||||
|   }); | ||||
| 
 | ||||
|   return state.set('suggestions', accounts.map(account => account.get('id'))); | ||||
| }; | ||||
| 
 | ||||
| export default function timelines(state = initialState, action) { | ||||
|   switch(action.type) { | ||||
|     case TIMELINE_REFRESH_SUCCESS: | ||||
|  | @ -221,6 +231,8 @@ export default function timelines(state = initialState, action) { | |||
|       return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); | ||||
|     case ACCOUNT_TIMELINE_EXPAND_SUCCESS: | ||||
|       return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); | ||||
|     case SUGGESTIONS_FETCH_SUCCESS: | ||||
|       return normalizeSuggestions(state, Immutable.fromJS(action.suggestions)); | ||||
|     default: | ||||
|       return state; | ||||
|   } | ||||
|  |  | |||
|  | @ -79,3 +79,9 @@ export const getNotifications = createSelector([getNotificationsBase], (base) => | |||
| 
 | ||||
|   return arr; | ||||
| }); | ||||
| 
 | ||||
| const getSuggestionsBase = (state) => state.getIn(['timelines', 'suggestions']); | ||||
| 
 | ||||
| export const getSuggestions = createSelector([getSuggestionsBase, getAccounts], (base, accounts) => { | ||||
|   return base.map(accountId => accounts.get(accountId)); | ||||
| }); | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ class Follow < ApplicationRecord | |||
|     b = neo.create_unique_node('account_index', 'Account', target_account_id.to_s, account_id: target_account_id) | ||||
| 
 | ||||
|     neo.create_unique_relationship('follow_index', 'Follow', id.to_s, 'follows', a, b) | ||||
|   rescue Neography::NeographyError => e | ||||
|   rescue Neography::NeographyError, Excon::Error::Socket => e | ||||
|     Rails.logger.error e | ||||
|   end | ||||
| 
 | ||||
|  | @ -43,7 +43,7 @@ class Follow < ApplicationRecord | |||
|     neo = Neography::Rest.new | ||||
|     rel = neo.get_relationship_index('follow_index', 'Follow', id.to_s) | ||||
|     neo.delete_relationship(rel) | ||||
|   rescue Neography::NeographyError => e | ||||
|   rescue Neography::NeographyError, Excon::Error::Socket => e | ||||
|     Rails.logger.error e | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -3,5 +3,8 @@ class FollowSuggestion | |||
|     neo = Neography::Rest.new | ||||
|     account_ids = neo.execute_query('START a=node:account_index(Account={id}) MATCH (a)-[:follows]->(b)-[:follows]->(c) WHERE a <> c AND NOT (a)-[:follows]->(c) RETURN DISTINCT c.account_id', id: for_account_id) | ||||
|     Account.where(id: account_ids['data'].first) unless account_ids.empty? | ||||
|   rescue Neography::NeographyError, Excon::Error::Socket => e | ||||
|     Rails.logger.error e | ||||
|     [] | ||||
|   end | ||||
| end | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue