Remove singleUserMode, upgrade to redirectRootNoLogin
This commit is contained in:
parent
1e07c03479
commit
6f0e398a78
9 changed files with 70 additions and 47 deletions
|
@ -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…
Reference in a new issue