From 846b73eb9d540a524610f551b3082d856960d27c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 26 Jul 2021 14:25:55 -0500 Subject: [PATCH 1/4] RemoteTimeline: display dropdown menu to admins --- .../ui/components/instance_info_panel.js | 35 +++++++++++++++++-- app/styles/components/wtf-panel.scss | 4 +++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/ui/components/instance_info_panel.js b/app/soapbox/features/ui/components/instance_info_panel.js index ee8440262..7c677ca4c 100644 --- a/app/soapbox/features/ui/components/instance_info_panel.js +++ b/app/soapbox/features/ui/components/instance_info_panel.js @@ -4,21 +4,33 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage } from 'react-intl'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { makeGetRemoteInstance } from 'soapbox/selectors'; import InstanceRestrictions from 'soapbox/features/federation_restrictions/components/instance_restrictions'; +import DropdownMenu from 'soapbox/containers/dropdown_menu_container'; +import { openModal } from 'soapbox/actions/modal'; +import { isAdmin } from 'soapbox/utils/accounts'; const getRemoteInstance = makeGetRemoteInstance(); +const messages = defineMessages({ + editFederation: { id: 'remote_instance.edit_federation', defaultMessage: 'Edit federation' }, +}); + const mapStateToProps = (state, { host }) => { + const me = state.get('me'); + const account = state.getIn(['accounts', me]); + return { instance: state.get('instance'), remoteInstance: getRemoteInstance(state, host), + isAdmin: isAdmin(account), }; }; export default @connect(mapStateToProps, null, null, { forwardRef: true }) +@injectIntl class InstanceInfoPanel extends ImmutablePureComponent { static propTypes = { @@ -26,10 +38,26 @@ class InstanceInfoPanel extends ImmutablePureComponent { host: PropTypes.string.isRequired, instance: ImmutablePropTypes.map, remoteInstance: ImmutablePropTypes.map, + isAdmin: PropTypes.bool, }; + handleEditFederation = e => { + const { dispatch, host } = this.props; + dispatch(openModal('EDIT_FEDERATION', { host })); + } + + makeMenu = () => { + const { intl } = this.props; + + return [{ + text: intl.formatMessage(messages.editFederation), + action: this.handleEditFederation, + }]; + } + render() { - const { remoteInstance } = this.props; + const { remoteInstance, isAdmin } = this.props; + const menu = this.makeMenu(); return (
@@ -38,6 +66,9 @@ class InstanceInfoPanel extends ImmutablePureComponent { + {isAdmin &&
+ +
}
diff --git a/app/styles/components/wtf-panel.scss b/app/styles/components/wtf-panel.scss index ce46a038a..809d065f4 100644 --- a/app/styles/components/wtf-panel.scss +++ b/app/styles/components/wtf-panel.scss @@ -148,4 +148,8 @@ color: var(--primary-text-color); text-decoration: none; } + + &__menu { + margin-left: auto; + } } From 6c4be4a5152ba7204d5dc9fd54e8a5ba373ca3a9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 26 Jul 2021 15:07:35 -0500 Subject: [PATCH 2/4] Create EditFederationModal --- .../ui/components/edit_federation_modal.js | 51 +++++++++++++++++++ .../features/ui/components/modal_root.js | 2 + .../components/federation-restrictions.scss | 6 +++ 3 files changed, 59 insertions(+) create mode 100644 app/soapbox/features/ui/components/edit_federation_modal.js diff --git a/app/soapbox/features/ui/components/edit_federation_modal.js b/app/soapbox/features/ui/components/edit_federation_modal.js new file mode 100644 index 000000000..c83ba6557 --- /dev/null +++ b/app/soapbox/features/ui/components/edit_federation_modal.js @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { SimpleForm, Checkbox } from 'soapbox/features/forms'; +import { makeGetRemoteInstance } from 'soapbox/selectors'; + +const getRemoteInstance = makeGetRemoteInstance(); + +const mapStateToProps = (state, { host }) => { + return { + remoteInstance: getRemoteInstance(state, host), + }; +}; + +export default @connect(mapStateToProps) +class EditFederationModal extends React.PureComponent { + + static propTypes = { + host: PropTypes.string.isRequired, + remoteInstance: ImmutablePropTypes.map, + }; + + render() { + const { remoteInstance } = this.props; + + const { + avatar_removal, + banner_removal, + federated_timeline_removal, + followers_only, + media_nsfw, + media_removal, + reject, + } = remoteInstance.get('federation').toJS(); + + return ( +
+

{remoteInstance.get('host')}

+ + + + + + + +
+ ); + } + +} diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 657df1a26..ab690d44e 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -15,6 +15,7 @@ import HotkeysModal from './hotkeys_modal'; import ComposeModal from './compose_modal'; import UnauthorizedModal from './unauthorized_modal'; import CryptoDonateModal from './crypto_donate_modal'; +import EditFederationModal from './edit_federation_modal'; import { MuteModal, @@ -41,6 +42,7 @@ const MODAL_COMPONENTS = { 'COMPOSE': () => Promise.resolve({ default: ComposeModal }), 'UNAUTHORIZED': () => Promise.resolve({ default: UnauthorizedModal }), 'CRYPTO_DONATE': () => Promise.resolve({ default: CryptoDonateModal }), + 'EDIT_FEDERATION': () => Promise.resolve({ default: EditFederationModal }), }; export default class ModalRoot extends React.PureComponent { diff --git a/app/styles/components/federation-restrictions.scss b/app/styles/components/federation-restrictions.scss index b14b601f4..c18dae67d 100644 --- a/app/styles/components/federation-restrictions.scss +++ b/app/styles/components/federation-restrictions.scss @@ -63,3 +63,9 @@ } } } + +.edit-federation-modal { + background: var(--foreground-color); + border-radius: 8px; + padding: 20px; +} From 9536fba7a9f86ac495604e842a6108436f2ae67f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 27 Jul 2021 13:00:32 -0500 Subject: [PATCH 3/4] EditFederationModal: improve UI --- .../ui/components/edit_federation_modal.js | 99 ++++++++++++++++--- .../components/federation-restrictions.scss | 12 +++ 2 files changed, 100 insertions(+), 11 deletions(-) diff --git a/app/soapbox/features/ui/components/edit_federation_modal.js b/app/soapbox/features/ui/components/edit_federation_modal.js index c83ba6557..4a914148b 100644 --- a/app/soapbox/features/ui/components/edit_federation_modal.js +++ b/app/soapbox/features/ui/components/edit_federation_modal.js @@ -1,12 +1,24 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; import { SimpleForm, Checkbox } from 'soapbox/features/forms'; import { makeGetRemoteInstance } from 'soapbox/selectors'; +import { Map as ImmutableMap } from 'immutable'; const getRemoteInstance = makeGetRemoteInstance(); +const messages = defineMessages({ + reject: { id: 'edit_federation.reject', defaultMessage: 'Reject all activities' }, + mediaRemoval: { id: 'edit_federation.media_removal', defaultMessage: 'Strip media' }, + forceNsfw: { id: 'edit_federation.force_nsfw', defaultMessage: 'Force attachments to be marked sensitive' }, + unlisted: { id: 'edit_federation.unlisted', defaultMessage: 'Force posts unlisted' }, + followersOnly: { id: 'edit_federation.followers_only', defaultMessage: 'Hide posts except to followers' }, + save: { id: 'edit_federation.save', defaultMessage: 'Save' }, +}); + const mapStateToProps = (state, { host }) => { return { remoteInstance: getRemoteInstance(state, host), @@ -14,15 +26,47 @@ const mapStateToProps = (state, { host }) => { }; export default @connect(mapStateToProps) -class EditFederationModal extends React.PureComponent { +@injectIntl +class EditFederationModal extends ImmutablePureComponent { static propTypes = { host: PropTypes.string.isRequired, remoteInstance: ImmutablePropTypes.map, }; - render() { + state = { + data: ImmutableMap(), + } + + componentDidMount() { const { remoteInstance } = this.props; + this.setState({ data: remoteInstance.get('federation') }); + } + + handleDataChange = key => { + return ({ target }) => { + const { data } = this.state; + this.setState({ data: data.set(key, target.checked) }); + }; + } + + handleMediaRemoval = ({ target: { checked } }) => { + const data = this.state.data.merge({ + avatar_removal: checked, + banner_removal: checked, + media_removal: checked, + }); + + this.setState({ data }); + } + + handleSubmit = e => { + // TODO + } + + render() { + const { intl, remoteInstance } = this.props; + const { data } = this.state; const { avatar_removal, @@ -32,18 +76,51 @@ class EditFederationModal extends React.PureComponent { media_nsfw, media_removal, reject, - } = remoteInstance.get('federation').toJS(); + } = data.toJS(); + + const fullMediaRemoval = avatar_removal && banner_removal && media_removal; return (
-

{remoteInstance.get('host')}

- - - - - - - +
+
+ {remoteInstance.get('host')} +
+ + + + + + + + +
); } diff --git a/app/styles/components/federation-restrictions.scss b/app/styles/components/federation-restrictions.scss index c18dae67d..72dacc118 100644 --- a/app/styles/components/federation-restrictions.scss +++ b/app/styles/components/federation-restrictions.scss @@ -68,4 +68,16 @@ background: var(--foreground-color); border-radius: 8px; padding: 20px; + + &__title { + font-size: 18px; + margin-bottom: 15px; + font-weight: bold; + text-align: center; + } + + &__submit { + margin-bottom: 0 !important; + margin-top: 20px; + } } From 1fa3aa0008d8cf7d91b66cb786a5c75359bc0dfc Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 27 Jul 2021 14:40:41 -0500 Subject: [PATCH 4/4] EditFederationModal: handle submission --- app/soapbox/actions/instance.js | 2 +- app/soapbox/actions/mrf.js | 44 +++++++++++++++++++ .../ui/components/edit_federation_modal.js | 12 ++++- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/actions/mrf.js diff --git a/app/soapbox/actions/instance.js b/app/soapbox/actions/instance.js index 23d628872..94b5da79d 100644 --- a/app/soapbox/actions/instance.js +++ b/app/soapbox/actions/instance.js @@ -9,7 +9,7 @@ export const NODEINFO_FETCH_FAIL = 'NODEINFO_FETCH_FAIL'; export function fetchInstance() { return (dispatch, getState) => { - api(getState).get('/api/v1/instance').then(response => { + return api(getState).get('/api/v1/instance').then(response => { dispatch(importInstance(response.data)); const v = parseVersion(get(response.data, 'version')); if (v.software === 'Pleroma' && !get(response.data, ['pleroma', 'metadata'])) { diff --git a/app/soapbox/actions/mrf.js b/app/soapbox/actions/mrf.js new file mode 100644 index 000000000..9cddf85f8 --- /dev/null +++ b/app/soapbox/actions/mrf.js @@ -0,0 +1,44 @@ +import { fetchInstance } from './instance'; +import { updateConfig } from './admin'; +import { Set as ImmutableSet } from 'immutable'; + +const simplePolicyMerge = (simplePolicy, host, restrictions) => { + return simplePolicy.map((hosts, key) => { + const isRestricted = restrictions.get(key); + + if (isRestricted) { + return ImmutableSet(hosts).add(host); + } else { + return ImmutableSet(hosts).delete(host); + } + }); +}; + +const simplePolicyToConfig = simplePolicy => { + const value = simplePolicy.map((hosts, key) => ( + { tuple: [`:${key}`, hosts.toJS()] } + )).toList(); + + return [{ + group: ':pleroma', + key: ':mrf_simple', + value, + }]; +}; + +export function updateMrf(host, restrictions) { + return (dispatch, getState) => { + return dispatch(fetchInstance()) + .then(() => { + const simplePolicy = getState().getIn(['instance', 'pleroma', 'metadata', 'federation', 'mrf_simple']); + const merged = simplePolicyMerge(simplePolicy, host, restrictions); + const config = simplePolicyToConfig(merged); + dispatch(updateConfig(config)); + + // TODO: Make this less insane + setTimeout(() => { + dispatch(fetchInstance()); + }, 1000); + }); + }; +} diff --git a/app/soapbox/features/ui/components/edit_federation_modal.js b/app/soapbox/features/ui/components/edit_federation_modal.js index 4a914148b..abf7c2c3f 100644 --- a/app/soapbox/features/ui/components/edit_federation_modal.js +++ b/app/soapbox/features/ui/components/edit_federation_modal.js @@ -7,6 +7,8 @@ import { defineMessages, injectIntl } from 'react-intl'; import { SimpleForm, Checkbox } from 'soapbox/features/forms'; import { makeGetRemoteInstance } from 'soapbox/selectors'; import { Map as ImmutableMap } from 'immutable'; +import { updateMrf } from 'soapbox/actions/mrf'; +import snackbar from 'soapbox/actions/snackbar'; const getRemoteInstance = makeGetRemoteInstance(); @@ -17,6 +19,7 @@ const messages = defineMessages({ unlisted: { id: 'edit_federation.unlisted', defaultMessage: 'Force posts unlisted' }, followersOnly: { id: 'edit_federation.followers_only', defaultMessage: 'Hide posts except to followers' }, save: { id: 'edit_federation.save', defaultMessage: 'Save' }, + success: { id: 'edit_federation.success', defaultMessage: '{host} federation was updated' }, }); const mapStateToProps = (state, { host }) => { @@ -61,7 +64,14 @@ class EditFederationModal extends ImmutablePureComponent { } handleSubmit = e => { - // TODO + const { intl, dispatch, host, onClose } = this.props; + const { data } = this.state; + + dispatch(updateMrf(host, data)) + .then(() => dispatch(snackbar.success(intl.formatMessage(messages.success, { host })))) + .catch(() => {}); + + onClose(); } render() {