diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 2871e2aae..bc3bdc82f 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -2,6 +2,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { getHost } from 'soapbox/actions/instance'; +import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import KVStore from 'soapbox/storage/kv_store'; import { getFeatures } from 'soapbox/utils/features'; @@ -33,79 +34,10 @@ const allowedEmojiRGI = ImmutableList([ '😩', ]); -const year = new Date().getFullYear(); - export const makeDefaultConfig = features => { return ImmutableMap({ - logo: '', - banner: '', - brandColor: '', // Empty - accentColor: '', - colors: ImmutableMap({ - gray: ImmutableMap({ - 50: '#f9fafb', - 100: '#f3f4f6', - 200: '#e5e7eb', - 300: '#d1d5db', - 400: '#9ca3af', - 500: '#6b7280', - 600: '#4b5563', - 700: '#374151', - 800: '#1f2937', - 900: '#111827', - }), - success: ImmutableMap({ - 50: '#f0fdf4', - 100: '#dcfce7', - 200: '#bbf7d0', - 300: '#86efac', - 400: '#4ade80', - 500: '#22c55e', - 600: '#16a34a', - 700: '#15803d', - 800: '#166534', - 900: '#14532d', - }), - danger: ImmutableMap({ - 50: '#fef2f2', - 100: '#fee2e2', - 200: '#fecaca', - 300: '#fca5a5', - 400: '#f87171', - 500: '#ef4444', - 600: '#dc2626', - 700: '#b91c1c', - 800: '#991b1b', - 900: '#7f1d1d', - }), - 'gradient-purple': '#b8a3f9', - 'gradient-blue': '#9bd5ff', - 'sea-blue': '#2feecc', - }), - customCss: ImmutableList(), - promoPanel: ImmutableMap({ - items: ImmutableList(), - }), - extensions: ImmutableMap(), - defaultSettings: ImmutableMap(), - copyright: `♥${year}. Copying is an act of love. Please copy and share.`, - navlinks: ImmutableMap({ - homeFooter: ImmutableList(), - }), allowedEmoji: features.emojiReactsRGI ? allowedEmojiRGI : allowedEmoji, - verifiedIcon: '', - verifiedCanEditName: false, displayFqn: Boolean(features.federating), - cryptoAddresses: ImmutableList(), - cryptoDonatePanel: ImmutableMap({ - limit: 1, - }), - aboutPages: ImmutableMap(), - betaPages: ImmutableMap(), - mobilePages: ImmutableMap(), - authenticatedProfile: true, - singleUserMode: false, - singleUserModeProfile: '', }); }; @@ -114,7 +46,7 @@ export const getSoapboxConfig = createSelector([ state => getFeatures(state.get('instance')), ], (soapbox, features) => { const defaultConfig = makeDefaultConfig(features); - return soapbox.mergeDeepWith((o, n) => o || n, defaultConfig); + return normalizeSoapboxConfig(soapbox).merge(defaultConfig); }); export function rememberSoapboxConfig(host) { diff --git a/app/soapbox/features/crypto_donate/components/site_wallet.tsx b/app/soapbox/features/crypto_donate/components/site_wallet.tsx index af5964796..f0b6accd7 100644 --- a/app/soapbox/features/crypto_donate/components/site_wallet.tsx +++ b/app/soapbox/features/crypto_donate/components/site_wallet.tsx @@ -24,7 +24,7 @@ interface ISiteWallet { const SiteWallet: React.FC = ({ limit }): JSX.Element => { const addresses: ImmutableList
= - useSoapboxConfig().get('cryptoAddresses').map(normalizeAddress); + useSoapboxConfig().cryptoAddresses.map(normalizeAddress); const coinList = typeof limit === 'number' ? addresses.take(limit) : addresses; diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 84d80eea3..16cff42f3 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -12,8 +12,8 @@ import { connect } from 'react-redux'; import { updateConfig } from 'soapbox/actions/admin'; import { uploadMedia } from 'soapbox/actions/media'; import snackbar from 'soapbox/actions/snackbar'; -import { makeDefaultConfig } from 'soapbox/actions/soapbox'; import Icon from 'soapbox/components/icon'; +import { Column } from 'soapbox/components/ui'; import { SimpleForm, FieldsGroup, @@ -26,10 +26,9 @@ import { } from 'soapbox/features/forms'; import ThemeToggle from 'soapbox/features/ui/components/theme_toggle'; import { isMobile } from 'soapbox/is_mobile'; -import { getFeatures } from 'soapbox/utils/features'; +import { normalizeSoapboxConfig } from 'soapbox/normalizers'; import Accordion from '../ui/components/accordion'; -import Column from '../ui/components/column'; import IconPickerDropdown from './components/icon_picker_dropdown'; import SitePreview from './components/site_preview'; @@ -71,11 +70,8 @@ const templates = { }; const mapStateToProps = state => { - const instance = state.get('instance'); - return { - soapbox: state.get('soapbox'), - features: getFeatures(instance), + initialData: state.soapbox, }; }; @@ -84,37 +80,36 @@ export default @connect(mapStateToProps) class SoapboxConfig extends ImmutablePureComponent { static propTypes = { - soapbox: ImmutablePropTypes.map.isRequired, - features: PropTypes.object.isRequired, + initialData: ImmutablePropTypes.map.isRequired, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; state = { isLoading: false, - soapbox: this.props.soapbox, + data: this.props.initialData, jsonEditorExpanded: false, rawJSON: JSON.stringify(this.props.soapbox, null, 2), jsonValid: true, } setConfig = (path, value) => { - const { soapbox } = this.state; - const config = soapbox.setIn(path, value); - this.setState({ soapbox: config, jsonValid: true }); + const { data } = this.state; + const newData = data.setIn(path, value); + this.setState({ data: newData, jsonValid: true }); }; - putConfig = config => { - this.setState({ soapbox: config, jsonValid: true }); + putConfig = data => { + this.setState({ data, jsonValid: true }); }; getParams = () => { - const { soapbox } = this.state; + const { data } = this.state; return [{ group: ':pleroma', key: ':frontend_configurations', value: [{ - tuple: [':soapbox_fe', soapbox.toJS()], + tuple: [':soapbox_fe', data.toJS()], }], }]; } @@ -158,8 +153,8 @@ class SoapboxConfig extends ImmutablePureComponent { handleDeleteItem = path => { return e => { - const soapbox = this.state.soapbox.deleteIn(path); - this.setState({ soapbox }); + const data = this.state.data.deleteIn(path); + this.setState({ data }); }; }; @@ -195,20 +190,18 @@ class SoapboxConfig extends ImmutablePureComponent { } getSoapboxConfig = () => { - const { features } = this.props; - const { soapbox } = this.state; - return makeDefaultConfig(features).mergeDeep(soapbox); + return normalizeSoapboxConfig(this.state.data); } toggleJSONEditor = (value) => this.setState({ jsonEditorExpanded: value }); componentDidUpdate(prevProps, prevState) { - if (prevProps.soapbox !== this.props.soapbox) { - this.putConfig(this.props.soapbox); + if (prevProps.initialData !== this.props.initialData) { + this.putConfig(this.props.initialData); } - if (prevState.soapbox !== this.state.soapbox) { - this.setState({ rawJSON: JSON.stringify(this.state.soapbox, null, 2) }); + if (prevState.data !== this.state.data) { + this.setState({ rawJSON: JSON.stringify(this.state.data, null, 2) }); } if (prevState.rawJSON !== this.state.rawJSON) { @@ -226,7 +219,7 @@ class SoapboxConfig extends ImmutablePureComponent { const soapbox = this.getSoapboxConfig(); return ( - +
diff --git a/app/soapbox/hooks/useSoapboxConfig.ts b/app/soapbox/hooks/useSoapboxConfig.ts index 2e51e5923..92a1ddc21 100644 --- a/app/soapbox/hooks/useSoapboxConfig.ts +++ b/app/soapbox/hooks/useSoapboxConfig.ts @@ -1,9 +1,9 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { useAppSelector } from 'soapbox/hooks'; -import type { Map as ImmutableMap } from 'immutable'; +import type { SoapboxConfig } from 'soapbox/types/soapbox'; /** Get the Soapbox config from the store */ -export const useSoapboxConfig = (): ImmutableMap => { +export const useSoapboxConfig = (): SoapboxConfig => { return useAppSelector((state) => getSoapboxConfig(state)); }; diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index c4a34d66c..613de5331 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -7,3 +7,5 @@ export { MentionRecord, normalizeMention } from './mention'; export { NotificationRecord, normalizeNotification } from './notification'; export { PollRecord, PollOptionRecord, normalizePoll } from './poll'; export { StatusRecord, normalizeStatus } from './status'; + +export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox_config'; diff --git a/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js new file mode 100644 index 000000000..f564fd17d --- /dev/null +++ b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js @@ -0,0 +1,11 @@ +import { Record as ImmutableRecord } from 'immutable'; + +import { normalizeSoapboxConfig } from '../soapbox_config'; + +describe('normalizeSoapboxConfig()', () => { + it('adds base fields', () => { + const result = normalizeSoapboxConfig({}); + expect(result.brandColor).toBe(''); + expect(ImmutableRecord.isRecord(result)).toBe(true); + }); +}); diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts new file mode 100644 index 000000000..0696eb314 --- /dev/null +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -0,0 +1,103 @@ +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, + fromJS, +} from 'immutable'; + +const DEFAULT_COLORS = ImmutableMap({ + gray: ImmutableMap({ + 50: '#f9fafb', + 100: '#f3f4f6', + 200: '#e5e7eb', + 300: '#d1d5db', + 400: '#9ca3af', + 500: '#6b7280', + 600: '#4b5563', + 700: '#374151', + 800: '#1f2937', + 900: '#111827', + }), + success: ImmutableMap({ + 50: '#f0fdf4', + 100: '#dcfce7', + 200: '#bbf7d0', + 300: '#86efac', + 400: '#4ade80', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + 800: '#166534', + 900: '#14532d', + }), + danger: ImmutableMap({ + 50: '#fef2f2', + 100: '#fee2e2', + 200: '#fecaca', + 300: '#fca5a5', + 400: '#f87171', + 500: '#ef4444', + 600: '#dc2626', + 700: '#b91c1c', + 800: '#991b1b', + 900: '#7f1d1d', + }), + 'gradient-purple': '#b8a3f9', + 'gradient-blue': '#9bd5ff', + 'sea-blue': '#2feecc', +}); + +export const SoapboxConfigRecord = ImmutableRecord({ + logo: '', + banner: '', + brandColor: '', // Empty + accentColor: '', + colors: ImmutableMap(), + copyright: `♥${new Date().getFullYear()}. Copying is an act of love. Please copy and share.`, + customCss: ImmutableList(), + defaultSettings: ImmutableMap(), + extensions: ImmutableMap(), + greentext: false, + promoPanel: ImmutableMap({ + items: ImmutableList(), + }), + navlinks: ImmutableMap({ + homeFooter: ImmutableList(), + }), + allowedEmoji: ImmutableList([ + '👍', + '❤️', + '😆', + '😮', + '😢', + '😩', + ]), + verifiedIcon: '', + verifiedCanEditName: false, + displayFqn: true, + cryptoAddresses: ImmutableList>(), + cryptoDonatePanel: ImmutableMap({ + limit: 1, + }), + aboutPages: ImmutableMap(), + betaPages: ImmutableMap(), + mobilePages: ImmutableMap(), + authenticatedProfile: true, + singleUserMode: false, + singleUserModeProfile: '', +}, 'SoapboxConfig'); + +type SoapboxConfigMap = ImmutableMap; + +const normalizeColors = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const colors = DEFAULT_COLORS.mergeDeep(soapboxConfig.get('colors')); + return soapboxConfig.set('colors', colors); +}; + +export const normalizeSoapboxConfig = (soapboxConfig: Record) => { + return SoapboxConfigRecord( + ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => { + normalizeColors(soapboxConfig); + }), + ); +}; diff --git a/app/soapbox/types/soapbox.ts b/app/soapbox/types/soapbox.ts new file mode 100644 index 000000000..865a6567c --- /dev/null +++ b/app/soapbox/types/soapbox.ts @@ -0,0 +1,5 @@ +import { SoapboxConfigRecord } from 'soapbox/normalizers'; + +type SoapboxConfig = ReturnType; + +export { SoapboxConfig };