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'),