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
+
- { 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
new file mode 100644
index 000000000..b2b8614f5
--- /dev/null
+++ b/app/soapbox/features/admin/components/latest_accounts_panel.js
@@ -0,0 +1,86 @@
+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';
+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 => {
+ 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
+class LatestAccountsPanel extends ImmutablePureComponent {
+
+ static propTypes = {
+ accountIds: ImmutablePropTypes.orderedSet.isRequired,
+ limit: PropTypes.number,
+ };
+
+ static defaultProps = {
+ limit: 5,
+ }
+
+ state = {
+ total: 0,
+ }
+
+ componentDidMount() {
+ const { dispatch, limit } = this.props;
+
+ dispatch(fetchUsers(['local', 'active'], 1, null, limit))
+ .then(({ count }) => {
+ this.setState({ total: count });
+ })
+ .catch(() => {});
+ }
+
+ render() {
+ const { intl, accountIds, limit, isSorted, ...props } = this.props;
+ const { total } = this.state;
+
+ if (!isSorted || !accountIds || accountIds.isEmpty()) {
+ return null;
+ }
+
+ const expandCount = total - accountIds.size;
+
+ 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..2159b657b
--- /dev/null
+++ b/app/soapbox/features/ui/components/account_list_panel.js
@@ -0,0 +1,56 @@
+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';
+import { Link } from 'react-router-dom';
+
+export default class AccountListPanel extends ImmutablePureComponent {
+
+ static propTypes = {
+ title: PropTypes.node.isRequired,
+ accountIds: ImmutablePropTypes.orderedSet.isRequired,
+ icon: PropTypes.string.isRequired,
+ limit: PropTypes.number,
+ total: PropTypes.number,
+ expandMessage: PropTypes.string,
+ expandRoute: PropTypes.string,
+ };
+
+ static defaultProps = {
+ limit: Infinity,
+ }
+
+ render() {
+ 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 (
+
+
+
+
+ {title}
+
+
+
+
+ {accountIds.take(limit).map(accountId => (
+
+ ))}
+
+
+ {canExpand &&
+ {expandMessage}
+ }
+
+ );
+ };
+
+};
diff --git a/app/soapbox/pages/admin_page.js b/app/soapbox/pages/admin_page.js
index 28dd2d25f..a010581cc 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);
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;
+ }
+}