diff --git a/app/assets/javascripts/components/actions/suggestions.jsx b/app/assets/javascripts/components/actions/suggestions.jsx index 562a901c5cd..c70a4d121e4 100644 --- a/app/assets/javascripts/components/actions/suggestions.jsx +++ b/app/assets/javascripts/components/actions/suggestions.jsx @@ -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 + }; +}; diff --git a/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx b/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx new file mode 100644 index 00000000000..289260f12bf --- /dev/null +++ b/app/assets/javascripts/components/features/compose/components/suggestions_box.jsx @@ -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 ( +
+ Who to follow + + {accounts.map(account => { + let displayName = account.get('display_name'); + + if (displayName.length === 0) { + displayName = account.get('username'); + } + + return ( + +
+ {displayName} + {account.get('acct')} + + ) + })} +
+ ); + } + +}); + +export default SuggestionsBox; diff --git a/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx b/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx new file mode 100644 index 00000000000..7163cb10047 --- /dev/null +++ b/app/assets/javascripts/components/features/compose/containers/suggestions_container.jsx @@ -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); diff --git a/app/assets/javascripts/components/features/compose/index.jsx b/app/assets/javascripts/components/features/compose/index.jsx index 4be93815854..d76afc43755 100644 --- a/app/assets/javascripts/components/features/compose/index.jsx +++ b/app/assets/javascripts/components/features/compose/index.jsx @@ -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 ( @@ -18,6 +29,7 @@ const Compose = React.createClass({ + ); @@ -25,4 +37,4 @@ const Compose = React.createClass({ }); -export default Compose; +export default connect()(Compose); diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 927ac28fd27..9fb84b585e9 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -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; } diff --git a/app/assets/javascripts/components/selectors/index.jsx b/app/assets/javascripts/components/selectors/index.jsx index c1317b38e24..c3c007f282b 100644 --- a/app/assets/javascripts/components/selectors/index.jsx +++ b/app/assets/javascripts/components/selectors/index.jsx @@ -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)); +}); diff --git a/app/models/follow.rb b/app/models/follow.rb index 1c52f24c02e..95b6bd146bf 100644 --- a/app/models/follow.rb +++ b/app/models/follow.rb @@ -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 diff --git a/app/models/follow_suggestion.rb b/app/models/follow_suggestion.rb index 2f291bc491d..f4515700abb 100644 --- a/app/models/follow_suggestion.rb +++ b/app/models/follow_suggestion.rb @@ -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