From d3daf63dd524129367b3218e55e1803da0d7eb06 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 12:34:23 -0600 Subject: [PATCH 01/13] Start admin area, create dashboard --- .../features/admin/components/admin_nav.js | 22 ++++++ app/soapbox/features/admin/index.js | 78 +++++++++++++++++++ app/soapbox/features/ui/index.js | 12 +++ .../features/ui/util/async-components.js | 4 + app/styles/application.scss | 1 + app/styles/components/admin.scss | 69 ++++++++++++++++ 6 files changed, 186 insertions(+) create mode 100644 app/soapbox/features/admin/components/admin_nav.js create mode 100644 app/soapbox/features/admin/index.js create mode 100644 app/styles/components/admin.scss diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js new file mode 100644 index 000000000..95a50e38e --- /dev/null +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -0,0 +1,22 @@ +import React from 'react'; +import Icon from 'soapbox/components/icon'; +import { NavLink } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; + +export default +class AdminNav extends React.PureComponent { + + render() { + return ( +
+
+ + + + +
+
+ ); + } + +} diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js new file mode 100644 index 000000000..96115b7b6 --- /dev/null +++ b/app/soapbox/features/admin/index.js @@ -0,0 +1,78 @@ +import React from 'react'; +import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import { parseVersion } from 'soapbox/utils/features'; + +const messages = defineMessages({ + heading: { id: 'column.admin.dashboard', defaultMessage: 'Dashboard' }, +}); + +const mapStateToProps = (state, props) => ({ + instance: state.get('instance'), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Dashboard extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + instance: ImmutablePropTypes.map.isRequired, + }; + + render() { + const { intl, instance } = this.props; + const v = parseVersion(instance.get('version')); + + return ( + +
+
+ +
+ +
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+

+
    +
  • Soapbox FE 1.1.0
  • +
  • {v.software} {v.version}
  • +
+
+
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 8b714fe7e..6d6080ef7 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -39,6 +39,7 @@ import Icon from 'soapbox/components/icon'; import { isStaff } from 'soapbox/utils/accounts'; import ChatPanes from 'soapbox/features/chats/components/chat_panes'; import ProfileHoverCard from 'soapbox/components/profile_hover_card'; +import AdminNav from 'soapbox/features/admin/components/admin_nav'; import { Status, @@ -86,6 +87,7 @@ import { ChatIndex, ChatRoom, ServerInfo, + Dashboard, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -154,6 +156,14 @@ const LAYOUT = { , ], }, + ADMIN: { + LEFT: [ + , + ], + RIGHT: [ + , + ], + }, STATUS: { TOP: null, LEFT: null, @@ -274,6 +284,8 @@ class SwitchingColumnsArea extends React.PureComponent { + + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 3cb9e5142..3421f2d9e 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -217,3 +217,7 @@ export function ChatRoom() { export function ServerInfo() { return import(/* webpackChunkName: "features/server_info" */'../../server_info'); } + +export function Dashboard() { + return import(/* webpackChunkName: "features/admin" */'../../admin'); +} diff --git a/app/styles/application.scss b/app/styles/application.scss index 3841d231e..91f5ec192 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -79,6 +79,7 @@ @import 'components/snackbar'; @import 'components/accordion'; @import 'components/server-info'; +@import 'components/admin'; // Holiday @import 'holiday/halloween'; diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss new file mode 100644 index 000000000..5d04d77ea --- /dev/null +++ b/app/styles/components/admin.scss @@ -0,0 +1,69 @@ +.dashcounters { + display: flex; + flex-wrap: wrap; + margin: 0 -5px 0; + padding: 20px; +} + +.dashcounter { + box-sizing: border-box; + flex: 0 0 33.333%; + padding: 0 5px; + margin-bottom: 10px; + + > a, + > div { + text-decoration: none; + color: inherit; + display: block; + padding: 20px; + background: var(--accent-color--faint); + border-radius: 4px; + transition: 0.2s; + } + + > a:hover { + background: var(--accent-color--med); + transform: translateY(-2px); + } + + &__num, + &__text { + text-align: center; + font-weight: 500; + font-size: 24px; + line-height: 30px; + color: var(--primary-text-color); + margin-bottom: 10px; + } + + &__label { + font-size: 14px; + color: hsla(var(--primary-text-color_hsl), 0.6); + text-align: center; + font-weight: 500; + } +} + +.dashwidgets { + display: flex; + flex-wrap: wrap; + margin: 0 -5px; + padding: 0 20px 20px 20px; +} + +.dashwidget { + flex: 1; + margin-bottom: 20px; + padding: 0 5px; + + h4 { + text-transform: uppercase; + font-size: 13px; + font-weight: 700; + color: #999; + padding-bottom: 8px; + margin-bottom: 8px; + border-bottom: 1px solid #cfeaf3; + } +} From 248a33e79a5b50ef5f8f48ec33d56f1ecf890680 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 12:44:29 -0600 Subject: [PATCH 02/13] Admin: add reports Drop domain_count because it's less important --- app/soapbox/features/admin/components/admin_nav.js | 4 ++++ app/soapbox/features/admin/index.js | 14 ++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 95a50e38e..39a1e41b0 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -14,6 +14,10 @@ class AdminNav extends React.PureComponent { + + + + ); diff --git a/app/soapbox/features/admin/index.js b/app/soapbox/features/admin/index.js index 96115b7b6..344a47371 100644 --- a/app/soapbox/features/admin/index.js +++ b/app/soapbox/features/admin/index.js @@ -13,6 +13,7 @@ const messages = defineMessages({ const mapStateToProps = (state, props) => ({ instance: state.get('instance'), + openReportCount: state.getIn(['admin', 'open_report_count']), }); export default @connect(mapStateToProps) @@ -22,6 +23,7 @@ class Dashboard extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, instance: ImmutablePropTypes.map.isRequired, + openReportCount: PropTypes.number, }; render() { @@ -32,7 +34,7 @@ class Dashboard extends ImmutablePureComponent {
From 62a4338cf3ffac23cfcb019d019fb7e21d731efe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 13:14:09 -0600 Subject: [PATCH 03/13] Admin: placeholding "Awaiting Approval" link --- .../features/admin/components/admin_nav.js | 21 ++++++++++++++++++- app/soapbox/features/admin/index.js | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 39a1e41b0..7bd4f41ec 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -1,12 +1,24 @@ import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import Icon from 'soapbox/components/icon'; import { NavLink } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; -export default +const mapStateToProps = (state, props) => ({ + instance: state.get('instance'), +}); + +export default @connect(mapStateToProps) class AdminNav extends React.PureComponent { + static propTypes = { + instance: ImmutablePropTypes.map.isRequired, + }; + render() { + const { instance } = this.props; + return ( + {/* TODO: Awaiting approval users count */}
From 0ccc113931eae73de6cf96bbdd4088440b31531c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 14:16:19 -0600 Subject: [PATCH 04/13] Admon: add dummy placeholder links --- .../features/admin/components/admin_nav.js | 79 +++++++++++++++---- app/styles/components/admin.scss | 4 +- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 7bd4f41ec..0469eb231 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -20,25 +20,70 @@ class AdminNav extends React.PureComponent { const { instance } = this.props; return ( -
-
- - - - - {/* TODO: Make this actually useful */} - {instance.get('approval_required') && ( - - - + <> +
+
+ + + + + + + - )} - - - - + {instance.get('approval_required') && ( + + + + + )} + + + + + + + + +
-
+
+
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + +
+
+ ); } diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index 5d04d77ea..c986d52e4 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -61,9 +61,9 @@ text-transform: uppercase; font-size: 13px; font-weight: 700; - color: #999; + color: hsla(var(--primary-text-color_hsl), 0.6); padding-bottom: 8px; margin-bottom: 8px; - border-bottom: 1px solid #cfeaf3; + border-bottom: 1px solid var(--accent-color--med); } } From c156dd7a0ddb48c2cf1b3d8a22ebc773f9444070 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 14:51:44 -0600 Subject: [PATCH 05/13] Admin: hide invite link when registrations are enabled --- app/soapbox/features/admin/components/admin_nav.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 0469eb231..d27bbfb80 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -37,14 +37,16 @@ class AdminNav extends React.PureComponent { )} + {!instance.get('registrations') && ( + + + + + )} - - - -
From 1ad3ea4437586b218613f257bafdfe14959ac727 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 15:55:04 -0600 Subject: [PATCH 06/13] Admin: awaiting approval basics --- app/soapbox/actions/admin.js | 17 +++++++ .../features/admin/awaiting_approval.js | 46 +++++++++++++++++++ .../features/admin/components/admin_nav.js | 4 +- app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 ++ app/soapbox/reducers/admin.js | 27 ++++++++++- 6 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/features/admin/awaiting_approval.js diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index cf3a5d0e7..118f26992 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -8,6 +8,10 @@ export const ADMIN_REPORTS_FETCH_REQUEST = 'ADMIN_REPORTS_FETCH_REQUEST'; export const ADMIN_REPORTS_FETCH_SUCCESS = 'ADMIN_REPORTS_FETCH_SUCCESS'; export const ADMIN_REPORTS_FETCH_FAIL = 'ADMIN_REPORTS_FETCH_FAIL'; +export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST'; +export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS'; +export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL'; + export function updateAdminConfig(params) { return (dispatch, getState) => { dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST }); @@ -33,3 +37,16 @@ export function fetchReports(params) { }); }; } + +export function fetchUsers(params) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_FETCH_REQUEST, params }); + return api(getState) + .get('/api/pleroma/admin/users', { params }) + .then(({ data }) => { + dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, data, params }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params }); + }); + }; +} diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js new file mode 100644 index 000000000..8e716d644 --- /dev/null +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -0,0 +1,46 @@ +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from '../ui/components/column'; +import { fetchUsers } from 'soapbox/actions/admin'; + +const messages = defineMessages({ + heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' }, +}); + +const mapStateToProps = state => { + const userIds = state.getIn(['admin', 'awaitingApproval']); + return { + users: userIds.map(id => state.getIn(['admin', 'users', id])), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class AwaitingApproval extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + users: ImmutablePropTypes.list.isRequired, + }; + + componentDidMount() { + this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' })); + } + + render() { + const { intl, users } = this.props; + + return ( + + {users.map((user, i) => ( +
{user.get('nickname')}
+ ))} +
+ ); + } + +} diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index d27bbfb80..12a53b3fd 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -32,10 +32,10 @@ class AdminNav extends React.PureComponent { {instance.get('approval_required') && ( - + - + )} {!instance.get('registrations') && ( diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 6d6080ef7..969aadc79 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -88,6 +88,7 @@ import { ChatRoom, ServerInfo, Dashboard, + AwaitingApproval, } from './util/async-components'; // Dummy import, to make sure that ends up in the application bundle. @@ -286,6 +287,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index 3421f2d9e..6e313b4d8 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -221,3 +221,7 @@ export function ServerInfo() { export function Dashboard() { return import(/* webpackChunkName: "features/admin" */'../../admin'); } + +export function AwaitingApproval() { + return import(/* webpackChunkName: "features/admin/awaiting_approval" */'../../admin/awaiting_approval'); +} diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 2a41666f6..f120d8794 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -1,11 +1,32 @@ -import { ADMIN_REPORTS_FETCH_SUCCESS } from '../actions/admin'; -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; +import { + ADMIN_REPORTS_FETCH_SUCCESS, + ADMIN_USERS_FETCH_SUCCESS, +} from '../actions/admin'; +import { + Map as ImmutableMap, + List as ImmutableList, + OrderedSet as ImmutableOrderedSet, + fromJS, +} from 'immutable'; const initialState = ImmutableMap({ reports: ImmutableList(), + users: ImmutableMap(), open_report_count: 0, + awaitingApproval: ImmutableOrderedSet(), }); +function importUsers(state, users) { + return state.withMutations(state => { + users.forEach(user => { + if (user.approval_pending) { + state.update('awaitingApproval', orderedSet => orderedSet.add(user.id)); + } + state.setIn(['users', user.id], fromJS(user)); + }); + }); +} + export default function admin(state = initialState, action) { switch(action.type) { case ADMIN_REPORTS_FETCH_SUCCESS: @@ -16,6 +37,8 @@ export default function admin(state = initialState, action) { } else { return state.set('reports', fromJS(action.data.reports)); } + case ADMIN_USERS_FETCH_SUCCESS: + return importUsers(state, action.data.users); default: return state; } From a3f208c1bee4bfbd803953f2bd0ea25c06887b35 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 18:22:31 -0600 Subject: [PATCH 07/13] Admin: make awaiting approval actions work --- app/soapbox/actions/admin.js | 34 +++++++++++++ .../features/admin/awaiting_approval.js | 50 ++++++++++++++++--- app/soapbox/reducers/admin.js | 28 ++++++++++- app/styles/components/admin.scss | 27 ++++++++++ 4 files changed, 130 insertions(+), 9 deletions(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 118f26992..4329aa670 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -12,6 +12,14 @@ export const ADMIN_USERS_FETCH_REQUEST = 'ADMIN_USERS_FETCH_REQUEST'; export const ADMIN_USERS_FETCH_SUCCESS = 'ADMIN_USERS_FETCH_SUCCESS'; export const ADMIN_USERS_FETCH_FAIL = 'ADMIN_USERS_FETCH_FAIL'; +export const ADMIN_USERS_DELETE_REQUEST = 'ADMIN_USERS_DELETE_REQUEST'; +export const ADMIN_USERS_DELETE_SUCCESS = 'ADMIN_USERS_DELETE_SUCCESS'; +export const ADMIN_USERS_DELETE_FAIL = 'ADMIN_USERS_DELETE_FAIL'; + +export const ADMIN_USERS_APPROVE_REQUEST = 'ADMIN_USERS_APPROVE_REQUEST'; +export const ADMIN_USERS_APPROVE_SUCCESS = 'ADMIN_USERS_APPROVE_SUCCESS'; +export const ADMIN_USERS_APPROVE_FAIL = 'ADMIN_USERS_APPROVE_FAIL'; + export function updateAdminConfig(params) { return (dispatch, getState) => { dispatch({ type: ADMIN_CONFIG_UPDATE_REQUEST }); @@ -50,3 +58,29 @@ export function fetchUsers(params) { }); }; } + +export function deleteUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_DELETE_REQUEST, nicknames }); + return api(getState) + .delete('/api/pleroma/admin/users', { data: { nicknames } }) + .then(({ data: nicknames }) => { + dispatch({ type: ADMIN_USERS_DELETE_SUCCESS, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_DELETE_FAIL, error, nicknames }); + }); + }; +} + +export function approveUsers(nicknames) { + return (dispatch, getState) => { + dispatch({ type: ADMIN_USERS_APPROVE_REQUEST, nicknames }); + return api(getState) + .patch('/api/pleroma/admin/users/approve', { nicknames }) + .then(({ data: { users } }) => { + dispatch({ type: ADMIN_USERS_APPROVE_SUCCESS, users, nicknames }); + }).catch(error => { + dispatch({ type: ADMIN_USERS_APPROVE_FAIL, error, nicknames }); + }); + }; +} diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 8e716d644..503e66809 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -5,16 +5,18 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; -import { fetchUsers } from 'soapbox/actions/admin'; +import IconButton from 'soapbox/components/icon_button'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { fetchUsers, deleteUsers, approveUsers } from 'soapbox/actions/admin'; const messages = defineMessages({ heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' }, }); const mapStateToProps = state => { - const userIds = state.getIn(['admin', 'awaitingApproval']); + const nicknames = state.getIn(['admin', 'awaitingApproval']); return { - users: userIds.map(id => state.getIn(['admin', 'users', id])), + users: nicknames.toList().map(nickname => state.getIn(['admin', 'users', nickname])), }; }; @@ -27,18 +29,52 @@ class AwaitingApproval extends ImmutablePureComponent { users: ImmutablePropTypes.list.isRequired, }; + state = { + isLoading: true, + } + componentDidMount() { - this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' })); + const { dispatch } = this.props; + const params = { page: 1, filters: 'local,need_approval' }; + dispatch(fetchUsers(params)) + .then(() => this.setState({ isLoading: false })) + .catch(() => {}); + } + + handleApprove = nickname => { + const { dispatch } = this.props; + return e => { + dispatch(approveUsers([nickname])); + }; + } + + handleReject = nickname => { + const { dispatch } = this.props; + return e => { + dispatch(deleteUsers([nickname])); + }; } render() { const { intl, users } = this.props; + const { isLoading } = this.state; return ( - {users.map((user, i) => ( -
{user.get('nickname')}
- ))} + + {users.map((user, i) => ( +
+
+
@{user.get('nickname')}
+
{user.get('registration_reason')}
+
+
+ + +
+
+ ))} +
); } diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index f120d8794..45a21cc67 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -1,6 +1,8 @@ import { ADMIN_REPORTS_FETCH_SUCCESS, ADMIN_USERS_FETCH_SUCCESS, + ADMIN_USERS_DELETE_SUCCESS, + ADMIN_USERS_APPROVE_SUCCESS, } from '../actions/admin'; import { Map as ImmutableMap, @@ -20,9 +22,27 @@ function importUsers(state, users) { return state.withMutations(state => { users.forEach(user => { if (user.approval_pending) { - state.update('awaitingApproval', orderedSet => orderedSet.add(user.id)); + state.update('awaitingApproval', orderedSet => orderedSet.add(user.nickname)); } - state.setIn(['users', user.id], fromJS(user)); + state.setIn(['users', user.nickname], fromJS(user)); + }); + }); +} + +function deleteUsers(state, nicknames) { + return state.withMutations(state => { + nicknames.forEach(nickname => { + state.update('awaitingApproval', orderedSet => orderedSet.delete(nickname)); + state.deleteIn(['users', nickname]); + }); + }); +} + +function approveUsers(state, users) { + return state.withMutations(state => { + users.forEach(user => { + state.update('awaitingApproval', orderedSet => orderedSet.delete(user.nickname)); + state.setIn(['users', user.nickname], fromJS(user)); }); }); } @@ -39,6 +59,10 @@ export default function admin(state = initialState, action) { } case ADMIN_USERS_FETCH_SUCCESS: return importUsers(state, action.data.users); + case ADMIN_USERS_DELETE_SUCCESS: + return deleteUsers(state, action.nicknames); + case ADMIN_USERS_APPROVE_SUCCESS: + return approveUsers(state, action.users); default: return state; } diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index c986d52e4..2712016e2 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -67,3 +67,30 @@ border-bottom: 1px solid var(--accent-color--med); } } + +.unapproved-account { + padding: 15px 20px; + font-size: 14px; + display: flex; + + &__nickname { + font-weight: bold; + } + + &__reason { + padding: 5px 0 5px 15px; + border-left: 3px solid hsla(var(--primary-text-color_hsl), 0.4); + color: var(--primary-text-color--faint); + } + + &__actions { + margin-left: auto; + padding-left: 20px; + display: flex; + flex-wrap: nowrap; + } +} + +.slist .item-list article:nth-child(2n-1) .unapproved-account { + background-color: hsla(var(--accent-color_hsl), 0.07); +} From 299c14adc8e311ac984010fb7c78c22263e0394f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 18:38:58 -0600 Subject: [PATCH 08/13] Admin: optimistic awaiting-approval actions --- app/soapbox/features/admin/awaiting_approval.js | 3 ++- app/soapbox/reducers/admin.js | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/awaiting_approval.js b/app/soapbox/features/admin/awaiting_approval.js index 503e66809..4486f4b27 100644 --- a/app/soapbox/features/admin/awaiting_approval.js +++ b/app/soapbox/features/admin/awaiting_approval.js @@ -11,6 +11,7 @@ import { fetchUsers, deleteUsers, approveUsers } from 'soapbox/actions/admin'; const messages = defineMessages({ heading: { id: 'column.admin.awaiting_approval', defaultMessage: 'Awaiting Approval' }, + emptyMessage: { id: 'admin.awaiting_approval.empty_message', defaultMessage: 'There is nobody waiting for approval. When a new user signs up, you can review them here.' }, }); const mapStateToProps = state => { @@ -61,7 +62,7 @@ class AwaitingApproval extends ImmutablePureComponent { return ( - + {users.map((user, i) => (
diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 45a21cc67..0b928dd5a 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -1,7 +1,9 @@ import { ADMIN_REPORTS_FETCH_SUCCESS, ADMIN_USERS_FETCH_SUCCESS, + ADMIN_USERS_DELETE_REQUEST, ADMIN_USERS_DELETE_SUCCESS, + ADMIN_USERS_APPROVE_REQUEST, ADMIN_USERS_APPROVE_SUCCESS, } from '../actions/admin'; import { @@ -59,8 +61,10 @@ export default function admin(state = initialState, action) { } case ADMIN_USERS_FETCH_SUCCESS: return importUsers(state, action.data.users); + case ADMIN_USERS_DELETE_REQUEST: case ADMIN_USERS_DELETE_SUCCESS: return deleteUsers(state, action.nicknames); + case ADMIN_USERS_APPROVE_REQUEST: case ADMIN_USERS_APPROVE_SUCCESS: return approveUsers(state, action.users); default: From 23a4a605a175028ccc73131058a94f4de27069b5 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 18:53:06 -0600 Subject: [PATCH 09/13] Admin: fix optimistic approve --- app/soapbox/reducers/admin.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index 0b928dd5a..c7c6be507 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -65,6 +65,7 @@ export default function admin(state = initialState, action) { case ADMIN_USERS_DELETE_SUCCESS: return deleteUsers(state, action.nicknames); case ADMIN_USERS_APPROVE_REQUEST: + return state.update('awaitingApproval', set => set.subtract(action.nicknames)); case ADMIN_USERS_APPROVE_SUCCESS: return approveUsers(state, action.users); default: From 19e1e15263ccc0fcd108ee0152f6ba076c6889c7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 18:59:31 -0600 Subject: [PATCH 10/13] Admin: fix reducer tests --- app/soapbox/reducers/__tests__/admin-test.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin-test.js index aa32ace8f..891b8cabe 100644 --- a/app/soapbox/reducers/__tests__/admin-test.js +++ b/app/soapbox/reducers/__tests__/admin-test.js @@ -1,11 +1,13 @@ import reducer from '../admin'; -import { fromJS } from 'immutable'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; describe('admin reducer', () => { it('should return the initial state', () => { expect(reducer(undefined, {})).toEqual(fromJS({ reports: [], open_report_count: 0, + users: ImmutableMap(), + awaitingApproval: ImmutableOrderedSet(), })); }); }); From 03344756e552b16c29790560135acd9aebde3bea Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 19:48:39 -0600 Subject: [PATCH 11/13] Admin: display awaiting-approval counter in nav --- app/soapbox/components/icon_with_counter.js | 23 ++++++++++++++++ .../features/admin/components/admin_nav.js | 12 ++++++--- app/styles/components/promo-panel.scss | 3 ++- app/styles/ui.scss | 26 +++++++++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 app/soapbox/components/icon_with_counter.js diff --git a/app/soapbox/components/icon_with_counter.js b/app/soapbox/components/icon_with_counter.js new file mode 100644 index 000000000..0daf608da --- /dev/null +++ b/app/soapbox/components/icon_with_counter.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Icon from 'soapbox/components/icon'; +import { shortNumberFormat } from 'soapbox/utils/numbers'; + +const IconWithCounter = ({ icon, count, fixedWidth }) => { + return ( +
+ + {count > 0 && + {shortNumberFormat(count)} + } +
+ ); +}; + +IconWithCounter.propTypes = { + icon: PropTypes.string.isRequired, + count: PropTypes.number.isRequired, + fixedWidth: PropTypes.bool, +}; + +export default IconWithCounter; diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 12a53b3fd..47076abf8 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -1,12 +1,16 @@ import React from 'react'; import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Icon from 'soapbox/components/icon'; +import IconWithCounter from 'soapbox/components/icon_with_counter'; import { NavLink } from 'react-router-dom'; import { FormattedMessage } from 'react-intl'; const mapStateToProps = (state, props) => ({ instance: state.get('instance'), + approvalCount: state.getIn(['admin', 'awaitingApproval']).count(), + reportsCount: state.getIn(['admin', 'open_report_count']), }); export default @connect(mapStateToProps) @@ -14,10 +18,12 @@ class AdminNav extends React.PureComponent { static propTypes = { instance: ImmutablePropTypes.map.isRequired, + approvalCount: PropTypes.number, + reportsCount: PropTypes.number, }; render() { - const { instance } = this.props; + const { instance, approvalCount, reportsCount } = this.props; return ( <> @@ -28,12 +34,12 @@ class AdminNav extends React.PureComponent { - + {instance.get('approval_required') && ( - + )} diff --git a/app/styles/components/promo-panel.scss b/app/styles/components/promo-panel.scss index e2030cee6..78625e338 100644 --- a/app/styles/components/promo-panel.scss +++ b/app/styles/components/promo-panel.scss @@ -28,7 +28,8 @@ } } - &__icon { + &__icon, + .icon-with-counter { margin-right: 12px; } } diff --git a/app/styles/ui.scss b/app/styles/ui.scss index a2ca21a06..0b4fa30d3 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -699,3 +699,29 @@ text-align: center; } } + +.icon-with-counter { + position: relative; + display: inline; + + &__counter { + @include font-montserrat; + @include font-size(14); + @include line-height(14); + position: absolute; + box-sizing: border-box; + left: 8px; + top: -12px; + min-width: 16px; + height: 16px; + padding: 1px 3px 0; + border-radius: 8px; + text-align: center; + color: #fff; + background: var(--accent-color); + + @media screen and (max-width: 895px) { + top: 0; + } + } +} From eec89aaeb5617ea4d4c7c31c7b9b1d4111360f60 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 20:26:26 -0600 Subject: [PATCH 12/13] Refactor TabsBar, refactor IconWithBadge, use Dashboard nav --- app/soapbox/components/icon_with_badge.js | 21 --------- .../ui/components/chats_counter_icon.js | 9 ---- .../ui/components/follow_requests_nav_link.js | 46 ------------------- .../components/notifications_counter_icon.js | 9 ---- .../ui/components/reports_counter_icon.js | 9 ---- .../features/ui/components/tabs_bar.js | 29 ++++++------ app/soapbox/features/ui/index.js | 6 ++- app/styles/components/tabs-bar.scss | 6 +++ app/styles/ui.scss | 4 -- 9 files changed, 26 insertions(+), 113 deletions(-) delete mode 100644 app/soapbox/components/icon_with_badge.js delete mode 100644 app/soapbox/features/ui/components/chats_counter_icon.js delete mode 100644 app/soapbox/features/ui/components/follow_requests_nav_link.js delete mode 100644 app/soapbox/features/ui/components/notifications_counter_icon.js delete mode 100644 app/soapbox/features/ui/components/reports_counter_icon.js diff --git a/app/soapbox/components/icon_with_badge.js b/app/soapbox/components/icon_with_badge.js deleted file mode 100644 index 741401982..000000000 --- a/app/soapbox/components/icon_with_badge.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { shortNumberFormat } from 'soapbox/utils/numbers'; - -const IconWithBadge = ({ id, count, className }) => { - if (count < 1) return null; - - return ( - - {count > 0 && {shortNumberFormat(count)}} - - ); -}; - -IconWithBadge.propTypes = { - id: PropTypes.string.isRequired, - count: PropTypes.number.isRequired, - className: PropTypes.string, -}; - -export default IconWithBadge; diff --git a/app/soapbox/features/ui/components/chats_counter_icon.js b/app/soapbox/features/ui/components/chats_counter_icon.js deleted file mode 100644 index bb6d32907..000000000 --- a/app/soapbox/features/ui/components/chats_counter_icon.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import IconWithBadge from 'soapbox/components/icon_with_badge'; - -const mapStateToProps = state => ({ - count: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0), - id: 'comment', -}); - -export default connect(mapStateToProps)(IconWithBadge); diff --git a/app/soapbox/features/ui/components/follow_requests_nav_link.js b/app/soapbox/features/ui/components/follow_requests_nav_link.js deleted file mode 100644 index 132637fa0..000000000 --- a/app/soapbox/features/ui/components/follow_requests_nav_link.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { fetchFollowRequests } from 'soapbox/actions/accounts'; -import { connect } from 'react-redux'; -import { NavLink, withRouter } from 'react-router-dom'; -import IconWithBadge from 'soapbox/components/icon_with_badge'; -import { List as ImmutableList } from 'immutable'; -import { FormattedMessage } from 'react-intl'; - -const mapStateToProps = state => { - const me = state.get('me'); - return { - locked: state.getIn(['accounts', me, 'locked']), - count: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size, - }; -}; - -export default @withRouter -@connect(mapStateToProps) -class FollowRequestsNavLink extends React.Component { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - locked: PropTypes.bool, - count: PropTypes.number.isRequired, - }; - - componentDidMount() { - const { dispatch, locked } = this.props; - - if (locked) { - dispatch(fetchFollowRequests()); - } - } - - render() { - const { locked, count } = this.props; - - if (!locked || count === 0) { - return null; - } - - return ; - } - -} diff --git a/app/soapbox/features/ui/components/notifications_counter_icon.js b/app/soapbox/features/ui/components/notifications_counter_icon.js deleted file mode 100644 index a2c3039cb..000000000 --- a/app/soapbox/features/ui/components/notifications_counter_icon.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import IconWithBadge from 'soapbox/components/icon_with_badge'; - -const mapStateToProps = state => ({ - count: state.getIn(['notifications', 'unread']), - id: 'bell', -}); - -export default connect(mapStateToProps)(IconWithBadge); diff --git a/app/soapbox/features/ui/components/reports_counter_icon.js b/app/soapbox/features/ui/components/reports_counter_icon.js deleted file mode 100644 index 14a121887..000000000 --- a/app/soapbox/features/ui/components/reports_counter_icon.js +++ /dev/null @@ -1,9 +0,0 @@ -import { connect } from 'react-redux'; -import IconWithBadge from 'soapbox/components/icon_with_badge'; - -const mapStateToProps = state => ({ - count: state.getIn(['admin', 'open_report_count']), - id: 'gavel', -}); - -export default connect(mapStateToProps)(IconWithBadge); diff --git a/app/soapbox/features/ui/components/tabs_bar.js b/app/soapbox/features/ui/components/tabs_bar.js index a87bf7429..013eef313 100644 --- a/app/soapbox/features/ui/components/tabs_bar.js +++ b/app/soapbox/features/ui/components/tabs_bar.js @@ -5,9 +5,7 @@ import { Link, NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; import classNames from 'classnames'; -import NotificationsCounterIcon from './notifications_counter_icon'; -import ReportsCounterIcon from './reports_counter_icon'; -import ChatsCounterIcon from './chats_counter_icon'; +import IconWithCounter from 'soapbox/components/icon_with_counter'; import SearchContainer from 'soapbox/features/compose/containers/search_container'; import Avatar from '../../../components/avatar'; import ActionBar from 'soapbox/features/compose/components/action_bar'; @@ -32,6 +30,9 @@ class TabsBar extends React.PureComponent { onOpenSidebar: PropTypes.func.isRequired, logo: PropTypes.string, account: ImmutablePropTypes.map, + dashboardCount: PropTypes.number, + notificationCount: PropTypes.number, + chatsCount: PropTypes.number, } state = { @@ -52,7 +53,7 @@ class TabsBar extends React.PureComponent { } getNavLinks() { - const { intl: { formatMessage }, logo, account } = this.props; + const { intl: { formatMessage }, logo, account, dashboardCount, notificationCount, chatsCount } = this.props; let links = []; if (logo) { links.push( @@ -69,26 +70,23 @@ class TabsBar extends React.PureComponent { if (account) { links.push( - - + ); } if (account) { links.push( - - + ); } if (account && isStaff(account)) { links.push( - - - - - ); + + + + ); } links.push( @@ -156,9 +154,14 @@ class TabsBar extends React.PureComponent { const mapStateToProps = state => { const me = state.get('me'); + const reportsCount = state.getIn(['admin', 'open_report_count']); + const approvalCount = state.getIn(['admin', 'awaitingApproval']).count(); return { account: state.getIn(['accounts', me]), logo: getSoapboxConfig(state).get('logo'), + notificationCount: state.getIn(['notifications', 'unread']), + chatsCount: state.get('chats').reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0), + dashboardCount: reportsCount + approvalCount, }; }; diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 969aadc79..313f60607 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -16,7 +16,7 @@ import { debounce } from 'lodash'; import { uploadCompose, resetCompose } from '../../actions/compose'; import { expandHomeTimeline } from '../../actions/timelines'; import { expandNotifications } from '../../actions/notifications'; -import { fetchReports } from '../../actions/admin'; +import { fetchReports, fetchUsers } from '../../actions/admin'; import { fetchFilters } from '../../actions/filters'; import { fetchChats } from 'soapbox/actions/chats'; import { clearHeight } from '../../actions/height_cache'; @@ -462,8 +462,10 @@ class UI extends React.PureComponent { this.props.dispatch(expandNotifications()); this.props.dispatch(fetchChats()); // this.props.dispatch(fetchGroups('member')); - if (isStaff(account)) + if (isStaff(account)) { this.props.dispatch(fetchReports({ state: 'open' })); + this.props.dispatch(fetchUsers({ page: 1, filters: 'local,need_approval' })); + } setTimeout(() => this.props.dispatch(fetchFilters()), 500); } diff --git a/app/styles/components/tabs-bar.scss b/app/styles/components/tabs-bar.scss index 8553b3df6..326f688dd 100644 --- a/app/styles/components/tabs-bar.scss +++ b/app/styles/components/tabs-bar.scss @@ -213,6 +213,12 @@ } } + .icon-with-counter__counter { + @media screen and (min-width: 895px) { + left: 5px; + } + } + &.optional { display: none; @media screen and (max-width: $nav-breakpoint-2) { diff --git a/app/styles/ui.scss b/app/styles/ui.scss index 0b4fa30d3..3f6637b49 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -719,9 +719,5 @@ text-align: center; color: #fff; background: var(--accent-color); - - @media screen and (max-width: 895px) { - top: 0; - } } } From 21bddfd37b003c47070c27d0b9b5d49855553fec Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 29 Dec 2020 20:34:28 -0600 Subject: [PATCH 13/13] Admin: disable non-working navlinks for now --- app/soapbox/features/admin/components/admin_nav.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/admin/components/admin_nav.js b/app/soapbox/features/admin/components/admin_nav.js index 47076abf8..06aa236f5 100644 --- a/app/soapbox/features/admin/components/admin_nav.js +++ b/app/soapbox/features/admin/components/admin_nav.js @@ -44,18 +44,18 @@ class AdminNav extends React.PureComponent { )} {!instance.get('registrations') && ( - + {/* - + */} )} - + {/* - + */}
-
+ {/*
@@ -90,7 +90,7 @@ class AdminNav extends React.PureComponent {
-
+
*/} ); }