Remove singleUserMode, upgrade to redirectRootNoLogin

environments/review-redirect-r-ks3942/deployments/2273
Alex Gleason 2 years ago
parent 1e07c03479
commit 6f0e398a78
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -96,7 +96,7 @@ const SoapboxMount = () => {
const waitlisted = account && !account.source.get('approved', true); const waitlisted = account && !account.source.get('approved', true);
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
const showOnboarding = account && !waitlisted && needsOnboarding; const showOnboarding = account && !waitlisted && needsOnboarding;
const singleUserMode = soapboxConfig.singleUserMode && soapboxConfig.singleUserModeProfile; const { redirectRootNoLogin } = soapboxConfig;
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true; const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
@ -134,8 +134,8 @@ const SoapboxMount = () => {
/> />
)} )}
{!me && (singleUserMode {!me && (redirectRootNoLogin
? <Redirect exact from='/' to={`/${singleUserMode}`} /> ? <Redirect exact from='/' to={redirectRootNoLogin} />
: <Route exact path='/' component={PublicLayout} />)} : <Route exact path='/' component={PublicLayout} />)}
{!me && ( {!me && (

@ -47,10 +47,6 @@ const messages = defineMessages({
authenticatedProfileLabel: { id: 'soapbox_config.authenticated_profile_label', defaultMessage: 'Profiles require authentication' }, 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.' }, 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' }, 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' }, 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.' }, 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' }, feedInjectionLabel: { id: 'soapbox_config.feed_injection_label', defaultMessage: 'Feed injection' },
@ -283,27 +279,6 @@ const SoapboxConfig: React.FC = () => {
/> />
</ListItem> </ListItem>
<ListItem
label={intl.formatMessage(messages.singleUserModeLabel)}
hint={intl.formatMessage(messages.singleUserModeHint)}
>
<Toggle
checked={soapbox.singleUserMode === true}
onChange={handleChange(['singleUserMode'], (e) => e.target.checked)}
/>
</ListItem>
{soapbox.get('singleUserMode') && (
<ListItem label={intl.formatMessage(messages.singleUserModeProfileLabel)}>
<Input
type='text'
placeholder={intl.formatMessage(messages.singleUserModeProfileHint)}
value={soapbox.singleUserModeProfile}
onChange={handleChange(['singleUserModeProfile'], (e) => e.target.value)}
/>
</ListItem>
)}
<ListItem <ListItem
label={intl.formatMessage(messages.redirectRootNoLoginLabel)} label={intl.formatMessage(messages.redirectRootNoLoginLabel)}
hint={intl.formatMessage(messages.redirectRootNoLoginHint)} hint={intl.formatMessage(messages.redirectRootNoLoginHint)}

@ -1,6 +1,7 @@
import { Map as ImmutableMap } from 'immutable';
import React from 'react'; import React from 'react';
import { normalizeInstance } from 'soapbox/normalizers';
import { render, screen } from '../../../../jest/test-helpers'; import { render, screen } from '../../../../jest/test-helpers';
import CtaBanner from '../cta-banner'; import CtaBanner from '../cta-banner';
@ -19,9 +20,9 @@ describe('<CtaBanner />', () => {
}); });
}); });
describe('with singleUserMode enabled', () => { describe('with registrations closed', () => {
it('renders empty', () => { it('renders empty', () => {
const store = { soapbox: ImmutableMap({ singleUserMode: true }) }; const store = { instance: normalizeInstance({ registrations: false }) };
render(<CtaBanner />, undefined, store); render(<CtaBanner />, undefined, store);
expect(screen.queryAllByTestId('cta-banner')).toHaveLength(0); expect(screen.queryAllByTestId('cta-banner')).toHaveLength(0);

@ -6,10 +6,10 @@ import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
const CtaBanner = () => { const CtaBanner = () => {
const instance = useInstance(); const instance = useInstance();
const { displayCta, singleUserMode } = useSoapboxConfig(); const { displayCta } = useSoapboxConfig();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
if (me || !displayCta || singleUserMode) return null; if (me || !displayCta || !instance.registrations) return null;
return ( return (
<div data-testid='cta-banner' className='hidden lg:block'> <div data-testid='cta-banner' className='hidden lg:block'>

@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
import { remoteInteraction } from 'soapbox/actions/interactions'; import { remoteInteraction } from 'soapbox/actions/interactions';
import { Button, Modal, Stack, Text } from 'soapbox/components/ui'; 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'; import toast from 'soapbox/toast';
const messages = defineMessages({ const messages = defineMessages({
@ -31,7 +31,6 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const instance = useInstance(); const instance = useInstance();
const { singleUserMode } = useSoapboxConfig();
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name); const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
const features = useFeatures(); const features = useFeatures();
@ -98,7 +97,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
<Modal <Modal
title={header} title={header}
onClose={onClickClose} onClose={onClickClose}
confirmationAction={!singleUserMode ? onLogin : undefined} confirmationAction={instance.registrations ? onLogin : undefined}
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />} confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
secondaryAction={onRegister} secondaryAction={onRegister}
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />} secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
@ -122,7 +121,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' /> <FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
</Text> </Text>
</div> </div>
{!singleUserMode && ( {instance.registrations && (
<Text size='lg' weight='medium'> <Text size='lg' weight='medium'>
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: instance.title }} /> <FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: instance.title }} />
</Text> </Text>

@ -9,7 +9,7 @@ import { openSidebar } from 'soapbox/actions/sidebar';
import SiteLogo from 'soapbox/components/site-logo'; import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui'; import { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search'; 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'; import ProfileDropdown from './profile-dropdown';
@ -29,8 +29,7 @@ const Navbar = () => {
const node = useRef(null); const node = useRef(null);
const account = useOwnAccount(); const account = useOwnAccount();
const soapboxConfig = useSoapboxConfig(); const instance = useInstance();
const singleUserMode = soapboxConfig.get('singleUserMode');
const [isLoading, setLoading] = useState<boolean>(false); const [isLoading, setLoading] = useState<boolean>(false);
const [username, setUsername] = useState<string>(''); const [username, setUsername] = useState<string>('');
@ -151,7 +150,7 @@ const Navbar = () => {
<FormattedMessage id='account.login' defaultMessage='Log In' /> <FormattedMessage id='account.login' defaultMessage='Log In' />
</Button> </Button>
{!singleUserMode && ( {!instance.registrations && (
<Button theme='primary' to='/signup' size='sm'> <Button theme='primary' to='/signup' size='sm'>
<FormattedMessage id='account.register' defaultMessage='Sign up' /> <FormattedMessage id='account.register' defaultMessage='Sign up' />
</Button> </Button>

@ -2,14 +2,13 @@ import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Button, Stack, Text } from 'soapbox/components/ui'; import { Button, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks'; import { useAppSelector, useInstance } from 'soapbox/hooks';
const SignUpPanel = () => { const SignUpPanel = () => {
const instance = useInstance(); const instance = useInstance();
const { singleUserMode } = useSoapboxConfig();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
if (me || singleUserMode) return null; if (me || !instance.registrations) return null;
return ( return (
<Stack space={2}> <Stack space={2}>

@ -34,4 +34,15 @@ describe('normalizeSoapboxConfig()', () => {
expect(ImmutableRecord.isRecord(result.promoPanel.items.get(0))).toBe(true); expect(ImmutableRecord.isRecord(result.promoPanel.items.get(0))).toBe(true);
expect(result.promoPanel.items.get(2)?.icon).toBe('question-circle'); 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('');
});
}); });

@ -106,8 +106,6 @@ export const SoapboxConfigRecord = ImmutableRecord({
}), }),
aboutPages: ImmutableMap<string, ImmutableMap<string, unknown>>(), aboutPages: ImmutableMap<string, ImmutableMap<string, unknown>>(),
authenticatedProfile: true, authenticatedProfile: true,
singleUserMode: false,
singleUserModeProfile: '',
linkFooterMessage: '', linkFooterMessage: '',
links: ImmutableMap<string, string>(), links: ImmutableMap<string, string>(),
displayCta: true, displayCta: true,
@ -115,7 +113,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
feedInjection: true, feedInjection: true,
tileServer: '', tileServer: '',
tileServerAttribution: '', tileServerAttribution: '',
redirectRootNoLogin: '/', redirectRootNoLogin: '',
/** /**
* Whether to use the preview URL for media thumbnails. * Whether to use the preview URL for media thumbnails.
* On some platforms this can be too blurry without additional configuration. * 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<string, any>) => { export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
return SoapboxConfigRecord( return SoapboxConfigRecord(
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => { ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
@ -210,6 +247,8 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
normalizeCryptoAddresses(soapboxConfig); normalizeCryptoAddresses(soapboxConfig);
normalizeAds(soapboxConfig); normalizeAds(soapboxConfig);
normalizeAdsAlgorithm(soapboxConfig); normalizeAdsAlgorithm(soapboxConfig);
upgradeSingleUserMode(soapboxConfig);
normalizeRedirectRootNoLogin(soapboxConfig);
}), }),
); );
}; };

Loading…
Cancel
Save