From c6b7a7ca8a9a4e2f450558b9cf8dcdbe4c9a1a39 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 17:29:48 -0500 Subject: [PATCH 1/6] Store admin log in reducer --- app/soapbox/features/admin/moderation_log.js | 20 ++++++--- app/soapbox/reducers/admin_log.js | 43 ++++++++++++++++++++ app/soapbox/reducers/index.js | 2 + 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 app/soapbox/reducers/admin_log.js diff --git a/app/soapbox/features/admin/moderation_log.js b/app/soapbox/features/admin/moderation_log.js index e84fd6ed6..3c6d5d7fe 100644 --- a/app/soapbox/features/admin/moderation_log.js +++ b/app/soapbox/features/admin/moderation_log.js @@ -1,41 +1,49 @@ import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import PropTypes from 'prop-types'; import Column from '../ui/components/column'; import ScrollableList from 'soapbox/components/scrollable_list'; import { fetchModerationLog } from 'soapbox/actions/admin'; -import { List as ImmutableList, fromJS } from 'immutable'; const messages = defineMessages({ heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' }, emptyMessage: { id: 'admin.moderation_log.empty_message', defaultMessage: 'You have not performed any moderation actions yet. When you do, a history will be shown here.' }, }); -export default @connect() +const mapStateToProps = state => ({ + items: state.getIn(['admin_log', 'index']).map(i => state.getIn(['admin_log', 'items', String(i)])), +}); + +export default @connect(mapStateToProps) @injectIntl class ModerationLog extends ImmutablePureComponent { static propTypes = { intl: PropTypes.object.isRequired, + list: ImmutablePropTypes.list, }; state = { isLoading: true, - items: ImmutableList(), + lastPage: 0, } componentDidMount() { const { dispatch } = this.props; dispatch(fetchModerationLog()) - .then(data => this.setState({ isLoading: false, items: fromJS(data.items) })) + .then(data => this.setState({ + isLoading: false, + lastPage: 1, + })) .catch(() => {}); } render() { - const { intl } = this.props; - const { isLoading, items } = this.state; + const { intl, items } = this.props; + const { isLoading } = this.state; const showLoading = isLoading && items.count() === 0; return ( diff --git a/app/soapbox/reducers/admin_log.js b/app/soapbox/reducers/admin_log.js new file mode 100644 index 000000000..874cab373 --- /dev/null +++ b/app/soapbox/reducers/admin_log.js @@ -0,0 +1,43 @@ +import { ADMIN_LOG_FETCH_SUCCESS } from 'soapbox/actions/admin'; +import { + Map as ImmutableMap, + OrderedSet as ImmutableOrderedSet, + fromJS, +} from 'immutable'; + +const initialState = ImmutableMap({ + items: ImmutableMap(), + index: ImmutableOrderedSet(), + total: 0, +}); + +const parseItems = items => { + let ids = []; + let map = {}; + + items.forEach(item => { + ids.push(item.id); + map[item.id] = item; + }); + + return { ids: ids, map: map }; +}; + +const importItems = (state, items, total) => { + const { ids, map } = parseItems(items); + + return state.withMutations(state => { + state.update('index', v => v.union(ids)); + state.update('items', v => v.merge(fromJS(map))); + state.set('total', total); + }); +}; + +export default function admin_log(state = initialState, action) { + switch(action.type) { + case ADMIN_LOG_FETCH_SUCCESS: + return importItems(state, action.items, action.total); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index e8cb4821b..45ab583d0 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -50,6 +50,7 @@ import chat_messages from './chat_messages'; import chat_message_lists from './chat_message_lists'; import profile_hover_card from './profile_hover_card'; import backups from './backups'; +import admin_log from './admin_log'; const appReducer = combineReducers({ dropdown_menu, @@ -101,6 +102,7 @@ const appReducer = combineReducers({ chat_message_lists, profile_hover_card, backups, + admin_log, }); // Clear the state (mostly) when the user logs out From 9156e01862a07876420b6172e306d12c586d340a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 17:54:48 -0500 Subject: [PATCH 2/6] Allow endless scrolling of moderation log --- app/soapbox/features/admin/moderation_log.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/soapbox/features/admin/moderation_log.js b/app/soapbox/features/admin/moderation_log.js index 3c6d5d7fe..41fda2b50 100644 --- a/app/soapbox/features/admin/moderation_log.js +++ b/app/soapbox/features/admin/moderation_log.js @@ -15,6 +15,7 @@ const messages = defineMessages({ const mapStateToProps = state => ({ items: state.getIn(['admin_log', 'index']).map(i => state.getIn(['admin_log', 'items', String(i)])), + hasMore: state.getIn(['admin_log', 'total'], 0) - state.getIn(['admin_log', 'index']).count() > 0, }); export default @connect(mapStateToProps) @@ -41,8 +42,20 @@ class ModerationLog extends ImmutablePureComponent { .catch(() => {}); } + handleLoadMore = () => { + const page = this.state.lastPage + 1; + + this.setState({ isLoading: true }); + this.props.dispatch(fetchModerationLog({ page })) + .then(data => this.setState({ + isLoading: false, + lastPage: page, + })) + .catch(() => {}); + } + render() { - const { intl, items } = this.props; + const { intl, items, hasMore } = this.props; const { isLoading } = this.state; const showLoading = isLoading && items.count() === 0; @@ -53,6 +66,8 @@ class ModerationLog extends ImmutablePureComponent { showLoading={showLoading} scrollKey='moderation-log' emptyMessage={intl.formatMessage(messages.emptyMessage)} + hasMore={hasMore} + onLoadMore={this.handleLoadMore} > {items.map((item, i) => (
From 6ed87aaf8959652c0b1851c01e3e498cb6890b5d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 18:17:00 -0500 Subject: [PATCH 3/6] Display timestamps in mod log --- app/soapbox/features/admin/moderation_log.js | 15 +++++++++++++-- app/styles/components/admin.scss | 6 ++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/admin/moderation_log.js b/app/soapbox/features/admin/moderation_log.js index 41fda2b50..853cb9c1e 100644 --- a/app/soapbox/features/admin/moderation_log.js +++ b/app/soapbox/features/admin/moderation_log.js @@ -1,5 +1,5 @@ import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedDate } from 'react-intl'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -71,7 +71,18 @@ class ModerationLog extends ImmutablePureComponent { > {items.map((item, i) => (
- {item.get('message')} +
{item.get('message')}
+
+ +
))} diff --git a/app/styles/components/admin.scss b/app/styles/components/admin.scss index db09ed55f..f8a562834 100644 --- a/app/styles/components/admin.scss +++ b/app/styles/components/admin.scss @@ -214,4 +214,10 @@ .logentry { padding: 15px; + + &__timestamp { + color: var(--primary-text-color--faint); + font-size: 13px; + text-align: right; + } } From 75df329a26e889857c06a160827fdf5c95473177 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 18:44:48 -0500 Subject: [PATCH 4/6] Display user account in deletion modal --- app/soapbox/actions/moderation.js | 9 ++++++++- app/styles/components/modal.scss | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js index 3cdf076df..2aa8896be 100644 --- a/app/soapbox/actions/moderation.js +++ b/app/soapbox/actions/moderation.js @@ -1,7 +1,9 @@ +import React from 'react'; import { defineMessages } from 'react-intl'; import { openModal } from 'soapbox/actions/modal'; import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; +import AccountContainer from 'soapbox/containers/account_container'; const messages = defineMessages({ deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' }, @@ -47,8 +49,13 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { const acct = state.getIn(['accounts', accountId, 'acct']); const name = state.getIn(['accounts', accountId, 'username']); + const message = (<> + + {intl.formatMessage(messages.deleteUserPrompt, { acct })} + ); + dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.deleteUserPrompt, { acct }), + message, confirm: intl.formatMessage(messages.deleteUserConfirm, { name }), onConfirm: () => { dispatch(deleteUsers([acct])).then(() => { diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 44f32c686..37c1bcf6e 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -604,6 +604,13 @@ } } } + + .account { + text-align: left; + background-color: var(--background-color); + border-radius: 4px; + margin-bottom: 16px; + } } .report-modal__target { From c44de0030cf90398e2857312f8f50c457ac23141 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 18:59:42 -0500 Subject: [PATCH 5/6] Display user favicon in deletion modal --- app/soapbox/actions/moderation.js | 11 ++++++++++- app/styles/components/modal.scss | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js index 2aa8896be..d2489658f 100644 --- a/app/soapbox/actions/moderation.js +++ b/app/soapbox/actions/moderation.js @@ -48,15 +48,24 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { const state = getState(); const acct = state.getIn(['accounts', accountId, 'acct']); const name = state.getIn(['accounts', accountId, 'username']); + const favicon = state.getIn(['accounts', accountId, 'pleroma', 'favicon']); const message = (<> {intl.formatMessage(messages.deleteUserPrompt, { acct })} ); + const confirm = (<> + {favicon && +
+ +
} + {intl.formatMessage(messages.deleteUserConfirm, { name })} + ); + dispatch(openModal('CONFIRM', { message, - confirm: intl.formatMessage(messages.deleteUserConfirm, { name }), + confirm, onConfirm: () => { dispatch(deleteUsers([acct])).then(() => { const message = intl.formatMessage(messages.userDeleted, { acct }); diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 37c1bcf6e..4327ac152 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -393,6 +393,23 @@ .button { flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: center; + + .submit__favicon { + width: 16px; + height: 16px; + margin-right: 8px; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + max-height: 100%; + } + } } } From 453290c6d7ccfd6980c417c228536a07be7aa80b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Mar 2021 19:29:42 -0500 Subject: [PATCH 6/6] Make it harder to accidentally delete a local user --- app/soapbox/actions/moderation.js | 6 ++++ .../ui/components/confirmation_modal.js | 34 +++++++++++++++++-- app/soapbox/utils/accounts.js | 5 +++ app/styles/components/modal.scss | 8 +++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/soapbox/actions/moderation.js b/app/soapbox/actions/moderation.js index d2489658f..cc05bd905 100644 --- a/app/soapbox/actions/moderation.js +++ b/app/soapbox/actions/moderation.js @@ -4,6 +4,7 @@ import { openModal } from 'soapbox/actions/modal'; import { deactivateUsers, deleteUsers, deleteStatus, toggleStatusSensitivity } from 'soapbox/actions/admin'; import snackbar from 'soapbox/actions/snackbar'; import AccountContainer from 'soapbox/containers/account_container'; +import { isLocal } from 'soapbox/utils/accounts'; const messages = defineMessages({ deactivateUserPrompt: { id: 'confirmations.admin.deactivate_user.message', defaultMessage: 'You are about to deactivate @{acct}. Deactivating a user is a reversible action.' }, @@ -11,6 +12,7 @@ const messages = defineMessages({ userDeactivated: { id: 'admin.users.user_deactivated_message', defaultMessage: '@{acct} was deactivated' }, deleteUserPrompt: { id: 'confirmations.admin.delete_user.message', defaultMessage: 'You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.' }, deleteUserConfirm: { id: 'confirmations.admin.delete_user.confirm', defaultMessage: 'Delete @{name}' }, + deleteLocalUserCheckbox: { id: 'confirmations.admin.delete_local_user.checkbox', defaultMessage: 'I understand that I am about to delete a local user.' }, userDeleted: { id: 'admin.users.user_deleted_message', defaultMessage: '@{acct} was deleted' }, deleteStatusPrompt: { id: 'confirmations.admin.delete_status.message', defaultMessage: 'You are about to delete a post by @{acct}. This action cannot be undone.' }, deleteStatusConfirm: { id: 'confirmations.admin.delete_status.confirm', defaultMessage: 'Delete post' }, @@ -49,6 +51,7 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { const acct = state.getIn(['accounts', accountId, 'acct']); const name = state.getIn(['accounts', accountId, 'username']); const favicon = state.getIn(['accounts', accountId, 'pleroma', 'favicon']); + const local = isLocal(state.getIn(['accounts', accountId])); const message = (<> @@ -63,9 +66,12 @@ export function deleteUserModal(intl, accountId, afterConfirm = () => {}) { {intl.formatMessage(messages.deleteUserConfirm, { name })} ); + const checkbox = local ? intl.formatMessage(messages.deleteLocalUserCheckbox) : false; + dispatch(openModal('CONFIRM', { message, confirm, + checkbox, onConfirm: () => { dispatch(deleteUsers([acct])).then(() => { const message = intl.formatMessage(messages.userDeleted, { acct }); diff --git a/app/soapbox/features/ui/components/confirmation_modal.js b/app/soapbox/features/ui/components/confirmation_modal.js index d4caac3cd..4c7a91145 100644 --- a/app/soapbox/features/ui/components/confirmation_modal.js +++ b/app/soapbox/features/ui/components/confirmation_modal.js @@ -2,21 +2,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, FormattedMessage } from 'react-intl'; import Button from '../../../components/button'; +import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms'; export default @injectIntl class ConfirmationModal extends React.PureComponent { static propTypes = { message: PropTypes.node.isRequired, - confirm: PropTypes.string.isRequired, + confirm: PropTypes.node.isRequired, onClose: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired, secondary: PropTypes.string, onSecondary: PropTypes.func, intl: PropTypes.object.isRequired, onCancel: PropTypes.func, + checkbox: PropTypes.node, }; + state = { + checked: false, + } + componentDidMount() { this.button.focus(); } @@ -37,12 +43,17 @@ class ConfirmationModal extends React.PureComponent { if (onCancel) onCancel(); } + handleCheckboxChange = e => { + this.setState({ checked: e.target.checked }); + } + setRef = (c) => { this.button = c; } render() { - const { message, confirm, secondary } = this.props; + const { message, confirm, secondary, checkbox } = this.props; + const { checked } = this.state; return (
@@ -50,6 +61,18 @@ class ConfirmationModal extends React.PureComponent { {message}
+ {checkbox &&
+ + + + + +
} +
); diff --git a/app/soapbox/utils/accounts.js b/app/soapbox/utils/accounts.js index 929429b24..acc4cde65 100644 --- a/app/soapbox/utils/accounts.js +++ b/app/soapbox/utils/accounts.js @@ -40,3 +40,8 @@ export const getFollowDifference = (state, accountId, type) => { const counter = state.getIn(['accounts_counters', accountId, `${type}_count`], 0); return Math.max(counter - listSize, 0); }; + +export const isLocal = account => { + let domain = account.get('acct').split('@')[1]; + return domain === undefined ? true : false; +}; diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 4327ac152..e64965da4 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -630,6 +630,14 @@ } } +.confirmation-modal__checkbox { + padding: 0 30px; + + .simple_form { + margin-top: -14px; + } +} + .report-modal__target { padding: 20px;