From b2ebdbcf80679d61f4849ca85da22377ef7c9ad9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 14 Jul 2021 12:27:51 -0500 Subject: [PATCH 1/5] Admin: display latest accounts in dashboard --- app/soapbox/actions/admin.js | 1 + .../admin/components/latest_accounts_panel.js | 54 +++++++++++++++++ .../ui/components/account_list_panel.js | 47 +++++++++++++++ app/soapbox/pages/admin_page.js | 2 + app/soapbox/reducers/__tests__/admin-test.js | 1 + app/soapbox/reducers/admin.js | 59 +++++++++++++++---- 6 files changed, 153 insertions(+), 11 deletions(-) create mode 100644 app/soapbox/features/admin/components/latest_accounts_panel.js create mode 100644 app/soapbox/features/ui/components/account_list_panel.js diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index ee957948d..0ad5932c6 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,6 +1,7 @@ import api from '../api'; import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer'; import { fetchRelationships } from 'soapbox/actions/accounts'; +import { Set as ImmutableSet } from 'immutable'; export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS'; diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.js b/app/soapbox/features/admin/components/latest_accounts_panel.js new file mode 100644 index 000000000..37b82b6e7 --- /dev/null +++ b/app/soapbox/features/admin/components/latest_accounts_panel.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { injectIntl, defineMessages } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import AccountListPanel from 'soapbox/features/ui/components/account_list_panel'; +import { fetchUsers } from 'soapbox/actions/admin'; + +const messages = defineMessages({ + title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['admin', 'latestUsers']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class LatestAccountsPanel extends ImmutablePureComponent { + + static propTypes = { + accountIds: ImmutablePropTypes.orderedSet.isRequired, + limit: PropTypes.number, + }; + + static defaultProps = { + limit: 5, + } + + componentDidMount() { + const { dispatch, limit } = this.props; + dispatch(fetchUsers(['local', 'active'], 1, null, limit)); + } + + render() { + const { intl, accountIds, limit, ...props } = this.props; + + if (!accountIds || accountIds.isEmpty()) { + return null; + } + + return ( + + ); + }; + +}; diff --git a/app/soapbox/features/ui/components/account_list_panel.js b/app/soapbox/features/ui/components/account_list_panel.js new file mode 100644 index 000000000..8563a2231 --- /dev/null +++ b/app/soapbox/features/ui/components/account_list_panel.js @@ -0,0 +1,47 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Icon from 'soapbox/components/icon'; +import AccountContainer from '../../../containers/account_container'; + +export default class AccountListPanel extends ImmutablePureComponent { + + static propTypes = { + title: PropTypes.node.isRequired, + accountIds: ImmutablePropTypes.orderedSet.isRequired, + icon: PropTypes.string.isRequired, + limit: PropTypes.number, + }; + + static defaultProps = { + limit: Infinity, + } + + render() { + const { title, icon, accountIds, limit, ...props } = this.props; + + if (!accountIds || accountIds.isEmpty()) { + return null; + } + + return ( +
+
+ + + {title} + +
+
+
+ {accountIds.take(limit).map(accountId => ( + + ))} +
+
+
+ ); + }; + +}; diff --git a/app/soapbox/pages/admin_page.js b/app/soapbox/pages/admin_page.js index 28dd2d25f..009b599b2 100644 --- a/app/soapbox/pages/admin_page.js +++ b/app/soapbox/pages/admin_page.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; import LinkFooter from '../features/ui/components/link_footer'; import AdminNav from 'soapbox/features/admin/components/admin_nav'; +import LatestAccountsPanel from 'soapbox/features/admin/components/latest_accounts_panel'; export default class AdminPage extends ImmutablePureComponent { @@ -28,6 +29,7 @@ class AdminPage extends ImmutablePureComponent {
+
diff --git a/app/soapbox/reducers/__tests__/admin-test.js b/app/soapbox/reducers/__tests__/admin-test.js index 588abe7aa..e56eff6e9 100644 --- a/app/soapbox/reducers/__tests__/admin-test.js +++ b/app/soapbox/reducers/__tests__/admin-test.js @@ -11,6 +11,7 @@ describe('admin reducer', () => { reports: ImmutableMap(), openReports: ImmutableOrderedSet(), users: ImmutableMap(), + latestUsers: ImmutableOrderedSet(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js index f7cb33fb6..b4978c218 100644 --- a/app/soapbox/reducers/admin.js +++ b/app/soapbox/reducers/admin.js @@ -12,31 +12,68 @@ import { import { Map as ImmutableMap, List as ImmutableList, + Set as ImmutableSet, OrderedSet as ImmutableOrderedSet, fromJS, + is, } from 'immutable'; -import { normalizePleromaUserFields } from 'soapbox/utils/pleroma'; const initialState = ImmutableMap({ reports: ImmutableMap(), openReports: ImmutableOrderedSet(), users: ImmutableMap(), + latestUsers: ImmutableOrderedSet(), awaitingApproval: ImmutableOrderedSet(), configs: ImmutableList(), needsReboot: false, }); -function importUsers(state, users) { +const FILTER_UNAPPROVED = ['local', 'need_approval']; +const FILTER_LATEST = ['local', 'active']; + +const filtersMatch = (f1, f2) => is(ImmutableSet(f1), ImmutableSet(f2)); +const toIds = items => items.map(item => item.id); + +const mergeSet = (state, key, users) => { + const newIds = toIds(users); + return state.update(key, ImmutableOrderedSet(), ids => ids.union(newIds)); +}; + +const replaceSet = (state, key, users) => { + const newIds = toIds(users); + return state.set(key, ImmutableOrderedSet(newIds)); +}; + +const maybeImportUnapproved = (state, users, filters) => { + if (filtersMatch(FILTER_UNAPPROVED, filters)) { + return mergeSet(state, 'awaitingApproval', users); + } else { + return state; + } +}; + +const maybeImportLatest = (state, users, filters, page) => { + if (page === 1 && filtersMatch(FILTER_LATEST, filters)) { + return replaceSet(state, 'latestUsers', users); + } else { + return state; + } +}; + +const importUser = (state, user) => ( + state.setIn(['users', user.id], ImmutableMap({ + email: user.email, + registration_reason: user.registration_reason, + })) +); + +function importUsers(state, users, filters, page) { return state.withMutations(state => { + maybeImportUnapproved(state, users, filters); + maybeImportLatest(state, users, filters, page); + users.forEach(user => { - user = normalizePleromaUserFields(user); - if (!user.is_approved) { - state.update('awaitingApproval', orderedSet => orderedSet.add(user.id)); - } - state.setIn(['users', user.id], ImmutableMap({ - email: user.email, - registration_reason: user.registration_reason, - })); + importUser(state, user); }); }); } @@ -97,7 +134,7 @@ export default function admin(state = initialState, action) { case ADMIN_REPORTS_PATCH_SUCCESS: return handleReportDiffs(state, action.reports); case ADMIN_USERS_FETCH_SUCCESS: - return importUsers(state, action.users); + return importUsers(state, action.users, action.filters, action.page); case ADMIN_USERS_DELETE_REQUEST: case ADMIN_USERS_DELETE_SUCCESS: return deleteUsers(state, action.accountIds); From 12939e3354eed5b0937956a568e73a2e330620ba Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 14 Jul 2021 13:17:21 -0500 Subject: [PATCH 2/5] LatestAccountsPanel: add expand button, use compact display of accounts --- app/soapbox/components/account.js | 46 ++++++++++++------- .../admin/components/latest_accounts_panel.js | 18 +++++++- .../ui/components/account_list_panel.js | 11 ++++- app/soapbox/pages/admin_page.js | 2 +- app/styles/accounts.scss | 10 +++- 5 files changed, 67 insertions(+), 20 deletions(-) diff --git a/app/soapbox/components/account.js b/app/soapbox/components/account.js index 189e90686..e1f057b59 100644 --- a/app/soapbox/components/account.js +++ b/app/soapbox/components/account.js @@ -11,6 +11,7 @@ import IconButton from './icon_button'; import RelativeTimestamp from './relative_timestamp'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -44,8 +45,14 @@ class Account extends ImmutablePureComponent { actionTitle: PropTypes.string, onActionClick: PropTypes.func, withDate: PropTypes.bool, + withRelationship: PropTypes.bool, }; + static defaultProps = { + withDate: false, + withRelationship: true, + } + handleFollow = () => { this.props.onFollow(this.props.account); } @@ -71,7 +78,7 @@ class Account extends ImmutablePureComponent { } render() { - const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate } = this.props; + const { account, intl, hidden, onActionClick, actionIcon, actionTitle, me, withDate, withRelationship } = this.props; if (!account) { return
; @@ -87,7 +94,7 @@ class Account extends ImmutablePureComponent { } let buttons; - let followed_by; + let followedBy; if (onActionClick && actionIcon) { buttons = ; @@ -97,7 +104,7 @@ class Account extends ImmutablePureComponent { const blocking = account.getIn(['relationship', 'blocking']); const muting = account.getIn(['relationship', 'muting']); - followed_by = account.getIn(['relationship', 'followed_by']); + followedBy = account.getIn(['relationship', 'followed_by']); if (requested) { buttons = ; @@ -121,29 +128,36 @@ class Account extends ImmutablePureComponent { } } + const createdAt = account.get('created_at'); + + const joinedAt = createdAt ? ( +
+ + +
+ ) : null; + return ( -
+
- { followed_by ? - - - - : '' } + {withRelationship ? (<> + {followedBy && + + + } -
- {buttons} -
+
+ {buttons} +
+ ) : withDate && joinedAt}
- {withDate && (
- - -
)} + {(withDate && withRelationship) && joinedAt}
); } diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.js b/app/soapbox/features/admin/components/latest_accounts_panel.js index 37b82b6e7..5d4f9b662 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.js +++ b/app/soapbox/features/admin/components/latest_accounts_panel.js @@ -9,6 +9,7 @@ import { fetchUsers } from 'soapbox/actions/admin'; const messages = defineMessages({ title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, + expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' }, }); const mapStateToProps = state => ({ @@ -28,13 +29,23 @@ class LatestAccountsPanel extends ImmutablePureComponent { limit: 5, } + state = { + total: 0, + } + componentDidMount() { const { dispatch, limit } = this.props; - dispatch(fetchUsers(['local', 'active'], 1, null, limit)); + + dispatch(fetchUsers(['local', 'active'], 1, null, limit)) + .then(({ count }) => { + this.setState({ total: count }); + }) + .catch(() => {}); } render() { const { intl, accountIds, limit, ...props } = this.props; + const { total } = this.state; if (!accountIds || accountIds.isEmpty()) { return null; @@ -46,6 +57,11 @@ class LatestAccountsPanel extends ImmutablePureComponent { title={intl.formatMessage(messages.title)} accountIds={accountIds} limit={limit} + total={total} + expandMessage={intl.formatMessage(messages.expand, { count: total })} + expandRoute='/admin/users' + withDate + withRelationship={false} {...props} /> ); diff --git a/app/soapbox/features/ui/components/account_list_panel.js b/app/soapbox/features/ui/components/account_list_panel.js index 8563a2231..2159b657b 100644 --- a/app/soapbox/features/ui/components/account_list_panel.js +++ b/app/soapbox/features/ui/components/account_list_panel.js @@ -4,6 +4,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Icon from 'soapbox/components/icon'; import AccountContainer from '../../../containers/account_container'; +import { Link } from 'react-router-dom'; export default class AccountListPanel extends ImmutablePureComponent { @@ -12,6 +13,9 @@ export default class AccountListPanel extends ImmutablePureComponent { accountIds: ImmutablePropTypes.orderedSet.isRequired, icon: PropTypes.string.isRequired, limit: PropTypes.number, + total: PropTypes.number, + expandMessage: PropTypes.string, + expandRoute: PropTypes.string, }; static defaultProps = { @@ -19,12 +23,14 @@ export default class AccountListPanel extends ImmutablePureComponent { } render() { - const { title, icon, accountIds, limit, ...props } = this.props; + const { title, icon, accountIds, limit, total, expandMessage, expandRoute, ...props } = this.props; if (!accountIds || accountIds.isEmpty()) { return null; } + const canExpand = expandMessage && expandRoute && (accountIds.size < total); + return (
@@ -40,6 +46,9 @@ export default class AccountListPanel extends ImmutablePureComponent { ))}
+ {canExpand && + {expandMessage} + }
); }; diff --git a/app/soapbox/pages/admin_page.js b/app/soapbox/pages/admin_page.js index 009b599b2..a010581cc 100644 --- a/app/soapbox/pages/admin_page.js +++ b/app/soapbox/pages/admin_page.js @@ -29,7 +29,7 @@ class AdminPage extends ImmutablePureComponent {
- +
diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss index 35bc1aa3d..7e7e2680d 100644 --- a/app/styles/accounts.scss +++ b/app/styles/accounts.scss @@ -518,10 +518,18 @@ a .account__avatar { } .account__joined-at { - padding: 3px 2px 0 48px; + padding: 3px 2px 0 5px; font-size: 14px; + display: flex; + white-space: nowrap; i.fa-calendar { padding-right: 5px; } } + +.account--with-date.account--with-relationship { + .account__joined-at { + padding-left: 48px; + } +} From 4ca3d68bf6847fc4e322d7c6289a0c3a543d90d3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 14 Jul 2021 13:21:23 -0500 Subject: [PATCH 3/5] LatestAccountsPanel: fix expand count --- .../features/admin/components/latest_accounts_panel.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.js b/app/soapbox/features/admin/components/latest_accounts_panel.js index 5d4f9b662..7c46b112b 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.js +++ b/app/soapbox/features/admin/components/latest_accounts_panel.js @@ -51,6 +51,8 @@ class LatestAccountsPanel extends ImmutablePureComponent { return null; } + const expandCount = total - accountIds.size; + return ( Date: Wed, 14 Jul 2021 13:45:36 -0500 Subject: [PATCH 4/5] LatestAccountsPanel: add backwards compatibility with unsorted AdminAPI endpoint --- .../admin/components/latest_accounts_panel.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/admin/components/latest_accounts_panel.js b/app/soapbox/features/admin/components/latest_accounts_panel.js index 7c46b112b..b2b8614f5 100644 --- a/app/soapbox/features/admin/components/latest_accounts_panel.js +++ b/app/soapbox/features/admin/components/latest_accounts_panel.js @@ -6,15 +6,29 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import AccountListPanel from 'soapbox/features/ui/components/account_list_panel'; import { fetchUsers } from 'soapbox/actions/admin'; +import { is } from 'immutable'; +import compareId from 'soapbox/compare_id'; const messages = defineMessages({ title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' }, expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' }, }); -const mapStateToProps = state => ({ - accountIds: state.getIn(['admin', 'latestUsers']), -}); +const mapStateToProps = state => { + const accountIds = state.getIn(['admin', 'latestUsers']); + + // HACK: AdminAPI only recently started sorting new users at the top. + // Try a dirty check to see if the users are sorted properly, or don't show the panel. + // Probably works most of the time. + const sortedIds = accountIds.sort(compareId).reverse(); + const hasDates = accountIds.every(id => state.getIn(['accounts', id, 'created_at'])); + const isSorted = hasDates && is(accountIds, sortedIds); + + return { + isSorted, + accountIds, + }; +}; export default @connect(mapStateToProps) @injectIntl @@ -44,10 +58,10 @@ class LatestAccountsPanel extends ImmutablePureComponent { } render() { - const { intl, accountIds, limit, ...props } = this.props; + const { intl, accountIds, limit, isSorted, ...props } = this.props; const { total } = this.state; - if (!accountIds || accountIds.isEmpty()) { + if (!isSorted || !accountIds || accountIds.isEmpty()) { return null; } From e2bc0b55a434f0ed507e0f22d7e12fb74a745db1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 14 Jul 2021 13:56:44 -0500 Subject: [PATCH 5/5] Fix lint --- app/soapbox/actions/admin.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js index 0ad5932c6..ee957948d 100644 --- a/app/soapbox/actions/admin.js +++ b/app/soapbox/actions/admin.js @@ -1,7 +1,6 @@ import api from '../api'; import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer'; import { fetchRelationships } from 'soapbox/actions/accounts'; -import { Set as ImmutableSet } from 'immutable'; export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST'; export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';