Merge branch 'admin-recent-users' into 'develop'

Admin: display latest accounts in dashboard

See merge request soapbox-pub/soapbox-fe!618
merge-requests/619/head
Alex Gleason 3 years ago
commit 76b05f738a

@ -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 <div />;
@ -87,7 +94,7 @@ class Account extends ImmutablePureComponent {
}
let buttons;
let followed_by;
let followedBy;
if (onActionClick && actionIcon) {
buttons = <IconButton icon={actionIcon} title={actionTitle} onClick={this.handleAction} />;
@ -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 = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
@ -121,29 +128,36 @@ class Account extends ImmutablePureComponent {
}
}
const createdAt = account.get('created_at');
const joinedAt = createdAt ? (
<div className='account__joined-at'>
<Icon id='calendar' />
<RelativeTimestamp timestamp={createdAt} />
</div>
) : null;
return (
<div className='account'>
<div className={classNames('account', { 'account--with-relationship': withRelationship, 'account--with-date': withDate })}>
<div className='account__wrapper'>
<Permalink key={account.get('id')} className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
<DisplayName account={account} />
</Permalink>
{ followed_by ?
<span className='relationship-tag'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>
: '' }
{withRelationship ? (<>
{followedBy &&
<span className='relationship-tag'>
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
</span>}
<div className='account__relationship'>
{buttons}
</div>
<div className='account__relationship'>
{buttons}
</div>
</>) : withDate && joinedAt}
</div>
{withDate && (<div className='account__joined-at'>
<Icon id='calendar' />
<RelativeTimestamp timestamp={account.get('created_at')} />
</div>)}
{(withDate && withRelationship) && joinedAt}
</div>
);
}

@ -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 (
<AccountListPanel
icon='users'
title={intl.formatMessage(messages.title)}
accountIds={accountIds}
limit={limit}
total={total}
expandMessage={intl.formatMessage(messages.expand, { count: expandCount })}
expandRoute='/admin/users'
withDate
withRelationship={false}
{...props}
/>
);
};
};

@ -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 (
<div className='wtf-panel'>
<div className='wtf-panel-header'>
<Icon id={icon} className='wtf-panel-header__icon' />
<span className='wtf-panel-header__label'>
{title}
</span>
</div>
<div className='wtf-panel__content'>
<div className='wtf-panel__list'>
{accountIds.take(limit).map(accountId => (
<AccountContainer key={accountId} id={accountId} {...props} />
))}
</div>
</div>
{canExpand && <Link className='wtf-panel__expand-btn' to={expandRoute}>
{expandMessage}
</Link>}
</div>
);
};
};

@ -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 {
<div className='columns-area__panels__pane columns-area__panels__pane--right'>
<div className='columns-area__panels__pane__inner'>
<LatestAccountsPanel limit={5} />
<LinkFooter />
</div>
</div>

@ -11,6 +11,7 @@ describe('admin reducer', () => {
reports: ImmutableMap(),
openReports: ImmutableOrderedSet(),
users: ImmutableMap(),
latestUsers: ImmutableOrderedSet(),
awaitingApproval: ImmutableOrderedSet(),
configs: ImmutableList(),
needsReboot: false,

@ -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);

@ -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;
}
}

Loading…
Cancel
Save