diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js
index bd2b199ba..39a7a2ceb 100644
--- a/app/soapbox/actions/accounts.js
+++ b/app/soapbox/actions/accounts.js
@@ -130,7 +130,9 @@ export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
- if (getState().getIn(['accounts', id], null) !== null) {
+ const account = getState().getIn(['accounts', id]);
+
+ if (account && !account.get('dirty')) {
return;
}
@@ -156,7 +158,15 @@ export function fetchAccount(id) {
export function fetchAccountByUsername(username) {
return (dispatch, getState) => {
+ const account = getState().get('accounts').find(account => account.get('acct') === username);
+
+ if (account) {
+ dispatch(fetchAccount(account.get('id')));
+ return;
+ }
+
api(getState).get(`/api/v1/accounts/${username}`).then(response => {
+ dispatch(fetchRelationships([response.data.id]));
dispatch(importFetchedAccount(response.data));
}).then(() => {
dispatch(fetchAccountSuccess());
diff --git a/app/soapbox/actions/admin.js b/app/soapbox/actions/admin.js
index 6a93b1682..96f7e5e12 100644
--- a/app/soapbox/actions/admin.js
+++ b/app/soapbox/actions/admin.js
@@ -1,5 +1,6 @@
import api from '../api';
import { importFetchedAccount, importFetchedStatuses } from 'soapbox/actions/importer';
+import { fetchRelationships } from 'soapbox/actions/accounts';
export const ADMIN_CONFIG_FETCH_REQUEST = 'ADMIN_CONFIG_FETCH_REQUEST';
export const ADMIN_CONFIG_FETCH_SUCCESS = 'ADMIN_CONFIG_FETCH_SUCCESS';
@@ -129,8 +130,9 @@ export function fetchUsers(params) {
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 });
+ .then(({ data: { users, count, page_size: pageSize } }) => {
+ dispatch(fetchRelationships(users.map(user => user.id)));
+ dispatch({ type: ADMIN_USERS_FETCH_SUCCESS, users, count, pageSize, params });
}).catch(error => {
dispatch({ type: ADMIN_USERS_FETCH_FAIL, error, params });
});
diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js
index 6aebd5e28..537990134 100644
--- a/app/soapbox/features/account/components/header.js
+++ b/app/soapbox/features/account/components/header.js
@@ -291,7 +291,8 @@ class Header extends ImmutablePureComponent {
const info = this.makeInfo();
const menu = this.makeMenu();
- const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1);
+ const header = account.get('header', '');
+ const headerMissing = !header || ['/images/banner.png', '/headers/original/missing.png'].some(path => header.endsWith(path));
const avatarSize = isSmallScreen ? 90 : 200;
const deactivated = !account.getIn(['pleroma', 'is_active'], true);
@@ -306,7 +307,7 @@ class Header extends ImmutablePureComponent {
-
+ {header && }
diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js
new file mode 100644
index 000000000..9501c3ceb
--- /dev/null
+++ b/app/soapbox/features/admin/user_index.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { debounce } from 'lodash';
+import LoadingIndicator from 'soapbox/components/loading_indicator';
+import { fetchUsers } from 'soapbox/actions/admin';
+import { FormattedMessage } from 'react-intl';
+import AccountContainer from 'soapbox/containers/account_container';
+import Column from 'soapbox/features/ui/components/column';
+import ScrollableList from 'soapbox/components/scrollable_list';
+
+const mapStateToProps = state => {
+ return {
+ accountIds: state.getIn(['admin', 'usersList']),
+ hasMore: false,
+ };
+};
+
+export default @connect(mapStateToProps)
+class UserIndex extends ImmutablePureComponent {
+
+ static propTypes = {
+ params: PropTypes.object.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ accountIds: ImmutablePropTypes.orderedSet,
+ hasMore: PropTypes.bool,
+ diffCount: PropTypes.number,
+ isAccount: PropTypes.bool,
+ unavailable: PropTypes.bool,
+ };
+
+ componentDidMount() {
+ this.props.dispatch(fetchUsers({ filters: 'local,active' }));
+ }
+
+ handleLoadMore = debounce(() => {
+ // if (this.props.accountId && this.props.accountId !== -1) {
+ // this.props.dispatch(expandFollowers(this.props.accountId));
+ // }
+ }, 300, { leading: true });
+
+ render() {
+ const { accountIds, hasMore } = this.props;
+
+ if (!accountIds) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ }
+ >
+ {accountIds.map(id =>
+ ,
+ )}
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index a140cd0a7..8b1204ed7 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -95,6 +95,7 @@ import {
ModerationLog,
CryptoDonate,
ScheduledStatuses,
+ UserIndex,
} from './util/async-components';
// Dummy import, to make sure that
ends up in the application bundle.
@@ -265,6 +266,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 9d1fcae1d..c1bb39301 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -241,3 +241,7 @@ export function CryptoDonate() {
export function ScheduledStatuses() {
return import(/* webpackChunkName: "features/scheduled_statuses" */'../../scheduled_statuses');
}
+
+export function UserIndex() {
+ return import(/* webpackChunkName: "features/admin/user_index" */'../../admin/user_index');
+}
diff --git a/app/soapbox/reducers/accounts.js b/app/soapbox/reducers/accounts.js
index 7bab5aad4..0ea392263 100644
--- a/app/soapbox/reducers/accounts.js
+++ b/app/soapbox/reducers/accounts.js
@@ -13,6 +13,7 @@ import {
} from 'immutable';
import { normalizePleromaUserFields } from 'soapbox/utils/pleroma';
import {
+ ADMIN_USERS_FETCH_SUCCESS,
ADMIN_USERS_TAG_REQUEST,
ADMIN_USERS_TAG_FAIL,
ADMIN_USERS_UNTAG_REQUEST,
@@ -121,6 +122,66 @@ const removePermission = (state, accountIds, permissionGroup) => {
});
};
+const buildAccount = adminUser => fromJS({
+ id: adminUser.get('id'),
+ username: adminUser.get('nickname').split('@')[0],
+ acct: adminUser.get('nickname'),
+ display_name: adminUser.get('display_name'),
+ display_name_html: adminUser.get('display_name'),
+ note: '',
+ url: adminUser.get('url'),
+ avatar: adminUser.get('avatar'),
+ avatar_static: adminUser.get('avatar'),
+ header: '',
+ header_static: '',
+ emojis: [],
+ fields: [],
+ pleroma: {
+ is_active: adminUser.get('is_active'),
+ is_confirmed: adminUser.get('is_confirmed'),
+ is_admin: adminUser.getIn(['roles', 'admin']),
+ is_moderator: adminUser.getIn(['roles', 'moderator']),
+ },
+ source: {
+ pleroma: {
+ actor_type: adminUser.get('actor_type'),
+ },
+ },
+ dirty: true,
+});
+
+const mergeAdminUser = (account, adminUser) => {
+ return account.withMutations(account => {
+ account.set('display_name', adminUser.get('display_name'));
+ account.set('avatar', adminUser.get('avatar'));
+ account.set('avatar_static', adminUser.get('avatar'));
+ account.setIn(['pleroma', 'is_active'], adminUser.get('is_active'));
+ account.setIn(['pleroma', 'is_admin'], adminUser.getIn(['roles', 'admin']));
+ account.setIn(['pleroma', 'is_moderator'], adminUser.getIn(['roles', 'moderator']));
+ account.setIn(['pleroma', 'is_confirmed'], adminUser.get('is_confirmed'));
+ account.set('dirty', true);
+ });
+};
+
+const importAdminUser = (state, adminUser) => {
+ const id = adminUser.get('id');
+ const account = state.get(id);
+
+ if (!account) {
+ return state.set(id, buildAccount(adminUser));
+ } else {
+ return state.set(id, mergeAdminUser(account, adminUser));
+ }
+};
+
+const importAdminUsers = (state, adminUsers) => {
+ return state.withMutations(state => {
+ fromJS(adminUsers).forEach(adminUser => {
+ importAdminUser(state, adminUser);
+ });
+ });
+};
+
export default function accounts(state = initialState, action) {
switch(action.type) {
case ACCOUNT_IMPORT:
@@ -150,6 +211,8 @@ export default function accounts(state = initialState, action) {
return removePermission(state, action.accountIds, action.permissionGroup);
case ADMIN_USERS_DELETE_REQUEST:
return setDeactivated(state, action.nicknames);
+ case ADMIN_USERS_FETCH_SUCCESS:
+ return importAdminUsers(state, action.users);
default:
return state;
}
diff --git a/app/soapbox/reducers/admin.js b/app/soapbox/reducers/admin.js
index 960df70f6..f8a654c74 100644
--- a/app/soapbox/reducers/admin.js
+++ b/app/soapbox/reducers/admin.js
@@ -21,6 +21,7 @@ const initialState = ImmutableMap({
reports: ImmutableMap(),
openReports: ImmutableOrderedSet(),
users: ImmutableMap(),
+ usersList: ImmutableOrderedSet(),
awaitingApproval: ImmutableOrderedSet(),
configs: ImmutableList(),
needsReboot: false,
@@ -28,6 +29,8 @@ const initialState = ImmutableMap({
function importUsers(state, users) {
return state.withMutations(state => {
+ const ids = users.map(user => user.id);
+ state.update('usersList', ImmutableOrderedSet(), items => items.union(ids));
users.forEach(user => {
user = normalizePleromaUserFields(user);
if (!user.is_approved) {
@@ -94,7 +97,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.data.users);
+ return importUsers(state, action.users);
case ADMIN_USERS_DELETE_REQUEST:
case ADMIN_USERS_DELETE_SUCCESS:
return deleteUsers(state, action.nicknames);