diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 2871e2aae5..bc3bdc82fd 100644 Binary files a/app/soapbox/actions/soapbox.js and b/app/soapbox/actions/soapbox.js differ diff --git a/app/soapbox/components/media_gallery.js b/app/soapbox/components/media_gallery.js index df1e4db42b..2164031a89 100644 Binary files a/app/soapbox/components/media_gallery.js and b/app/soapbox/components/media_gallery.js differ diff --git a/app/soapbox/features/chats/index.js b/app/soapbox/features/chats/index.js index d7e1036bcf..ed8c35e605 100644 Binary files a/app/soapbox/features/chats/index.js and b/app/soapbox/features/chats/index.js differ diff --git a/app/soapbox/features/crypto_donate/components/site_wallet.tsx b/app/soapbox/features/crypto_donate/components/site_wallet.tsx index af59647967..1e09292bd3 100644 --- a/app/soapbox/features/crypto_donate/components/site_wallet.tsx +++ b/app/soapbox/features/crypto_donate/components/site_wallet.tsx @@ -1,4 +1,3 @@ -import { trimStart } from 'lodash'; import React from 'react'; import { Stack } from 'soapbox/components/ui'; @@ -6,36 +5,22 @@ import { useSoapboxConfig } from 'soapbox/hooks'; import CryptoAddress from './crypto_address'; -import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -type Address = ImmutableMap; - -// Address example: -// {"ticker": "btc", "address": "bc1q9cx35adpm73aq2fw40ye6ts8hfxqzjr5unwg0n", "note": "This is our main address"} -const normalizeAddress = (address: Address): Address => { - return address.update('ticker', '', ticker => { - return trimStart(ticker, '$').toLowerCase(); - }); -}; - interface ISiteWallet { limit?: number, } const SiteWallet: React.FC = ({ limit }): JSX.Element => { - const addresses: ImmutableList
= - useSoapboxConfig().get('cryptoAddresses').map(normalizeAddress); - - const coinList = typeof limit === 'number' ? addresses.take(limit) : addresses; + const { cryptoAddresses } = useSoapboxConfig(); + const addresses = typeof limit === 'number' ? cryptoAddresses.take(limit) : cryptoAddresses; return ( - {coinList.map(coin => ( + {addresses.map(address => ( ))} diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 84d80eea35..16cff42f3c 100644 Binary files a/app/soapbox/features/soapbox_config/index.js and b/app/soapbox/features/soapbox_config/index.js differ diff --git a/app/soapbox/hooks/useSoapboxConfig.ts b/app/soapbox/hooks/useSoapboxConfig.ts index 2e51e59233..92a1ddc212 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 c4a34d66c2..613de53313 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 0000000000..f0a021586c Binary files /dev/null and b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js differ diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts new file mode 100644 index 0000000000..3d0658851e --- /dev/null +++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts @@ -0,0 +1,171 @@ +import { + Map as ImmutableMap, + List as ImmutableList, + Record as ImmutableRecord, + fromJS, +} from 'immutable'; +import { trimStart } from 'lodash'; + +import { toTailwind } from 'soapbox/utils/tailwind'; +import { generateAccent } from 'soapbox/utils/theme'; + +import type { + PromoPanelItem, + FooterItem, + CryptoAddress, +} from 'soapbox/types/soapbox'; + +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 PromoPanelItemRecord = ImmutableRecord({ + icon: '', + text: '', + url: '', +}); + +export const FooterItemRecord = ImmutableRecord({ + title: '', + url: '', +}); + +export const CryptoAddressRecord = ImmutableRecord({ + address: '', + note: '', + ticker: '', +}); + +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 normalizeCryptoAddress = (address: unknown): CryptoAddress => { + return CryptoAddressRecord(ImmutableMap(fromJS(address))).update('ticker', ticker => { + return trimStart(ticker, '$').toLowerCase(); + }); +}; + +const normalizeCryptoAddresses = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const addresses = ImmutableList(soapboxConfig.get('cryptoAddresses')); + return soapboxConfig.set('cryptoAddresses', addresses.map(normalizeCryptoAddress)); +}; + +const normalizeBrandColor = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const brandColor = soapboxConfig.get('brandColor') || soapboxConfig.getIn(['colors', 'primary', '500']) || ''; + return soapboxConfig.set('brandColor', brandColor); +}; + +const normalizeAccentColor = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const brandColor = soapboxConfig.get('brandColor'); + + const accentColor = soapboxConfig.get('accentColor') + || soapboxConfig.getIn(['colors', 'accent', '500']) + || (brandColor ? generateAccent(brandColor) : ''); + + return soapboxConfig.set('accentColor', accentColor); +}; + +const normalizeColors = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const colors = DEFAULT_COLORS.mergeDeep(soapboxConfig.get('colors')); + return toTailwind(soapboxConfig.set('colors', colors)); +}; + +const maybeAddMissingColors = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => { + const colors = soapboxConfig.get('colors'); + + const missing = { + 'bg-shape-1': colors.getIn(['accent', '50']), + 'bg-shape-2': colors.getIn(['primary', '500']), + }; + + return soapboxConfig.set('colors', colors.mergeDeep(missing)); +}; + +export const normalizeSoapboxConfig = (soapboxConfig: Record) => { + return SoapboxConfigRecord( + ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => { + normalizeBrandColor(soapboxConfig); + normalizeAccentColor(soapboxConfig); + normalizeColors(soapboxConfig); + maybeAddMissingColors(soapboxConfig); + normalizeCryptoAddresses(soapboxConfig); + }), + ); +}; diff --git a/app/soapbox/types/soapbox.ts b/app/soapbox/types/soapbox.ts new file mode 100644 index 0000000000..386734d3b5 --- /dev/null +++ b/app/soapbox/types/soapbox.ts @@ -0,0 +1,18 @@ +import { + PromoPanelItemRecord, + FooterItemRecord, + CryptoAddressRecord, + SoapboxConfigRecord, +} from 'soapbox/normalizers/soapbox/soapbox_config'; + +type PromoPanelItem = ReturnType; +type FooterItem = ReturnType; +type CryptoAddress = ReturnType; +type SoapboxConfig = ReturnType; + +export { + PromoPanelItem, + FooterItem, + CryptoAddress, + SoapboxConfig, +}; diff --git a/app/soapbox/utils/__tests__/colors-test.js b/app/soapbox/utils/__tests__/colors-test.js index 3e444620ad..8677cb1c6e 100644 Binary files a/app/soapbox/utils/__tests__/colors-test.js and b/app/soapbox/utils/__tests__/colors-test.js differ diff --git a/app/soapbox/utils/__tests__/tailwind-test.js b/app/soapbox/utils/__tests__/tailwind-test.js index 93f928b73a..822408393c 100644 Binary files a/app/soapbox/utils/__tests__/tailwind-test.js and b/app/soapbox/utils/__tests__/tailwind-test.js differ diff --git a/app/soapbox/utils/colors.ts b/app/soapbox/utils/colors.ts index c20c988306..796b2123ac 100644 --- a/app/soapbox/utils/colors.ts +++ b/app/soapbox/utils/colors.ts @@ -103,8 +103,8 @@ export default function(baseColor: string): TailwindColorObject { 50: 0.95, 100: 0.9, 200: 0.75, - 300: 0.6, - 400: 0.3, + 300: 0.3, + 400: 0.2, 600: 0.9, 700: 0.75, 800: 0.6, diff --git a/app/soapbox/utils/theme.ts b/app/soapbox/utils/theme.ts index bb83b01119..ccf523b8be 100644 --- a/app/soapbox/utils/theme.ts +++ b/app/soapbox/utils/theme.ts @@ -1,8 +1,7 @@ import { hexToRgb } from './colors'; -import { toTailwind } from './tailwind'; -import type { Map as ImmutableMap } from 'immutable'; import type { Rgb, Hsl, TailwindColorPalette, TailwindColorObject } from 'soapbox/types/colors'; +import type { SoapboxConfig } from 'soapbox/types/soapbox'; // Taken from chromatism.js // https://github.com/graypegg/chromatism/blob/master/src/conversions/rgb.js @@ -69,16 +68,19 @@ export const generateAccent = (brandColor: string): string | null => { return hslToHex({ h: h - 15, s: 86, l: 44 }); }; -const parseShades = (obj: Record, color: string, shades: Record) => { +const parseShades = (obj: Record, color: string, shades: Record): void => { + if (!shades) return; + if (typeof shades === 'string') { const rgb = hexToRgb(shades); - if (!rgb) return obj; + if (!rgb) return; const { r, g, b } = rgb; - return obj[`--color-${color}`] = `${r} ${g} ${b}`; + obj[`--color-${color}`] = `${r} ${g} ${b}`; + return; } - return Object.keys(shades).forEach(shade => { + Object.keys(shades).forEach(shade => { const rgb = hexToRgb(shades[shade]); if (!rgb) return; @@ -102,6 +104,6 @@ export const colorsToCss = (colors: TailwindColorPalette): string => { }, ''); }; -export const generateThemeCss = (soapboxConfig: ImmutableMap): string => { - return colorsToCss(toTailwind(soapboxConfig).get('colors').toJS() as TailwindColorPalette); +export const generateThemeCss = (soapboxConfig: SoapboxConfig): string => { + return colorsToCss(soapboxConfig.colors.toJS() as TailwindColorPalette); };