diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx
index b9603b3b1..d6c17db5f 100644
--- a/app/soapbox/containers/soapbox.tsx
+++ b/app/soapbox/containers/soapbox.tsx
@@ -96,7 +96,7 @@ const SoapboxMount = () => {
const waitlisted = account && !account.source.get('approved', true);
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
const showOnboarding = account && !waitlisted && needsOnboarding;
- const singleUserMode = soapboxConfig.singleUserMode && soapboxConfig.singleUserModeProfile;
+ const { redirectRootNoLogin } = soapboxConfig;
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
@@ -134,8 +134,8 @@ const SoapboxMount = () => {
/>
)}
- {!me && (singleUserMode
- ?
+ {!me && (redirectRootNoLogin
+ ?
: )}
{!me && (
diff --git a/app/soapbox/features/soapbox-config/index.tsx b/app/soapbox/features/soapbox-config/index.tsx
index 8484863b6..761c34d59 100644
--- a/app/soapbox/features/soapbox-config/index.tsx
+++ b/app/soapbox/features/soapbox-config/index.tsx
@@ -47,10 +47,6 @@ const messages = defineMessages({
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.' },
displayCtaLabel: { id: 'soapbox_config.cta_label', defaultMessage: 'Display call to action panels if not authenticated' },
- 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' },
mediaPreviewLabel: { id: 'soapbox_config.media_preview_label', defaultMessage: 'Prefer preview media for thumbnails' },
mediaPreviewHint: { id: 'soapbox_config.media_preview_hint', defaultMessage: 'Some backends provide an optimized version of media for display in timelines. However, these preview images may be too small without additional configuration.' },
feedInjectionLabel: { id: 'soapbox_config.feed_injection_label', defaultMessage: 'Feed injection' },
@@ -283,27 +279,6 @@ const SoapboxConfig: React.FC = () => {
/>
-
- e.target.checked)}
- />
-
-
- {soapbox.get('singleUserMode') && (
-
- e.target.value)}
- />
-
- )}
-
', () => {
});
});
- describe('with singleUserMode enabled', () => {
+ describe('with registrations closed', () => {
it('renders empty', () => {
- const store = { soapbox: ImmutableMap({ singleUserMode: true }) };
+ const store = { instance: normalizeInstance({ registrations: false }) };
render(, undefined, store);
expect(screen.queryAllByTestId('cta-banner')).toHaveLength(0);
diff --git a/app/soapbox/features/ui/components/cta-banner.tsx b/app/soapbox/features/ui/components/cta-banner.tsx
index 87860b318..5e06b207c 100644
--- a/app/soapbox/features/ui/components/cta-banner.tsx
+++ b/app/soapbox/features/ui/components/cta-banner.tsx
@@ -6,10 +6,10 @@ import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const CtaBanner = () => {
const instance = useInstance();
- const { displayCta, singleUserMode } = useSoapboxConfig();
+ const { displayCta } = useSoapboxConfig();
const me = useAppSelector((state) => state.me);
- if (me || !displayCta || singleUserMode) return null;
+ if (me || !displayCta || !instance.registrations) return null;
return (
diff --git a/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx b/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx
index 9a7906e87..809abcbda 100644
--- a/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx
+++ b/app/soapbox/features/ui/components/modals/unauthorized-modal.tsx
@@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import { remoteInteraction } from 'soapbox/actions/interactions';
import { Button, Modal, Stack, Text } from 'soapbox/components/ui';
-import { useAppSelector, useAppDispatch, useFeatures, useSoapboxConfig, useInstance } from 'soapbox/hooks';
+import { useAppSelector, useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks';
import toast from 'soapbox/toast';
const messages = defineMessages({
@@ -31,7 +31,6 @@ const UnauthorizedModal: React.FC = ({ action, onClose, acco
const dispatch = useAppDispatch();
const instance = useInstance();
- const { singleUserMode } = useSoapboxConfig();
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
const features = useFeatures();
@@ -98,7 +97,7 @@ const UnauthorizedModal: React.FC = ({ action, onClose, acco
}
secondaryAction={onRegister}
secondaryText={}
@@ -122,7 +121,7 @@ const UnauthorizedModal: React.FC = ({ action, onClose, acco
- {!singleUserMode && (
+ {instance.registrations && (
diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx
index d9829afca..bbb357bfa 100644
--- a/app/soapbox/features/ui/components/navbar.tsx
+++ b/app/soapbox/features/ui/components/navbar.tsx
@@ -9,7 +9,7 @@ import { openSidebar } from 'soapbox/actions/sidebar';
import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search';
-import { useAppDispatch, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
+import { useAppDispatch, useInstance, useOwnAccount } from 'soapbox/hooks';
import ProfileDropdown from './profile-dropdown';
@@ -29,8 +29,7 @@ const Navbar = () => {
const node = useRef(null);
const account = useOwnAccount();
- const soapboxConfig = useSoapboxConfig();
- const singleUserMode = soapboxConfig.get('singleUserMode');
+ const instance = useInstance();
const [isLoading, setLoading] = useState(false);
const [username, setUsername] = useState('');
@@ -151,7 +150,7 @@ const Navbar = () => {
- {!singleUserMode && (
+ {!instance.registrations && (
diff --git a/app/soapbox/features/ui/components/panels/sign-up-panel.tsx b/app/soapbox/features/ui/components/panels/sign-up-panel.tsx
index 318080770..117a8a8b9 100644
--- a/app/soapbox/features/ui/components/panels/sign-up-panel.tsx
+++ b/app/soapbox/features/ui/components/panels/sign-up-panel.tsx
@@ -2,14 +2,13 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, Stack, Text } from 'soapbox/components/ui';
-import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
+import { useAppSelector, useInstance } from 'soapbox/hooks';
const SignUpPanel = () => {
const instance = useInstance();
- const { singleUserMode } = useSoapboxConfig();
const me = useAppSelector((state) => state.me);
- if (me || singleUserMode) return null;
+ if (me || !instance.registrations) return null;
return (
diff --git a/app/soapbox/normalizers/soapbox/__tests__/soapbox-config.test.ts b/app/soapbox/normalizers/soapbox/__tests__/soapbox-config.test.ts
index 9b70eba36..5eccab870 100644
--- a/app/soapbox/normalizers/soapbox/__tests__/soapbox-config.test.ts
+++ b/app/soapbox/normalizers/soapbox/__tests__/soapbox-config.test.ts
@@ -34,4 +34,15 @@ describe('normalizeSoapboxConfig()', () => {
expect(ImmutableRecord.isRecord(result.promoPanel.items.get(0))).toBe(true);
expect(result.promoPanel.items.get(2)?.icon).toBe('question-circle');
});
+
+ it('upgrades singleUserModeProfile to redirectRootNoLogin', () => {
+ expect(normalizeSoapboxConfig({ singleUserMode: true, singleUserModeProfile: 'alex' }).redirectRootNoLogin).toBe('/@alex');
+ expect(normalizeSoapboxConfig({ singleUserMode: false, singleUserModeProfile: 'alex' }).redirectRootNoLogin).toBe('');
+ });
+
+ it('normalizes redirectRootNoLogin', () => {
+ expect(normalizeSoapboxConfig({ redirectRootNoLogin: 'benis' }).redirectRootNoLogin).toBe('/benis');
+ expect(normalizeSoapboxConfig({ redirectRootNoLogin: '/benis' }).redirectRootNoLogin).toBe('/benis');
+ expect(normalizeSoapboxConfig({ redirectRootNoLogin: '/' }).redirectRootNoLogin).toBe('');
+ });
});
diff --git a/app/soapbox/normalizers/soapbox/soapbox-config.ts b/app/soapbox/normalizers/soapbox/soapbox-config.ts
index e899e1e80..f2366bdf1 100644
--- a/app/soapbox/normalizers/soapbox/soapbox-config.ts
+++ b/app/soapbox/normalizers/soapbox/soapbox-config.ts
@@ -106,8 +106,6 @@ export const SoapboxConfigRecord = ImmutableRecord({
}),
aboutPages: ImmutableMap>(),
authenticatedProfile: true,
- singleUserMode: false,
- singleUserModeProfile: '',
linkFooterMessage: '',
links: ImmutableMap(),
displayCta: true,
@@ -115,7 +113,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
feedInjection: true,
tileServer: '',
tileServerAttribution: '',
- redirectRootNoLogin: '/',
+ redirectRootNoLogin: '',
/**
* Whether to use the preview URL for media thumbnails.
* On some platforms this can be too blurry without additional configuration.
@@ -198,6 +196,45 @@ const normalizeAdsAlgorithm = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMa
}
};
+/** Single user mode is now managed by `redirectRootNoLogin`. */
+const upgradeSingleUserMode = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
+ const singleUserMode = soapboxConfig.get('singleUserMode');
+ const singleUserModeProfile = soapboxConfig.get('singleUserModeProfile');
+ const redirectRootNoLogin = soapboxConfig.get('redirectRootNoLogin');
+
+ if (!redirectRootNoLogin && singleUserMode && singleUserModeProfile) {
+ return soapboxConfig
+ .set('redirectRootNoLogin', `/@${singleUserModeProfile}`)
+ .deleteAll(['singleUserMode', 'singleUserModeProfile']);
+ } else {
+ return soapboxConfig
+ .deleteAll(['singleUserMode', 'singleUserModeProfile']);
+ }
+};
+
+/** Ensure a valid path is used. */
+const normalizeRedirectRootNoLogin = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
+ const redirectRootNoLogin = soapboxConfig.get('redirectRootNoLogin');
+
+ if (!redirectRootNoLogin) return soapboxConfig;
+
+ try {
+ // Basically just get the pathname with a leading slash.
+ const normalized = new URL(redirectRootNoLogin, 'a://').pathname;
+
+ if (normalized !== '/') {
+ return soapboxConfig.set('redirectRootNoLogin', normalized);
+ } else {
+ // Prevent infinite redirect(?)
+ return soapboxConfig.delete('redirectRootNoLogin');
+ }
+ } catch (e) {
+ console.error('You have configured an invalid redirect in Soapbox Config.');
+ console.error(e);
+ return soapboxConfig.delete('redirectRootNoLogin');
+ }
+};
+
export const normalizeSoapboxConfig = (soapboxConfig: Record) => {
return SoapboxConfigRecord(
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
@@ -210,6 +247,8 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record) => {
normalizeCryptoAddresses(soapboxConfig);
normalizeAds(soapboxConfig);
normalizeAdsAlgorithm(soapboxConfig);
+ upgradeSingleUserMode(soapboxConfig);
+ normalizeRedirectRootNoLogin(soapboxConfig);
}),
);
};