import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import React, { useState, useEffect, useMemo } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; 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, HStack, Input } from 'soapbox/components/ui'; import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield'; import { SimpleForm, FieldsGroup, TextInput, SimpleInput, SimpleTextarea, FileChooserLogo, Checkbox, } from 'soapbox/features/forms'; import ThemeToggle from 'soapbox/features/ui/components/theme-toggle'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import Accordion from '../ui/components/accordion'; import ColorWithPicker from './components/color-with-picker'; import IconPicker from './components/icon-picker'; import SitePreview from './components/site-preview'; import type { ColorChangeHandler, ColorResult } from 'react-color'; import type { CryptoAddress } from 'soapbox/types/soapbox'; 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' }, }); type ValueGetter = (e: React.ChangeEvent) => any; type ColorValueGetter = (color: ColorResult, event: React.ChangeEvent) => any; type Template = ImmutableMap; type ConfigPath = Array; const templates: Record = { promoPanelItem: ImmutableMap({ icon: '', text: '', url: '' }), footerItem: ImmutableMap({ title: '', url: '' }), cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), }; const CryptoAddressInput: StreamfieldComponent = ({ value, onChange }) => { const intl = useIntl(); const handleChange = (key: 'ticker' | 'address' | 'note'): React.ChangeEventHandler => { return e => { onChange(value.set(key, e.currentTarget.value)); }; }; return ( ); }; const SoapboxConfig: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const initialData = useAppSelector(state => state.soapbox); const [isLoading, setLoading] = useState(false); const [data, setData] = useState(initialData); const [jsonEditorExpanded, setJsonEditorExpanded] = useState(false); const [rawJSON, setRawJSON] = useState(JSON.stringify(initialData, null, 2)); const [jsonValid, setJsonValid] = useState(true); const soapbox = useMemo(() => { return normalizeSoapboxConfig(data); }, [data]); const setConfig = (path: ConfigPath, value: any) => { const newData = data.setIn(path, value); setData(newData); setJsonValid(true); }; const putConfig = (newData: any) => { setData(newData); setJsonValid(true); }; const getParams = () => { return [{ group: ':pleroma', key: ':frontend_configurations', value: [{ tuple: [':soapbox_fe', data.toJS()], }], }]; }; const handleSubmit: React.FormEventHandler = (e) => { dispatch(updateConfig(getParams())).then(() => { setLoading(false); dispatch(snackbar.success(intl.formatMessage(messages.saved))); }).catch(() => { setLoading(false); }); setLoading(true); e.preventDefault(); }; const handleChange = (path: ConfigPath, getValue: ValueGetter): React.ChangeEventHandler => { return e => { setConfig(path, getValue(e)); }; }; const handleColorChange = (path: ConfigPath, getValue: ColorValueGetter): ColorChangeHandler => { return (color, event) => { setConfig(path, getValue(color, event)); }; }; const handleFileChange = (path: ConfigPath): React.ChangeEventHandler => { return e => { const data = new FormData(); const file = e.target.files?.item(0); if (file) { data.append('file', file); dispatch(uploadMedia(data)).then(({ data }: any) => { handleChange(path, () => data.url)(e); }).catch(console.error); } }; }; const handleAddItem = (path: ConfigPath, template: ImmutableMap) => { const value = (soapbox.getIn(path) || ImmutableList()) as ImmutableList; return () => { setConfig( path, value.push(template), ); }; }; const handleDeleteItem = (path: ConfigPath) => { return () => { const newData = data.deleteIn(path); setData(newData); }; }; const handleItemChange = ( path: Array, key: string, field: ImmutableMap, template: Template, getValue: ValueGetter = e => e.target.value, ) => { return handleChange( path, (e) => template .merge(field) .set(key, getValue(e)), ); }; const handlePromoItemChange = (index: number, key: string, field: any, getValue?: ValueGetter) => { return handleItemChange( ['promoPanel', 'items', index], key, field, templates.promoPanelItem, getValue, ); }; const handleHomeFooterItemChange = (index: number, key: string, field: any, getValue?: ValueGetter) => { return handleItemChange( ['navlinks', 'homeFooter', index], key, field, templates.footerItem, getValue, ); }; const handleCryptoAdressChange = (values: CryptoAddress[]) => { setConfig(['cryptoAddresses'], ImmutableList(values)); }; const addCryptoAddress = () => { const cryptoAddresses = data.get('cryptoAddresses'); setConfig(['cryptoAddresses'], cryptoAddresses.push(templates.cryptoAddress)); }; const removeCryptoAddress = (i: number) => { const cryptoAddresses = data.get('cryptoAddresses'); setConfig(['cryptoAddresses'], cryptoAddresses.delete(i)); }; const handleEditJSON: React.ChangeEventHandler = e => { setRawJSON(e.target.value); }; const toggleJSONEditor = (expanded: boolean) => setJsonEditorExpanded(expanded); useEffect(() => { putConfig(initialData); }, [initialData]); useEffect(() => { setRawJSON(JSON.stringify(data, null, 2)); }, [data]); useEffect(() => { try { const data = fromJS(JSON.parse(rawJSON)); putConfig(data); } catch { setJsonValid(false); } }, [rawJSON]); return (
} value={soapbox.brandColor} onChange={handleColorChange(['brandColor'], (color) => color.hex)} /> } value={soapbox.accentColor} onChange={handleColorChange(['accentColor'], (color) => color.hex)} />
{/* value)} themeMode={soapbox.defaultSettings.get('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.promoPanel.items.map((field, i) => (
val.id)} />
)) }
{ soapbox.navlinks.get('homeFooter')?.map((field, i) => (
)) }
} hint={} component={CryptoAddressInput} values={soapbox.cryptoAddresses.toArray()} onChange={handleCryptoAdressChange} onAddItem={addCryptoAddress} onRemoveItem={removeCryptoAddress} /> Number(e.target.value))} />
); }; export default SoapboxConfig;