diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
index a48636d0e..8e3de078e 100644
--- a/app/soapbox/actions/admin.js
+++ b/app/soapbox/actions/admin.js
@@ -62,6 +62,14 @@ export const ADMIN_REMOVE_PERMISSION_GROUP_REQUEST = 'ADMIN_REMOVE_PERMISSION_GR
export const ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS = 'ADMIN_REMOVE_PERMISSION_GROUP_SUCCESS';
export const ADMIN_REMOVE_PERMISSION_GROUP_FAIL = 'ADMIN_REMOVE_PERMISSION_GROUP_FAIL';
+export const ADMIN_USERS_SUGGEST_REQUEST = 'ADMIN_USERS_SUGGEST_REQUEST';
+export const ADMIN_USERS_SUGGEST_SUCCESS = 'ADMIN_USERS_SUGGEST_SUCCESS';
+export const ADMIN_USERS_SUGGEST_FAIL = 'ADMIN_USERS_SUGGEST_FAIL';
+
+export const ADMIN_USERS_UNSUGGEST_REQUEST = 'ADMIN_USERS_UNSUGGEST_REQUEST';
+export const ADMIN_USERS_UNSUGGEST_SUCCESS = 'ADMIN_USERS_UNSUGGEST_SUCCESS';
+export const ADMIN_USERS_UNSUGGEST_FAIL = 'ADMIN_USERS_UNSUGGEST_FAIL';
+
const nicknamesFromIds = (getState, ids) => ids.map(id => getState().getIn(['accounts', id, 'acct']));
export function fetchConfig() {
@@ -319,3 +327,31 @@ export function demoteToUser(accountId) {
]);
};
}
+
+export function suggestUsers(accountIds) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_USERS_SUGGEST_REQUEST, accountIds });
+ return api(getState)
+ .patch('/api/pleroma/admin/users/suggest', { nicknames })
+ .then(({ data: { users } }) => {
+ dispatch({ type: ADMIN_USERS_SUGGEST_SUCCESS, users, accountIds });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_SUGGEST_FAIL, error, accountIds });
+ });
+ };
+}
+
+export function unsuggestUsers(accountIds) {
+ return (dispatch, getState) => {
+ const nicknames = nicknamesFromIds(getState, accountIds);
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_REQUEST, accountIds });
+ return api(getState)
+ .patch('/api/pleroma/admin/users/unsuggest', { nicknames })
+ .then(({ data: { users } }) => {
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_SUCCESS, users, accountIds });
+ }).catch(error => {
+ dispatch({ type: ADMIN_USERS_UNSUGGEST_FAIL, error, accountIds });
+ });
+ };
+}
diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
index 5334c9784..50e44d07e 100644
--- a/app/soapbox/features/account/components/header.js
+++ b/app/soapbox/features/account/components/header.js
@@ -68,6 +68,8 @@ const messages = defineMessages({
demoteToUser: { id: 'admin.users.actions.demote_to_user', defaultMessage: 'Demote @{name} to a regular user' },
subscribe: { id: 'account.subscribe', defaultMessage: 'Subscribe to notifications from @{name}' },
unsubscribe: { id: 'account.unsubscribe', defaultMessage: 'Unsubscribe to notifications from @{name}' },
+ suggestUser: { id: 'admin.users.actions.suggest_user', defaultMessage: 'Suggest @{name}' },
+ unsuggestUser: { id: 'admin.users.actions.unsuggest_user', defaultMessage: 'Unsuggest @{name}' },
});
const mapStateToProps = state => {
@@ -405,6 +407,20 @@ class Header extends ImmutablePureComponent {
});
}
+ if (account.getIn(['pleroma', 'is_suggested'])) {
+ menu.push({
+ text: intl.formatMessage(messages.unsuggestUser, { name: account.get('username') }),
+ action: this.props.onUnsuggestUser,
+ icon: require('@tabler/icons/icons/user-x.svg'),
+ });
+ } else {
+ menu.push({
+ text: intl.formatMessage(messages.suggestUser, { name: account.get('username') }),
+ action: this.props.onSuggestUser,
+ icon: require('@tabler/icons/icons/user-check.svg'),
+ });
+ }
+
if (account.get('id') !== me) {
menu.push({
text: intl.formatMessage(messages.deactivateUser, { name: account.get('username') }),
diff --git a/app/soapbox/features/account_timeline/components/header.js b/app/soapbox/features/account_timeline/components/header.js
index aba9af207..e2b9d3207 100644
--- a/app/soapbox/features/account_timeline/components/header.js
+++ b/app/soapbox/features/account_timeline/components/header.js
@@ -117,6 +117,14 @@ export default class Header extends ImmutablePureComponent {
this.props.onDemoteToUser(this.props.account);
}
+ handleSuggestUser = () => {
+ this.props.onSuggestUser(this.props.account);
+ }
+
+ handleUnsuggestUser = () => {
+ this.props.onUnsuggestUser(this.props.account);
+ }
+
render() {
const { account, identity_proofs } = this.props;
const moved = (account) ? account.get('moved') : false;
@@ -148,6 +156,8 @@ export default class Header extends ImmutablePureComponent {
onPromoteToAdmin={this.handlePromoteToAdmin}
onPromoteToModerator={this.handlePromoteToModerator}
onDemoteToUser={this.handleDemoteToUser}
+ onSuggestUser={this.handleSuggestUser}
+ onUnsuggestUser={this.handleUnsuggestUser}
username={this.props.username}
/>
diff --git a/app/soapbox/features/account_timeline/containers/header_container.js b/app/soapbox/features/account_timeline/containers/header_container.js
index 7ce9f8e9a..cd6fa0d84 100644
--- a/app/soapbox/features/account_timeline/containers/header_container.js
+++ b/app/soapbox/features/account_timeline/containers/header_container.js
@@ -32,6 +32,8 @@ import {
promoteToAdmin,
promoteToModerator,
demoteToUser,
+ suggestUsers,
+ unsuggestUsers,
} from 'soapbox/actions/admin';
import { isAdmin } from 'soapbox/utils/accounts';
import snackbar from 'soapbox/actions/snackbar';
@@ -47,7 +49,8 @@ const messages = defineMessages({
promotedToModerator: { id: 'admin.users.actions.promote_to_moderator_message', defaultMessage: '@{acct} was promoted to a moderator' },
demotedToModerator: { id: 'admin.users.actions.demote_to_moderator_message', defaultMessage: '@{acct} was demoted to a moderator' },
demotedToUser: { id: 'admin.users.actions.demote_to_user_message', defaultMessage: '@{acct} was demoted to a regular user' },
-
+ userSuggested: { id: 'admin.users.user_suggested_message', defaultMessage: '@{acct} was suggested' },
+ userUnsuggested: { id: 'admin.users.user_unsuggested_message', defaultMessage: '@{acct} was unsuggested' },
});
const makeMapStateToProps = () => {
@@ -213,6 +216,22 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
.then(() => dispatch(snackbar.success(message)))
.catch(() => {});
},
+
+ onSuggestUser(account) {
+ const message = intl.formatMessage(messages.userSuggested, { acct: account.get('acct') });
+
+ dispatch(suggestUsers([account.get('id')]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
+
+ onUnsuggestUser(account) {
+ const message = intl.formatMessage(messages.userUnsuggested, { acct: account.get('acct') });
+
+ dispatch(unsuggestUsers([account.get('id')]))
+ .then(() => dispatch(snackbar.success(message)))
+ .catch(() => {});
+ },
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header));
diff --git a/app/soapbox/pages/default_page.js b/app/soapbox/pages/default_page.js
index 5d7a26e76..2e7520357 100644
--- a/app/soapbox/pages/default_page.js
+++ b/app/soapbox/pages/default_page.js
@@ -62,6 +62,9 @@ class DefaultPage extends ImmutablePureComponent {
{Component => }
)}
+
+ {Component => }
+
{showTrendsPanel && (
{Component => }
@@ -72,9 +75,6 @@ class DefaultPage extends ImmutablePureComponent {
{Component => }
)}
-
- {Component => }
-
diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js
index 62db48263..96493e5ac 100644
--- a/app/soapbox/pages/home_page.js
+++ b/app/soapbox/pages/home_page.js
@@ -96,16 +96,6 @@ class HomePage extends ImmutablePureComponent {
{Component => }
)}
- {showTrendsPanel && (
-
- {Component => }
-
- )}
- {showWhoToFollowPanel && (
-
- {Component => }
-
- )}
{Component => }
@@ -119,6 +109,16 @@ class HomePage extends ImmutablePureComponent {
{Component => }
)}
+ {showTrendsPanel && (
+
+ {Component => }
+
+ )}
+ {showWhoToFollowPanel && (
+
+ {Component => }
+
+ )}
diff --git a/app/soapbox/pages/status_page.js b/app/soapbox/pages/status_page.js
index fa28193ca..c20ea91d3 100644
--- a/app/soapbox/pages/status_page.js
+++ b/app/soapbox/pages/status_page.js
@@ -63,6 +63,9 @@ class StatusPage extends ImmutablePureComponent {
{Component => }
)}
+
+ {Component => }
+
{showTrendsPanel && (
{Component => }
@@ -73,9 +76,6 @@ class StatusPage extends ImmutablePureComponent {
{Component => }
)}
-
- {Component => }
-
diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js
index fb0ac3ca6..70b9bb98f 100644
--- a/app/soapbox/reducers/accounts.js
+++ b/app/soapbox/reducers/accounts.js
@@ -30,6 +30,10 @@ import {
ADMIN_USERS_DELETE_FAIL,
ADMIN_USERS_DEACTIVATE_REQUEST,
ADMIN_USERS_DEACTIVATE_FAIL,
+ ADMIN_USERS_SUGGEST_REQUEST,
+ ADMIN_USERS_SUGGEST_FAIL,
+ ADMIN_USERS_UNSUGGEST_REQUEST,
+ ADMIN_USERS_UNSUGGEST_FAIL,
} from 'soapbox/actions/admin';
const initialState = ImmutableMap();
@@ -185,6 +189,14 @@ const importAdminUsers = (state, adminUsers) => {
});
};
+const setSuggested = (state, accountIds, isSuggested) => {
+ return state.withMutations(state => {
+ accountIds.forEach(id => {
+ state.setIn([id, 'pleroma', 'is_suggested'], isSuggested);
+ });
+ });
+};
+
export default function accounts(state = initialState, action) {
switch(action.type) {
case ACCOUNT_IMPORT:
@@ -224,6 +236,12 @@ export default function accounts(state = initialState, action) {
return setActive(state, action.accountIds, true);
case ADMIN_USERS_FETCH_SUCCESS:
return importAdminUsers(state, action.users);
+ case ADMIN_USERS_SUGGEST_REQUEST:
+ case ADMIN_USERS_UNSUGGEST_FAIL:
+ return setSuggested(state, action.accountIds, true);
+ case ADMIN_USERS_UNSUGGEST_REQUEST:
+ case ADMIN_USERS_SUGGEST_FAIL:
+ return setSuggested(state, action.accountIds, false);
default:
return state;
}
diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js
index e36892e76..bcab0098b 100644
--- a/app/soapbox/utils/features.js
+++ b/app/soapbox/utils/features.js
@@ -24,8 +24,14 @@ export const getFeatures = createSelector([
v.software === MASTODON && gte(v.compatVersion, '2.1.0'),
v.software === PLEROMA && gte(v.version, '0.9.9'),
]),
- suggestions: v.software === MASTODON && gte(v.compatVersion, '2.4.3'),
- suggestionsV2: v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
+ suggestions: any([
+ v.software === MASTODON && gte(v.compatVersion, '2.4.3'),
+ features.includes('v2_suggestions'),
+ ]),
+ suggestionsV2: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.4.0'),
+ features.includes('v2_suggestions'),
+ ]),
trends: v.software === MASTODON && gte(v.compatVersion, '3.0.0'),
mediaV2: any([
v.software === MASTODON && gte(v.compatVersion, '3.1.3'),