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 new file mode 100644 index 000000000..abf7c2c3f --- /dev/null +++ b/app/soapbox/features/ui/components/edit_federation_modal.js @@ -0,0 +1,138 @@ +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'; +import { updateMrf } from 'soapbox/actions/mrf'; +import snackbar from 'soapbox/actions/snackbar'; + +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' }, + success: { id: 'edit_federation.success', defaultMessage: '{host} federation was updated' }, +}); + +const mapStateToProps = (state, { host }) => { + return { + remoteInstance: getRemoteInstance(state, host), + }; +}; + +export default @connect(mapStateToProps) +@injectIntl +class EditFederationModal extends ImmutablePureComponent { + + static propTypes = { + host: PropTypes.string.isRequired, + remoteInstance: ImmutablePropTypes.map, + }; + + 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 => { + 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() { + const { intl, remoteInstance } = this.props; + const { data } = this.state; + + const { + avatar_removal, + banner_removal, + federated_timeline_removal, + followers_only, + media_nsfw, + media_removal, + reject, + } = data.toJS(); + + const fullMediaRemoval = avatar_removal && banner_removal && media_removal; + + return ( +
+
+
+ {remoteInstance.get('host')} +
+ + + + + + + + +
+
+ ); + } + +} 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/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..72dacc118 100644 --- a/app/styles/components/federation-restrictions.scss +++ b/app/styles/components/federation-restrictions.scss @@ -63,3 +63,21 @@ } } } + +.edit-federation-modal { + 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; + } +} 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; + } }