about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Dockerfile2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/javascript/mastodon/actions/compose.js4
-rw-r--r--app/javascript/mastodon/actions/suggestions.js52
-rw-r--r--app/javascript/mastodon/components/account.js13
-rw-r--r--app/javascript/mastodon/components/display_name.js3
-rw-r--r--app/javascript/mastodon/features/compose/components/search_results.js43
-rw-r--r--app/javascript/mastodon/features/compose/containers/search_results_container.js9
-rw-r--r--app/javascript/mastodon/reducers/index.js2
-rw-r--r--app/javascript/mastodon/reducers/suggestions.js30
-rw-r--r--docker-compose.yml17
12 files changed, 158 insertions, 23 deletions
diff --git a/Dockerfile b/Dockerfile
index bdc10c4ab..3e54b6555 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 FROM node:8.12.0-alpine as node
-FROM ruby:2.4.4-alpine3.7
+FROM ruby:2.4.5-alpine3.8
 
 LABEL maintainer="https://github.com/tootsuite/mastodon" \
       description="Your self-hosted, globally interconnected microblogging community"
diff --git a/Gemfile b/Gemfile
index 950e666de..ee9b746a6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -115,7 +115,7 @@ group :test do
   gem 'rspec-sidekiq', '~> 3.0'
   gem 'simplecov', '~> 0.16', require: false
   gem 'webmock', '~> 3.4'
-  gem 'parallel_tests', '~> 2.23'
+  gem 'parallel_tests', '~> 2.24'
 end
 
 group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index c2af2719e..283a6c25d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -387,7 +387,7 @@ GEM
       av (~> 0.9.0)
       paperclip (>= 2.5.2)
     parallel (1.12.1)
-    parallel_tests (2.23.0)
+    parallel_tests (2.24.0)
       parallel
     parser (2.5.1.2)
       ast (~> 2.4.0)
@@ -717,7 +717,7 @@ DEPENDENCIES
   ox (~> 2.10)
   paperclip (~> 6.0)
   paperclip-av-transcoder (~> 0.6)
-  parallel_tests (~> 2.23)
+  parallel_tests (~> 2.24)
   pg (~> 1.1)
   pghero (~> 2.2)
   pkg-config (~> 1.3)
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index f72671228..fac8d32a1 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -146,7 +146,9 @@ export function submitCompose(routerHistory) {
         routerHistory.push('/timelines/direct');
       } else if (response.data.visibility !== 'direct') {
         insertIfOnline('home');
-      } else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
+      }
+
+      if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
         insertIfOnline('community');
         insertIfOnline('public');
       }
diff --git a/app/javascript/mastodon/actions/suggestions.js b/app/javascript/mastodon/actions/suggestions.js
new file mode 100644
index 000000000..b15bd916b
--- /dev/null
+++ b/app/javascript/mastodon/actions/suggestions.js
@@ -0,0 +1,52 @@
+import api from '../api';
+import { importFetchedAccounts } from './importer';
+
+export const SUGGESTIONS_FETCH_REQUEST = 'SUGGESTIONS_FETCH_REQUEST';
+export const SUGGESTIONS_FETCH_SUCCESS = 'SUGGESTIONS_FETCH_SUCCESS';
+export const SUGGESTIONS_FETCH_FAIL    = 'SUGGESTIONS_FETCH_FAIL';
+
+export const SUGGESTIONS_DISMISS = 'SUGGESTIONS_DISMISS';
+
+export function fetchSuggestions() {
+  return (dispatch, getState) => {
+    dispatch(fetchSuggestionsRequest());
+
+    api(getState).get('/api/v1/suggestions').then(response => {
+      dispatch(importFetchedAccounts(response.data));
+      dispatch(fetchSuggestionsSuccess(response.data));
+    }).catch(error => dispatch(fetchSuggestionsFail(error)));
+  };
+};
+
+export function fetchSuggestionsRequest() {
+  return {
+    type: SUGGESTIONS_FETCH_REQUEST,
+    skipLoading: true,
+  };
+};
+
+export function fetchSuggestionsSuccess(accounts) {
+  return {
+    type: SUGGESTIONS_FETCH_SUCCESS,
+    accounts,
+    skipLoading: true,
+  };
+};
+
+export function fetchSuggestionsFail(error) {
+  return {
+    type: SUGGESTIONS_FETCH_FAIL,
+    error,
+    skipLoading: true,
+    skipAlert: true,
+  };
+};
+
+export const dismissSuggestion = accountId => (dispatch, getState) => {
+  dispatch({
+    type: SUGGESTIONS_DISMISS,
+    id: accountId,
+  });
+
+  api(getState).delete(`/api/v1/suggestions/${accountId}`);
+};
diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js
index c021e3267..2bcea8b67 100644
--- a/app/javascript/mastodon/components/account.js
+++ b/app/javascript/mastodon/components/account.js
@@ -30,6 +30,9 @@ class Account extends ImmutablePureComponent {
     onMuteNotifications: PropTypes.func.isRequired,
     intl: PropTypes.object.isRequired,
     hidden: PropTypes.bool,
+    actionIcon: PropTypes.string,
+    actionTitle: PropTypes.string,
+    onActionClick: PropTypes.func,
   };
 
   handleFollow = () => {
@@ -52,8 +55,12 @@ class Account extends ImmutablePureComponent {
     this.props.onMuteNotifications(this.props.account, false);
   }
 
+  handleAction = () => {
+    this.props.onActionClick(this.props.account);
+  }
+
   render () {
-    const { account, intl, hidden } = this.props;
+    const { account, intl, hidden, onActionClick, actionIcon, actionTitle } = this.props;
 
     if (!account) {
       return <div />;
@@ -70,7 +77,9 @@ class Account extends ImmutablePureComponent {
 
     let buttons;
 
-    if (account.get('id') !== me && account.get('relationship', null) !== null) {
+    if (onActionClick && actionIcon) {
+      buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
+    } else if (account.get('id') !== me && account.get('relationship', null) !== null) {
       const following = account.getIn(['relationship', 'following']);
       const requested = account.getIn(['relationship', 'requested']);
       const blocking  = account.getIn(['relationship', 'blocking']);
diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js
index 31efab80a..c2c40cb3f 100644
--- a/app/javascript/mastodon/components/display_name.js
+++ b/app/javascript/mastodon/components/display_name.js
@@ -22,8 +22,7 @@ export default class DisplayName extends React.PureComponent {
 
     return (
       <span className='display-name'>
-        <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi>
-        <span>{suffix}</span>
+        <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
       </span>
     );
   }
diff --git a/app/javascript/mastodon/features/compose/components/search_results.js b/app/javascript/mastodon/features/compose/components/search_results.js
index c351b84bb..f0ddf6d71 100644
--- a/app/javascript/mastodon/features/compose/components/search_results.js
+++ b/app/javascript/mastodon/features/compose/components/search_results.js
@@ -1,19 +1,56 @@
 import React from 'react';
+import PropTypes from 'prop-types';
 import ImmutablePropTypes from 'react-immutable-proptypes';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 import AccountContainer from '../../../containers/account_container';
 import StatusContainer from '../../../containers/status_container';
 import ImmutablePureComponent from 'react-immutable-pure-component';
 import Hashtag from '../../../components/hashtag';
 
-export default class SearchResults extends ImmutablePureComponent {
+const messages = defineMessages({
+  dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
+});
+
+export default @injectIntl
+class SearchResults extends ImmutablePureComponent {
 
   static propTypes = {
     results: ImmutablePropTypes.map.isRequired,
+    suggestions: ImmutablePropTypes.list.isRequired,
+    fetchSuggestions: PropTypes.func.isRequired,
+    dismissSuggestion: PropTypes.func.isRequired,
+    intl: PropTypes.object.isRequired,
   };
 
+  componentDidMount () {
+    this.props.fetchSuggestions();
+  }
+
   render () {
-    const { results } = this.props;
+    const { intl, results, suggestions, dismissSuggestion } = this.props;
+
+    if (results.isEmpty() && !suggestions.isEmpty()) {
+      return (
+        <div className='search-results'>
+          <div className='trends'>
+            <div className='trends__header'>
+              <i className='fa fa-user-plus fa-fw' />
+              <FormattedMessage id='suggestions.header' defaultMessage='You might be interested in…' />
+            </div>
+
+            {suggestions && suggestions.map(accountId => (
+              <AccountContainer
+                key={accountId}
+                id={accountId}
+                actionIcon='times'
+                actionTitle={intl.formatMessage(messages.dismissSuggestion)}
+                onActionClick={dismissSuggestion}
+              />
+            ))}
+          </div>
+        </div>
+      );
+    }
 
     let accounts, statuses, hashtags;
     let count = 0;
diff --git a/app/javascript/mastodon/features/compose/containers/search_results_container.js b/app/javascript/mastodon/features/compose/containers/search_results_container.js
index 16d95d417..f9637861a 100644
--- a/app/javascript/mastodon/features/compose/containers/search_results_container.js
+++ b/app/javascript/mastodon/features/compose/containers/search_results_container.js
@@ -1,8 +1,15 @@
 import { connect } from 'react-redux';
 import SearchResults from '../components/search_results';
+import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestions';
 
 const mapStateToProps = state => ({
   results: state.getIn(['search', 'results']),
+  suggestions: state.getIn(['suggestions', 'items']),
 });
 
-export default connect(mapStateToProps)(SearchResults);
+const mapDispatchToProps = dispatch => ({
+  fetchSuggestions: () => dispatch(fetchSuggestions()),
+  dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index d3b98d4f6..e98566e26 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -28,6 +28,7 @@ import lists from './lists';
 import listEditor from './list_editor';
 import filters from './filters';
 import conversations from './conversations';
+import suggestions from './suggestions';
 
 const reducers = {
   dropdown_menu,
@@ -59,6 +60,7 @@ const reducers = {
   listEditor,
   filters,
   conversations,
+  suggestions,
 };
 
 export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/reducers/suggestions.js b/app/javascript/mastodon/reducers/suggestions.js
new file mode 100644
index 000000000..9f4b89d58
--- /dev/null
+++ b/app/javascript/mastodon/reducers/suggestions.js
@@ -0,0 +1,30 @@
+import {
+  SUGGESTIONS_FETCH_REQUEST,
+  SUGGESTIONS_FETCH_SUCCESS,
+  SUGGESTIONS_FETCH_FAIL,
+  SUGGESTIONS_DISMISS,
+} from '../actions/suggestions';
+import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
+
+const initialState = ImmutableMap({
+  items: ImmutableList(),
+  isLoading: false,
+});
+
+export default function suggestionsReducer(state = initialState, action) {
+  switch(action.type) {
+  case SUGGESTIONS_FETCH_REQUEST:
+    return state.set('isLoading', true);
+  case SUGGESTIONS_FETCH_SUCCESS:
+    return state.withMutations(map => {
+      map.set('items', fromJS(action.accounts.map(x => x.id)));
+      map.set('isLoading', false);
+    });
+  case SUGGESTIONS_FETCH_FAIL:
+    return state.set('isLoading', false);
+  case SUGGESTIONS_DISMISS:
+    return state.update('items', list => list.filterNot(id => id === action.id));
+  default:
+    return state;
+  }
+};
diff --git a/docker-compose.yml b/docker-compose.yml
index 064d5a260..d9f80a38a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,18 +6,16 @@ services:
     image: postgres:9.6-alpine
     networks:
       - internal_network
-### Uncomment to enable DB persistence
-#    volumes:
-#      - ./postgres:/var/lib/postgresql/data
+    volumes:
+      - ./postgres:/var/lib/postgresql/data
 
   redis:
     restart: always
     image: redis:4.0-alpine
     networks:
       - internal_network
-### Uncomment to enable REDIS persistence
-#    volumes:
-#      - ./redis:/data
+    volumes:
+      - ./redis:/data
 
 #  es:
 #    restart: always
@@ -26,9 +24,8 @@ services:
 #      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
 #    networks:
 #      - internal_network
-#### Uncomment to enable ES persistence
-##    volumes:
-##      - ./elasticsearch:/usr/share/elasticsearch/data
+#    volumes:
+#      - ./elasticsearch:/usr/share/elasticsearch/data
 
   web:
     build: .
@@ -68,7 +65,7 @@ services:
     image: tootsuite/mastodon
     restart: always
     env_file: .env.production
-    command: bundle exec sidekiq -q default -q push -q mailers -q pull
+    command: bundle exec sidekiq
     depends_on:
       - db
       - redis