diff --git a/app/soapbox/__fixtures__/spinster-soapbox.json b/app/soapbox/__fixtures__/spinster-soapbox.json
new file mode 100644
index 000000000..8f3ed63fa
--- /dev/null
+++ b/app/soapbox/__fixtures__/spinster-soapbox.json
@@ -0,0 +1,119 @@
+{
+ "allowedEmoji": [
+ "👍",
+ "❤️",
+ "😆",
+ "😮",
+ "😢",
+ "😡",
+ "😩"
+ ],
+ "brandColor": "#990099",
+ "copyright": "♡2021. Copying is an act of love. Please copy and share.",
+ "cryptoAddresses": [
+ {
+ "address": "bc1qv7lk3algpfg4zpyuhvxfm0uza9ck4parz3y3l5",
+ "note": "",
+ "ticker": "btc"
+ },
+ {
+ "address": "0xadc66B63bFee7677CD27CFb81b16a8860f1A1226",
+ "note": "",
+ "ticker": "eth"
+ },
+ {
+ "address": "DSf7UmRf7DGGsjh4QYhzQaqtjJMTXZ8k79",
+ "note": "",
+ "ticker": "doge"
+ },
+ {
+ "address": "ltc1q642pnkuvw0gpuuvddw6vafvl9hhp3efyl9mnqz",
+ "note": "",
+ "ticker": "ltc"
+ },
+ {
+ "address": "t1faHDsoa4bd3pGaLjaU7DiuUtBPzbnEEse",
+ "note": "",
+ "ticker": "zec"
+ },
+ {
+ "address": "XchTLkcSMsDoZGESwr4tqtxSU5dideAZVQ",
+ "note": "",
+ "ticker": "dash"
+ },
+ {
+ "address": "bitcoincash:qp8f80z27294phmhdk55yf05p3f0tkxl4v9r2aavw5",
+ "note": "",
+ "ticker": "bch"
+ }
+ ],
+ "cryptoDonatePanel": {
+ "limit": 1
+ },
+ "customCss": [
+ "/instance/spinster.css"
+ ],
+ "defaultSettings": {
+ "autoPlayGif": false,
+ "themeMode": "light"
+ },
+ "extensions": {
+ "patron": {
+ "enabled": true
+ }
+ },
+ "logo": "https://spinster.xyz/instance/images/spinster-logo.svg",
+ "navlinks": {
+ "homeFooter": [
+ {
+ "title": "About",
+ "url": "/about"
+ },
+ {
+ "title": "Terms of Service",
+ "url": "/about/tos"
+ },
+ {
+ "title": "Privacy Policy",
+ "url": "/about/privacy"
+ },
+ {
+ "title": "DMCA",
+ "url": "/about/dmca"
+ },
+ {
+ "title": "Source Code",
+ "url": "/about#opensource"
+ }
+ ]
+ },
+ "promoPanel": {
+ "items": [
+ {
+ "icon": "shopping-basket",
+ "text": "Buy Spinster Merch",
+ "url": "https://shop.4w.pub/collections/spinster"
+ },
+ {
+ "icon": "eye-slash",
+ "text": "Privacy Guide",
+ "url": "https://4w.pub/your-guide-to-spinster-privacy-options/"
+ },
+ {
+ "icon": "question-circle",
+ "text": "Spinster FAQs",
+ "url": "https://spinster.xyz/about#faqs"
+ },
+ {
+ "icon": "bug",
+ "text": "Report a Bug",
+ "url": "https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/"
+ },
+ {
+ "icon": "fediverse",
+ "text": "About the Fediverse",
+ "url": "https://jointhefedi.com/"
+ }
+ ]
+ }
+}
diff --git a/app/soapbox/features/ui/components/promo_panel.js b/app/soapbox/features/ui/components/promo_panel.js
deleted file mode 100644
index 1d152be56..000000000
--- a/app/soapbox/features/ui/components/promo_panel.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import ImmutablePropTypes from 'react-immutable-proptypes';
-import { connect } from 'react-redux';
-
-import { getSettings } from 'soapbox/actions/settings';
-import { getSoapboxConfig } from 'soapbox/actions/soapbox';
-import Icon from 'soapbox/components/icon';
-
-const mapStateToProps = state => ({
- promoItems: getSoapboxConfig(state).getIn(['promoPanel', 'items']),
- locale: getSettings(state).get('locale'),
-});
-
-export default @connect(mapStateToProps)
-class PromoPanel extends React.PureComponent {
-
- static propTypes = {
- locale: PropTypes.string,
- promoItems: ImmutablePropTypes.list,
- }
-
- render() {
- const { locale, promoItems } = this.props;
- if (!promoItems || promoItems.isEmpty()) return null;
-
- return (
-
- );
- }
-
-}
diff --git a/app/soapbox/features/ui/components/promo_panel.tsx b/app/soapbox/features/ui/components/promo_panel.tsx
new file mode 100644
index 000000000..5a0b9a24d
--- /dev/null
+++ b/app/soapbox/features/ui/components/promo_panel.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import Icon from 'soapbox/components/icon';
+import { Widget, Stack, Text } from 'soapbox/components/ui';
+import { useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks';
+
+const PromoPanel: React.FC = () => {
+ const { promoPanel } = useSoapboxConfig();
+ const settings = useSettings();
+
+ const siteTitle = useAppSelector(state => state.instance.title);
+ const promoItems = promoPanel.get('items');
+ const locale = settings.get('locale');
+
+ if (!promoItems || promoItems.isEmpty()) return null;
+
+ return (
+
+
+ {promoItems.map((item, i) => (
+
+
+
+ {item.textLocales.get(locale) || item.text}
+
+
+ ))}
+
+
+ );
+};
+
+export default PromoPanel;
diff --git a/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js
index f0a021586..65c4291f8 100644
--- a/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js
+++ b/app/soapbox/normalizers/soapbox/__tests__/soapbox_config-test.js
@@ -27,4 +27,11 @@ describe('normalizeSoapboxConfig()', () => {
expect(ImmutableRecord.isRecord(result.cryptoAddresses.get(0))).toBe(true);
expect(result.toJS()).toMatchObject(expected);
});
+
+ it('normalizes promoPanel', () => {
+ const result = normalizeSoapboxConfig(require('soapbox/__fixtures__/spinster-soapbox.json'));
+ expect(ImmutableRecord.isRecord(result.promoPanel)).toBe(true);
+ expect(ImmutableRecord.isRecord(result.promoPanel.items.get(0))).toBe(true);
+ expect(result.promoPanel.items.get(2).icon).toBe('question-circle');
+ });
});
diff --git a/app/soapbox/normalizers/soapbox/soapbox_config.ts b/app/soapbox/normalizers/soapbox/soapbox_config.ts
index f2e8cfef8..e0f2cb4b3 100644
--- a/app/soapbox/normalizers/soapbox/soapbox_config.ts
+++ b/app/soapbox/normalizers/soapbox/soapbox_config.ts
@@ -61,6 +61,11 @@ export const PromoPanelItemRecord = ImmutableRecord({
icon: '',
text: '',
url: '',
+ textLocales: ImmutableMap(),
+});
+
+export const PromoPanelRecord = ImmutableRecord({
+ items: ImmutableList(),
});
export const FooterItemRecord = ImmutableRecord({
@@ -86,9 +91,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
defaultSettings: ImmutableMap(),
extensions: ImmutableMap(),
greentext: false,
- promoPanel: ImmutableMap({
- items: ImmutableList(),
- }),
+ promoPanel: PromoPanelRecord(),
navlinks: ImmutableMap({
homeFooter: ImmutableList(),
}),
@@ -160,12 +163,19 @@ const maybeAddMissingColors = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMa
return soapboxConfig.set('colors', missing.mergeDeep(colors));
};
+const normalizePromoPanel = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
+ const promoPanel = PromoPanelRecord(soapboxConfig.get('promoPanel'));
+ const items = promoPanel.items.map(PromoPanelItemRecord);
+ return soapboxConfig.set('promoPanel', promoPanel.set('items', items));
+};
+
export const normalizeSoapboxConfig = (soapboxConfig: Record) => {
return SoapboxConfigRecord(
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
normalizeBrandColor(soapboxConfig);
normalizeAccentColor(soapboxConfig);
normalizeColors(soapboxConfig);
+ normalizePromoPanel(soapboxConfig);
maybeAddMissingColors(soapboxConfig);
normalizeCryptoAddresses(soapboxConfig);
}),
diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js
index e9b6630aa..fcff5b1e3 100644
--- a/app/soapbox/pages/home_page.js
+++ b/app/soapbox/pages/home_page.js
@@ -10,6 +10,7 @@ import {
WhoToFollowPanel,
TrendsPanel,
SignUpPanel,
+ PromoPanel,
CryptoDonatePanel,
BirthdayPanel,
} from 'soapbox/features/ui/util/async-components';
@@ -94,6 +95,9 @@ class HomePage extends ImmutablePureComponent {
{Component => }
)}
+
+ {Component => }
+
{features.birthdays && (
{Component => }