2022-05-05 12:25:57 -07:00
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' ;
2022-05-05 16:30:25 -07:00
import List , { ListItem } from 'soapbox/components/list' ;
2022-05-05 16:46:16 -07:00
import {
Column ,
CardHeader ,
CardTitle ,
Form ,
FormActions ,
FormGroup ,
Input ,
2022-05-16 16:33:44 -07:00
FileInput ,
2022-05-05 16:46:16 -07:00
Textarea ,
Button ,
2022-05-06 12:55:16 -07:00
Toggle ,
2022-05-05 16:46:16 -07:00
} from 'soapbox/components/ui' ;
2022-05-05 14:18:43 -07:00
import Streamfield from 'soapbox/components/ui/streamfield/streamfield' ;
2022-05-05 16:12:08 -07:00
import ThemeSelector from 'soapbox/features/ui/components/theme-selector' ;
2022-05-05 12:25:57 -07:00
import { useAppSelector , useAppDispatch } from 'soapbox/hooks' ;
import { normalizeSoapboxConfig } from 'soapbox/normalizers' ;
import Accordion from '../ui/components/accordion' ;
import ColorWithPicker from './components/color-with-picker' ;
2022-05-05 13:12:38 -07:00
import CryptoAddressInput from './components/crypto-address-input' ;
2022-05-05 14:18:43 -07:00
import FooterLinkInput from './components/footer-link-input' ;
2022-05-05 13:57:30 -07:00
import PromoPanelInput from './components/promo-panel-input' ;
2022-05-05 12:25:57 -07:00
import SitePreview from './components/site-preview' ;
import type { ColorChangeHandler , ColorResult } from 'react-color' ;
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' } ,
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 < T = Element > = ( e : React.ChangeEvent < T > ) = > any ;
type ColorValueGetter = ( color : ColorResult , event : React.ChangeEvent < HTMLInputElement > ) = > any ;
type Template = ImmutableMap < string , any > ;
type ConfigPath = Array < string | number > ;
2022-05-05 16:12:08 -07:00
type ThemeChangeHandler = ( theme : string ) = > void ;
2022-05-05 12:25:57 -07:00
const templates : Record < string , Template > = {
promoPanelItem : ImmutableMap ( { icon : '' , text : '' , url : '' } ) ,
footerItem : ImmutableMap ( { title : '' , url : '' } ) ,
cryptoAddress : ImmutableMap ( { ticker : '' , address : '' , note : '' } ) ,
} ;
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 < string > ( 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 < any > ) : React . ChangeEventHandler = > {
return e = > {
setConfig ( path , getValue ( e ) ) ;
} ;
} ;
2022-05-05 16:12:08 -07:00
const handleThemeChange = ( path : ConfigPath ) : ThemeChangeHandler = > {
return theme = > {
setConfig ( path , theme ) ;
} ;
} ;
2022-05-05 12:25:57 -07:00
const handleColorChange = ( path : ConfigPath , getValue : ColorValueGetter ) : ColorChangeHandler = > {
return ( color , event ) = > {
setConfig ( path , getValue ( color , event ) ) ;
} ;
} ;
const handleFileChange = ( path : ConfigPath ) : React . ChangeEventHandler < HTMLInputElement > = > {
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 ) ;
}
} ;
} ;
2022-05-05 13:52:25 -07:00
const handleStreamItemChange = ( path : ConfigPath ) = > {
return ( values : any [ ] ) = > {
setConfig ( path , ImmutableList ( values ) ) ;
} ;
2022-05-05 12:25:57 -07:00
} ;
2022-05-05 13:52:25 -07:00
const addStreamItem = ( path : ConfigPath , template : Template ) = > {
return ( ) = > {
const items = data . getIn ( path ) ;
setConfig ( path , items . push ( template ) ) ;
} ;
2022-05-05 13:06:52 -07:00
} ;
2022-05-05 13:52:25 -07:00
const deleteStreamItem = ( path : ConfigPath ) = > {
return ( i : number ) = > {
2022-05-05 14:18:43 -07:00
const newData = data . deleteIn ( [ . . . path , i ] ) ;
setData ( newData ) ;
2022-05-05 13:52:25 -07:00
} ;
2022-05-05 13:06:52 -07:00
} ;
2022-05-05 12:25:57 -07:00
const handleEditJSON : React.ChangeEventHandler < HTMLTextAreaElement > = 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 (
< Column label = { intl . formatMessage ( messages . heading ) } >
2022-05-05 15:37:25 -07:00
< Form onSubmit = { handleSubmit } >
2022-05-05 14:56:34 -07:00
< fieldset className = 'space-y-6' disabled = { isLoading } >
2022-05-05 12:25:57 -07:00
< SitePreview soapbox = { soapbox } / >
2022-05-05 14:56:34 -07:00
2022-05-05 16:36:27 -07:00
< FormGroup
labelText = { < FormattedMessage id = 'soapbox_config.fields.logo_label' defaultMessage = 'Logo' / > }
hintText = { < FormattedMessage id = 'soapbox_config.hints.logo' defaultMessage = 'SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio' / > }
>
2022-05-16 16:33:44 -07:00
< FileInput
2022-05-05 16:36:27 -07:00
onChange = { handleFileChange ( [ 'logo' ] ) }
accept = 'image/svg,image/png'
/ >
< / FormGroup >
2022-05-05 16:46:16 -07:00
< CardHeader >
< CardTitle title = { < FormattedMessage id = 'soapbox_config.headings.theme' defaultMessage = 'Theme' / > } / >
< / CardHeader >
2022-05-05 16:36:27 -07:00
< List >
< ListItem label = { < FormattedMessage id = 'soapbox_config.fields.theme_label' defaultMessage = 'Default theme' / > } >
< ThemeSelector
value = { soapbox . defaultSettings . get ( 'themeMode' ) }
onChange = { handleThemeChange ( [ 'defaultSettings' , 'themeMode' ] ) }
/ >
< / ListItem >
< ListItem label = { < FormattedMessage id = 'soapbox_config.fields.brand_color_label' defaultMessage = 'Brand color' / > } >
2022-05-05 14:56:34 -07:00
< ColorWithPicker
2022-05-05 16:36:27 -07:00
buttonId = 'brandColor'
2022-05-05 14:56:34 -07:00
value = { soapbox . brandColor }
onChange = { handleColorChange ( [ 'brandColor' ] , ( color ) = > color . hex ) }
/ >
2022-05-05 16:36:27 -07:00
< / ListItem >
2022-05-05 16:19:53 -07:00
2022-05-05 16:36:27 -07:00
< ListItem label = { < FormattedMessage id = 'soapbox_config.fields.accent_color_label' defaultMessage = 'Accent color' / > } >
2022-05-05 14:56:34 -07:00
< ColorWithPicker
2022-05-05 16:36:27 -07:00
buttonId = 'accentColor'
2022-05-05 14:56:34 -07:00
value = { soapbox . accentColor }
onChange = { handleColorChange ( [ 'accentColor' ] , ( color ) = > color . hex ) }
/ >
2022-05-05 16:36:27 -07:00
< / ListItem >
< / List >
2022-05-05 14:35:30 -07:00
2022-05-05 16:46:16 -07:00
< CardHeader >
< CardTitle title = { < FormattedMessage id = 'soapbox_config.headings.options' defaultMessage = 'Options' / > } / >
< / CardHeader >
2022-05-05 14:35:30 -07:00
2022-05-05 16:30:25 -07:00
< List >
< ListItem label = { intl . formatMessage ( messages . verifiedCanEditNameLabel ) } >
< Toggle
checked = { soapbox . verifiedCanEditName === true }
onChange = { handleChange ( [ 'verifiedCanEditName' ] , ( e ) = > e . target . checked ) }
/ >
< / ListItem >
< ListItem label = { intl . formatMessage ( messages . displayFqnLabel ) } >
< Toggle
checked = { soapbox . displayFqn === true }
onChange = { handleChange ( [ 'displayFqn' ] , ( e ) = > e . target . checked ) }
/ >
< / ListItem >
< ListItem label = { intl . formatMessage ( messages . greentextLabel ) } >
< Toggle
checked = { soapbox . greentext === true }
onChange = { handleChange ( [ 'greentext' ] , ( e ) = > e . target . checked ) }
/ >
< / ListItem >
< ListItem
2022-05-05 12:25:57 -07:00
label = { intl . formatMessage ( messages . authenticatedProfileLabel ) }
hint = { intl . formatMessage ( messages . authenticatedProfileHint ) }
2022-05-05 16:30:25 -07:00
>
< Toggle
checked = { soapbox . authenticatedProfile === true }
onChange = { handleChange ( [ 'authenticatedProfile' ] , ( e ) = > e . target . checked ) }
/ >
< / ListItem >
< ListItem
2022-05-05 12:25:57 -07:00
label = { intl . formatMessage ( messages . singleUserModeLabel ) }
hint = { intl . formatMessage ( messages . singleUserModeHint ) }
2022-05-05 16:30:25 -07:00
>
< Toggle
checked = { soapbox . singleUserMode === true }
onChange = { handleChange ( [ 'singleUserMode' ] , ( e ) = > e . target . checked ) }
/ >
< / ListItem >
2022-05-05 12:25:57 -07:00
{ soapbox . get ( 'singleUserMode' ) && (
2022-05-05 16:30:25 -07:00
< ListItem label = { intl . formatMessage ( messages . singleUserModeProfileLabel ) } >
2022-05-05 14:35:30 -07:00
< Input
type = 'text'
placeholder = { intl . formatMessage ( messages . singleUserModeProfileHint ) }
value = { soapbox . singleUserModeProfile }
onChange = { handleChange ( [ 'singleUserModeProfile' ] , ( e ) = > e . target . value ) }
/ >
2022-05-05 16:30:25 -07:00
< / ListItem >
2022-05-05 12:25:57 -07:00
) }
2022-05-05 16:30:25 -07:00
< / List >
2022-05-05 13:52:25 -07:00
2022-05-05 16:46:16 -07:00
< CardHeader >
< CardTitle title = { < FormattedMessage id = 'soapbox_config.headings.navigation' defaultMessage = 'Navigation' / > } / >
< / CardHeader >
2022-05-05 13:52:25 -07:00
< Streamfield
label = { < FormattedMessage id = 'soapbox_config.fields.promo_panel_fields_label' defaultMessage = 'Promo panel items' / > }
hint = { < FormattedMessage id = 'soapbox_config.hints.promo_panel_fields' defaultMessage = 'You can have custom defined links displayed on the right panel of the timelines page.' / > }
component = { PromoPanelInput }
values = { soapbox . promoPanel . items . toArray ( ) }
onChange = { handleStreamItemChange ( [ 'promoPanel' , 'items' ] ) }
onAddItem = { addStreamItem ( [ 'promoPanel' , 'items' ] , templates . promoPanel ) }
onRemoveItem = { deleteStreamItem ( [ 'promoPanel' , 'items' ] ) }
/ >
2022-05-05 14:18:43 -07:00
< Streamfield
label = { < FormattedMessage id = 'soapbox_config.fields.home_footer_fields_label' defaultMessage = 'Home footer items' / > }
hint = { < FormattedMessage id = 'soapbox_config.hints.home_footer_fields' defaultMessage = 'You can have custom defined links displayed on the footer of your static pages' / > }
component = { FooterLinkInput }
values = { soapbox . navlinks . get ( 'homeFooter' ) ? . toArray ( ) || [ ] }
onChange = { handleStreamItemChange ( [ 'navlinks' , 'homeFooter' ] ) }
onAddItem = { addStreamItem ( [ 'navlinks' , 'homeFooter' ] , templates . footerItem ) }
onRemoveItem = { deleteStreamItem ( [ 'navlinks' , 'homeFooter' ] ) }
/ >
2022-05-05 13:06:52 -07:00
2022-05-05 16:46:16 -07:00
< FormGroup labelText = { intl . formatMessage ( messages . copyrightFooterLabel ) } >
< Input
type = 'text'
placeholder = { intl . formatMessage ( messages . copyrightFooterLabel ) }
value = { soapbox . copyright }
onChange = { handleChange ( [ 'copyright' ] , ( e ) = > e . target . value ) }
/ >
< / FormGroup >
< CardHeader >
< CardTitle title = { < FormattedMessage id = 'soapbox_config.headings.cryptocurrency' defaultMessage = 'Cryptocurrency' / > } / >
< / CardHeader >
2022-05-05 13:06:52 -07:00
< Streamfield
label = { < FormattedMessage id = 'soapbox_config.fields.crypto_addresses_label' defaultMessage = 'Cryptocurrency addresses' / > }
hint = { < FormattedMessage id = 'soapbox_config.hints.crypto_addresses' defaultMessage = 'Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.' / > }
component = { CryptoAddressInput }
values = { soapbox . cryptoAddresses . toArray ( ) }
2022-05-05 13:52:25 -07:00
onChange = { handleStreamItemChange ( [ 'cryptoAddresses' ] ) }
onAddItem = { addStreamItem ( [ 'cryptoAddresses' ] , templates . cryptoAddress ) }
onRemoveItem = { deleteStreamItem ( [ 'cryptoAddresses' ] ) }
2022-05-05 13:06:52 -07:00
/ >
2022-05-05 14:35:30 -07:00
< FormGroup labelText = { intl . formatMessage ( messages . cryptoDonatePanelLimitLabel ) } >
< Input
2022-05-05 12:25:57 -07:00
type = 'number'
min = { 0 }
pattern = '[0-9]+'
placeholder = { intl . formatMessage ( messages . cryptoDonatePanelLimitLabel ) }
value = { soapbox . cryptoDonatePanel . get ( 'limit' ) }
onChange = { handleChange ( [ 'cryptoDonatePanel' , 'limit' ] , ( e ) = > Number ( e . target . value ) ) }
/ >
2022-05-05 14:35:30 -07:00
< / FormGroup >
2022-05-05 16:46:16 -07:00
< CardHeader >
< CardTitle title = { < FormattedMessage id = 'soapbox_config.headings.advanced' defaultMessage = 'Advanced' / > } / >
< / CardHeader >
2022-05-05 12:25:57 -07:00
< Accordion
headline = { intl . formatMessage ( messages . rawJSONLabel ) }
expanded = { jsonEditorExpanded }
onToggle = { toggleJSONEditor }
>
2022-05-05 15:45:32 -07:00
< FormGroup hintText = { intl . formatMessage ( messages . rawJSONHint ) } >
< Textarea
2022-05-05 12:25:57 -07:00
value = { rawJSON }
onChange = { handleEditJSON }
2022-05-05 15:45:32 -07:00
hasError = { ! jsonValid }
isCodeEditor
2022-05-05 12:25:57 -07:00
rows = { 12 }
/ >
2022-05-05 15:45:32 -07:00
< / FormGroup >
2022-05-05 12:25:57 -07:00
< / Accordion >
< / fieldset >
2022-05-05 15:51:07 -07:00
2022-05-05 13:52:25 -07:00
< FormActions >
< Button type = 'submit' >
2022-05-05 12:25:57 -07:00
< FormattedMessage id = 'soapbox_config.save' defaultMessage = 'Save' / >
2022-05-05 13:52:25 -07:00
< / Button >
< / FormActions >
< / Form >
2022-05-05 12:25:57 -07:00
< / Column >
) ;
} ;
export default SoapboxConfig ;