import { supportsPassiveEvents } from 'detect-passive-events'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import PropTypes from 'prop-types'; import React from 'react'; import { SketchPicker } from 'react-color'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Overlay from 'react-overlays/lib/Overlay'; import { connect } from 'react-redux'; import { updateConfig } from 'soapbox/actions/admin'; import { uploadMedia } from 'soapbox/actions/media'; import snackbar from 'soapbox/actions/snackbar'; import Icon from 'soapbox/components/icon'; import { Column } from 'soapbox/components/ui'; import { SimpleForm, FieldsGroup, TextInput, SimpleInput, SimpleTextarea, FileChooserLogo, FormPropTypes, Checkbox, } from 'soapbox/features/forms'; import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; import { isMobile } from 'soapbox/is_mobile'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import Accordion from '../ui/components/accordion'; import IconPickerDropdown from './components/icon_picker_dropdown'; import SitePreview from './components/site_preview'; const messages = defineMessages({ heading: { id: 'column.soapbox_config', defaultMessage: 'Soapbox config' }, saved: { id: 'soapbox_config.saved', defaultMessage: 'Soapbox config saved!' }, copyrightFooterLabel: { id: 'soapbox_config.copyright_footer.meta_fields.label_placeholder', defaultMessage: 'Copyright footer' }, promoItemIcon: { id: 'soapbox_config.promo_panel.meta_fields.icon_placeholder', defaultMessage: 'Icon' }, promoItemLabel: { id: 'soapbox_config.promo_panel.meta_fields.label_placeholder', defaultMessage: 'Label' }, promoItemURL: { id: 'soapbox_config.promo_panel.meta_fields.url_placeholder', defaultMessage: 'URL' }, homeFooterItemLabel: { id: 'soapbox_config.home_footer.meta_fields.label_placeholder', defaultMessage: 'Label' }, homeFooterItemURL: { id: 'soapbox_config.home_footer.meta_fields.url_placeholder', defaultMessage: 'URL' }, cryptoAdressItemTicker: { id: 'soapbox_config.crypto_address.meta_fields.ticker_placeholder', defaultMessage: 'Ticker' }, cryptoAdressItemAddress: { id: 'soapbox_config.crypto_address.meta_fields.address_placeholder', defaultMessage: 'Address' }, cryptoAdressItemNote: { id: 'soapbox_config.crypto_address.meta_fields.note_placeholder', defaultMessage: 'Note (optional)' }, cryptoDonatePanelLimitLabel: { id: 'soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder', defaultMessage: 'Number of items to display in the crypto homepage widget' }, customCssLabel: { id: 'soapbox_config.custom_css.meta_fields.url_placeholder', defaultMessage: 'URL' }, rawJSONLabel: { id: 'soapbox_config.raw_json_label', defaultMessage: 'Advanced: Edit raw JSON data' }, rawJSONHint: { id: 'soapbox_config.raw_json_hint', defaultMessage: 'Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click "Save" to apply your changes.' }, verifiedCanEditNameLabel: { id: 'soapbox_config.verified_can_edit_name_label', defaultMessage: 'Allow verified users to edit their own display name.' }, displayFqnLabel: { id: 'soapbox_config.display_fqn_label', defaultMessage: 'Display domain (eg @user@domain) for local accounts.' }, greentextLabel: { id: 'soapbox_config.greentext_label', defaultMessage: 'Enable greentext support' }, promoPanelIconsLink: { id: 'soapbox_config.hints.promo_panel_icons.link', defaultMessage: 'Soapbox Icons List' }, authenticatedProfileLabel: { id: 'soapbox_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' }, authenticatedProfileHint: { id: 'soapbox_config.authenticated_profile_hint', defaultMessage: 'Users must be logged-in to view replies and media on user profiles.' }, singleUserModeLabel: { id: 'soapbox_config.single_user_mode_label', defaultMessage: 'Single user mode' }, singleUserModeHint: { id: 'soapbox_config.single_user_mode_hint', defaultMessage: 'Front page will redirect to a given user profile.' }, singleUserModeProfileLabel: { id: 'soapbox_config.single_user_mode_profile_label', defaultMessage: 'Main user handle' }, singleUserModeProfileHint: { id: 'soapbox_config.single_user_mode_profile_hint', defaultMessage: '@handle' }, }); const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const templates = { promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), footerItem: ImmutableMap({ title: '', url: '' }), cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), }; const mapStateToProps = state => { return { initialData: state.soapbox, }; }; export default @connect(mapStateToProps) @injectIntl class SoapboxConfig extends ImmutablePureComponent { static propTypes = { initialData: ImmutablePropTypes.map.isRequired, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { isLoading: false, data: this.props.initialData, jsonEditorExpanded: false, rawJSON: JSON.stringify(this.props.soapbox, null, 2), jsonValid: true, } setConfig = (path, value) => { const { data } = this.state; const newData = data.setIn(path, value); this.setState({ data: newData, jsonValid: true }); }; putConfig = data => { this.setState({ data, jsonValid: true }); }; getParams = () => { const { data } = this.state; return [{ group: ':pleroma', key: ':frontend_configurations', value: [{ tuple: [':soapbox_fe', data.toJS()], }], }]; } handleSubmit = (event) => { const { dispatch, intl } = this.props; dispatch(updateConfig(this.getParams())).then(() => { this.setState({ isLoading: false }); dispatch(snackbar.success(intl.formatMessage(messages.saved))); }).catch((error) => { this.setState({ isLoading: false }); }); this.setState({ isLoading: true }); event.preventDefault(); } handleChange = (path, getValue) => { return e => { this.setConfig(path, getValue(e)); }; }; handleFileChange = path => { return e => { const data = new FormData(); data.append('file', e.target.files[0]); this.props.dispatch(uploadMedia(data)).then(({ data }) => { this.handleChange(path, e => data.url)(e); }).catch(() => {}); }; }; handleAddItem = (path, template) => { return e => { this.setConfig( path, this.getSoapboxConfig().getIn(path, ImmutableList()).push(template), ); }; }; handleDeleteItem = path => { return e => { const data = this.state.data.deleteIn(path); this.setState({ data }); }; }; handleItemChange = (path, key, field, template, getValue = e => e.target.value) => { return this.handleChange( path, (e) => template .merge(field) .set(key, getValue(e)), ); }; handlePromoItemChange = (index, key, field, getValue) => { return this.handleItemChange( ['promoPanel', 'items', index], key, field, templates.promoPanelItem, getValue, ); }; handleHomeFooterItemChange = (index, key, field, getValue) => { return this.handleItemChange( ['navlinks', 'homeFooter', index], key, field, templates.footerItem, getValue, ); }; handleCryptoAdressItemChange = (index, key, field, getValue) => { return this.handleItemChange( ['cryptoAddresses', index], key, field, templates.cryptoAddress, getValue, ); }; handleEditJSON = e => { this.setState({ rawJSON: e.target.value }); } getSoapboxConfig = () => { return normalizeSoapboxConfig(this.state.data); } toggleJSONEditor = (value) => this.setState({ jsonEditorExpanded: value }); componentDidUpdate(prevProps, prevState) { if (prevProps.initialData !== this.props.initialData) { this.putConfig(this.props.initialData); } if (prevState.data !== this.state.data) { this.setState({ rawJSON: JSON.stringify(this.state.data, null, 2) }); } if (prevState.rawJSON !== this.state.rawJSON) { try { const data = fromJS(JSON.parse(this.state.rawJSON)); this.putConfig(data); } catch { this.setState({ jsonValid: false }); } } } render() { const { intl } = this.props; const soapbox = this.getSoapboxConfig(); return (
} value={soapbox.get('brandColor')} onChange={this.handleChange(['brandColor'], (e) => e.hex)} /> } value={soapbox.get('accentColor')} onChange={this.handleChange(['accentColor'], (e) => e.hex)} />
value)} themeMode={soapbox.getIn(['defaultSettings', 'themeMode'])} intl={intl} />
} name='logo' hint={
e.target.value)} /> e.target.checked)} /> e.target.checked)} /> e.target.checked)} /> e.target.checked)} /> e.target.checked)} /> {soapbox.get('singleUserMode') && ( e.target.value)} /> )}
{intl.formatMessage(messages.promoPanelIconsLink)} }} /> { soapbox.getIn(['promoPanel', 'items']).map((field, i) => (
val.id)} />
)) }
{ soapbox.getIn(['navlinks', 'homeFooter']).map((field, i) => (
)) }
{ soapbox.get('cryptoAddresses').map((address, i) => (
)) }
Number(e.target.value))} />
); } } class ColorPicker extends React.PureComponent { static propTypes = { style: PropTypes.object, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, onClose: PropTypes.func, } handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); } } componentDidMount() { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); } componentWillUnmount() { document.removeEventListener('click', this.handleDocumentClick, false); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } setRef = c => { this.node = c; } render() { const { style, value, onChange } = this.props; const margin_left_picker = isMobile(window.innerWidth) ? '20px' : '12px'; return (
); } } class ColorWithPicker extends ImmutablePureComponent { static propTypes = { buttonId: PropTypes.string.isRequired, label: FormPropTypes.label, value: PropTypes.string.isRequired, onChange: PropTypes.func.isRequired, } onToggle = (e) => { if (!e.key || e.key === 'Enter') { if (this.state.active) { this.onHidePicker(); } else { this.onShowPicker(e); } } } state = { active: false, placement: null, } onHidePicker = () => { this.setState({ active: false }); } onShowPicker = ({ target }) => { this.setState({ active: true }); this.setState({ placement: isMobile(window.innerWidth) ? 'bottom' : 'right' }); } render() { const { buttonId, label, value, onChange } = this.props; const { active, placement } = this.state; return (
); } } export class IconPicker extends ImmutablePureComponent { static propTypes = { icons: PropTypes.object, label: FormPropTypes.label, value: PropTypes.string, onChange: PropTypes.func.isRequired, } render() { const { onChange, value, label } = this.props; return (
{label && ()}
); } }