Merge branch 'redirect-root' into 'develop'
Redirect the homepage to any path See merge request soapbox-pub/soapbox!2160
This commit is contained in:
commit
d86878e561
26 changed files with 390 additions and 96 deletions
|
@ -7,11 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Admin: redirect the homepage to any URL.
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
### Removed
|
||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||
|
||||
## [3.1.0] - 2023-01-13
|
||||
|
||||
### Added
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
useTheme,
|
||||
useLocale,
|
||||
useInstance,
|
||||
useRegistrationStatus,
|
||||
} from 'soapbox/hooks';
|
||||
import MESSAGES from 'soapbox/locales/messages';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
|
@ -92,13 +93,12 @@ const SoapboxMount = () => {
|
|||
const account = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const features = useFeatures();
|
||||
const { pepeEnabled } = useRegistrationStatus();
|
||||
|
||||
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 pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
const { redirectRootNoLogin } = soapboxConfig;
|
||||
|
||||
// @ts-ignore: I don't actually know what these should be, lol
|
||||
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
|
@ -134,8 +134,8 @@ const SoapboxMount = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{!me && (singleUserMode
|
||||
? <Redirect exact from='/' to={`/${singleUserMode}`} />
|
||||
{!me && (redirectRootNoLogin
|
||||
? <Redirect exact from='/' to={redirectRootNoLogin} />
|
||||
: <Route exact path='/' component={PublicLayout} />)}
|
||||
|
||||
{!me && (
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Link, Redirect, Route, Switch, useHistory, useLocation } from 'react-ro
|
|||
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount, useInstance } from 'soapbox/hooks';
|
||||
import { useOwnAccount, useInstance, useRegistrationStatus } from 'soapbox/hooks';
|
||||
|
||||
import { Button, Card, CardBody } from '../../components/ui';
|
||||
import LoginPage from '../auth-login/components/login-page';
|
||||
|
@ -28,14 +28,8 @@ const AuthLayout = () => {
|
|||
|
||||
const account = useOwnAccount();
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
const isOpen = features.accountCreation && instance.registrations;
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const isLoginPage = history.location.pathname === '/login';
|
||||
const shouldShowRegisterLink = (isLoginPage && (isOpen || (pepeEnabled && pepeOpen)));
|
||||
|
||||
return (
|
||||
<div className='h-full'>
|
||||
|
@ -50,7 +44,7 @@ const AuthLayout = () => {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
{shouldShowRegisterLink && (
|
||||
{(isLoginPage && isOpen) && (
|
||||
<div className='relative z-10 ml-auto flex items-center'>
|
||||
<Button
|
||||
theme='tertiary'
|
||||
|
|
|
@ -237,6 +237,7 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
pattern='^[a-zA-Z\d_-]+'
|
||||
icon={require('@tabler/icons/at.svg')}
|
||||
onChange={onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
required
|
||||
|
|
|
@ -6,17 +6,15 @@ import Markup from 'soapbox/components/markup';
|
|||
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import RegistrationForm from 'soapbox/features/auth-login/components/registration-form';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useFeatures, useInstance, useRegistrationStatus, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { capitalize } from 'soapbox/utils/strings';
|
||||
|
||||
const LandingPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
|
||||
const { pepeEnabled, pepeOpen } = useRegistrationStatus();
|
||||
const instance = useInstance();
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
|
||||
/** Registrations are closed */
|
||||
const renderClosed = () => {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import Header from '../header';
|
||||
|
||||
describe('<Header />', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<Header />);
|
||||
expect(screen.getByTestId('public-layout-header')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t display the signup button by default', () => {
|
||||
render(<Header />);
|
||||
expect(screen.queryByText('Register')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with registrations enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<Header />, undefined, storeOpen);
|
||||
expect(screen.getByText('Register')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with registrations closed, Pepe enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<Header />, undefined, storePepeOpen);
|
||||
expect(screen.getByText('Register')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,7 +7,7 @@ import { fetchInstance } from 'soapbox/actions/instance';
|
|||
import { openModal } from 'soapbox/actions/modals';
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import { Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useFeatures, useSoapboxConfig, useOwnAccount, useInstance, useAppDispatch } from 'soapbox/hooks';
|
||||
import { useSoapboxConfig, useOwnAccount, useAppDispatch, useRegistrationStatus } from 'soapbox/hooks';
|
||||
|
||||
import Sonar from './sonar';
|
||||
|
||||
|
@ -29,14 +29,9 @@ const Header = () => {
|
|||
|
||||
const account = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const { links } = soapboxConfig;
|
||||
|
||||
const features = useFeatures();
|
||||
const instance = useInstance();
|
||||
const isOpen = features.accountCreation && instance.registrations;
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [username, setUsername] = React.useState('');
|
||||
const [password, setPassword] = React.useState('');
|
||||
|
@ -70,7 +65,7 @@ const Header = () => {
|
|||
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
|
||||
|
||||
return (
|
||||
<header>
|
||||
<header data-testid='public-layout-header'>
|
||||
<nav className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' aria-label='Header'>
|
||||
<div className='w-full py-6 flex items-center justify-between border-b border-indigo-500 lg:border-none'>
|
||||
<div className='flex items-center sm:justify-center relative w-36'>
|
||||
|
@ -111,7 +106,7 @@ const Header = () => {
|
|||
{intl.formatMessage(messages.login)}
|
||||
</Button>
|
||||
|
||||
{(isOpen || pepeEnabled && pepeOpen) && (
|
||||
{isOpen && (
|
||||
<Button
|
||||
to='/signup'
|
||||
theme='primary'
|
||||
|
|
|
@ -47,16 +47,14 @@ 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' },
|
||||
feedInjectionHint: { id: 'soapbox_config.feed_injection_hint', defaultMessage: 'Inject the feed with additional content, such as suggested profiles.' },
|
||||
tileServerLabel: { id: 'soapbox_config.tile_server_label', defaultMessage: 'Map tile server' },
|
||||
tileServerAttributionLabel: { id: 'soapbox_config.tile_server_attribution_label', defaultMessage: 'Map tiles attribution' },
|
||||
redirectRootNoLoginLabel: { id: 'soapbox_config.redirect_root_no_login_label', defaultMessage: 'Redirect homepage' },
|
||||
redirectRootNoLoginHint: { id: 'soapbox_config.redirect_root_no_login_hint', defaultMessage: 'Path to redirect the homepage when a user is not logged in.' },
|
||||
});
|
||||
|
||||
type ValueGetter<T = Element> = (e: React.ChangeEvent<T>) => any;
|
||||
|
@ -281,25 +279,16 @@ const SoapboxConfig: React.FC = () => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.singleUserModeLabel)}
|
||||
hint={intl.formatMessage(messages.singleUserModeHint)}
|
||||
label={intl.formatMessage(messages.redirectRootNoLoginLabel)}
|
||||
hint={intl.formatMessage(messages.redirectRootNoLoginHint)}
|
||||
>
|
||||
<Toggle
|
||||
checked={soapbox.singleUserMode === true}
|
||||
onChange={handleChange(['singleUserMode'], (e) => e.target.checked)}
|
||||
<Input
|
||||
type='text'
|
||||
placeholder='/timeline/local'
|
||||
value={String(data.get('redirectRootNoLogin', ''))}
|
||||
onChange={handleChange(['redirectRootNoLogin'], (e) => e.target.value)}
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</List>
|
||||
|
||||
<CardHeader>
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { render, screen, waitFor } from '../../../jest/test-helpers';
|
||||
import { normalizeAccount } from '../../../normalizers';
|
||||
import { normalizeAccount, normalizeInstance } from '../../../normalizers';
|
||||
import UI from '../index';
|
||||
import { WrappedRoute } from '../util/react-router-helpers';
|
||||
|
||||
|
@ -33,6 +33,7 @@ describe('<UI />', () => {
|
|||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
instance: normalizeInstance({ registrations: true }),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,30 +1,34 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { storeClosed, storeLoggedIn, storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import CtaBanner from '../cta-banner';
|
||||
|
||||
describe('<CtaBanner />', () => {
|
||||
it('renders the banner', () => {
|
||||
render(<CtaBanner />);
|
||||
render(<CtaBanner />, undefined, storeOpen);
|
||||
expect(screen.getByTestId('cta-banner')).toHaveTextContent(/sign up/i);
|
||||
});
|
||||
|
||||
describe('with a logged in user', () => {
|
||||
it('renders empty', () => {
|
||||
const store = { me: true };
|
||||
|
||||
render(<CtaBanner />, undefined, store);
|
||||
render(<CtaBanner />, undefined, storeLoggedIn);
|
||||
expect(screen.queryAllByTestId('cta-banner')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with singleUserMode enabled', () => {
|
||||
describe('with registrations closed', () => {
|
||||
it('renders empty', () => {
|
||||
const store = { soapbox: ImmutableMap({ singleUserMode: true }) };
|
||||
|
||||
render(<CtaBanner />, undefined, store);
|
||||
render(<CtaBanner />, undefined, storeClosed);
|
||||
expect(screen.queryAllByTestId('cta-banner')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with Pepe enabled', () => {
|
||||
it('renders the banner', () => {
|
||||
render(<CtaBanner />, undefined, storePepeOpen);
|
||||
expect(screen.getByTestId('cta-banner')).toHaveTextContent(/sign up/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
32
app/soapbox/features/ui/components/__tests__/navbar.test.tsx
Normal file
32
app/soapbox/features/ui/components/__tests__/navbar.test.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import Navbar from '../navbar';
|
||||
|
||||
describe('<Navbar />', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<Navbar />);
|
||||
expect(screen.getByTestId('navbar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t display the signup button by default', () => {
|
||||
render(<Navbar />);
|
||||
expect(screen.queryByText('Sign up')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with registrations enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<Navbar />, undefined, storeOpen);
|
||||
expect(screen.getByText('Sign up')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with registrations closed, Pepe enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<Navbar />, undefined, storePepeOpen);
|
||||
expect(screen.getByText('Sign up')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,14 +2,15 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Banner, Button, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { useAppSelector, useInstance, useRegistrationStatus, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
const CtaBanner = () => {
|
||||
const instance = useInstance();
|
||||
const { displayCta, singleUserMode } = useSoapboxConfig();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const { displayCta } = useSoapboxConfig();
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
if (me || !displayCta || singleUserMode) return null;
|
||||
if (me || !displayCta || !isOpen) return null;
|
||||
|
||||
return (
|
||||
<div data-testid='cta-banner' className='hidden lg:block'>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import LandingPageModal from '../landing-page-modal';
|
||||
|
||||
describe('<LandingPageModal />', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<LandingPageModal onClose={jest.fn} />);
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t display the signup button by default', () => {
|
||||
render(<LandingPageModal onClose={jest.fn} />);
|
||||
expect(screen.queryByText('Register')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with registrations enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<LandingPageModal onClose={jest.fn} />, undefined, storeOpen);
|
||||
expect(screen.getByText('Register')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with registrations closed, Pepe enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<LandingPageModal onClose={jest.fn} />, undefined, storePepeOpen);
|
||||
expect(screen.getByText('Register')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import React from 'react';
|
||||
|
||||
import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import UnauthorizedModal from '../unauthorized-modal';
|
||||
|
||||
describe('<UnauthorizedModal />', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<UnauthorizedModal onClose={jest.fn} action='FOLLOW' />);
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t display the signup button by default', () => {
|
||||
render(<UnauthorizedModal onClose={jest.fn} action='FOLLOW' />);
|
||||
expect(screen.queryByText('Sign up')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with registrations enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<UnauthorizedModal onClose={jest.fn} action='FOLLOW' />, undefined, storeOpen);
|
||||
expect(screen.getByText('Sign up')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with registrations closed, Pepe enabled', () => {
|
||||
it('displays the signup button', () => {
|
||||
render(<UnauthorizedModal onClose={jest.fn} action='FOLLOW' />, undefined, storePepeOpen);
|
||||
expect(screen.getByText('Sign up')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import SiteLogo from 'soapbox/components/site-logo';
|
||||
import { Text, Button, Icon, Modal } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { useRegistrationStatus, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
download: { id: 'landing_page_modal.download', defaultMessage: 'Download' },
|
||||
|
@ -22,15 +22,9 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const { links } = soapboxConfig;
|
||||
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
|
||||
const isOpen = features.accountCreation && instance.registrations;
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<SiteLogo alt='Logo' className='h-6 w-auto cursor-pointer' />}
|
||||
|
@ -63,7 +57,7 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
|
|||
{intl.formatMessage(messages.login)}
|
||||
</Button>
|
||||
|
||||
{(isOpen || pepeEnabled && pepeOpen) && (
|
||||
{isOpen && (
|
||||
<Button to='/signup' theme='primary' block>
|
||||
{intl.formatMessage(messages.register)}
|
||||
</Button>
|
||||
|
|
|
@ -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, useRegistrationStatus } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -30,8 +30,8 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
|||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const instance = useInstance();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
|
||||
const { singleUserMode } = useSoapboxConfig();
|
||||
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
|
||||
const features = useFeatures();
|
||||
|
||||
|
@ -98,10 +98,10 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
|||
<Modal
|
||||
title={header}
|
||||
onClose={onClickClose}
|
||||
confirmationAction={!singleUserMode ? onLogin : undefined}
|
||||
confirmationAction={onLogin}
|
||||
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||
secondaryAction={onRegister}
|
||||
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||
secondaryAction={isOpen ? onRegister : undefined}
|
||||
secondaryText={isOpen ? <FormattedMessage id='account.register' defaultMessage='Sign up' /> : undefined}
|
||||
>
|
||||
<div className='remote-interaction-modal__content'>
|
||||
<form className='simple_form remote-interaction-modal__fields' onSubmit={onSubmit}>
|
||||
|
@ -122,7 +122,7 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
|||
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
|
||||
</Text>
|
||||
</div>
|
||||
{!singleUserMode && (
|
||||
{isOpen && (
|
||||
<Text size='lg' weight='medium'>
|
||||
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: instance.title }} />
|
||||
</Text>
|
||||
|
@ -142,8 +142,8 @@ const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, acco
|
|||
onClose={onClickClose}
|
||||
confirmationAction={onLogin}
|
||||
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||
secondaryAction={onRegister}
|
||||
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||
secondaryAction={isOpen ? onRegister : undefined}
|
||||
secondaryText={isOpen ? <FormattedMessage id='account.register' defaultMessage='Sign up' /> : undefined}
|
||||
>
|
||||
<Stack>
|
||||
<Text>
|
||||
|
|
|
@ -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, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
|
||||
|
||||
import ProfileDropdown from './profile-dropdown';
|
||||
|
||||
|
@ -25,12 +25,9 @@ const messages = defineMessages({
|
|||
const Navbar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const node = useRef(null);
|
||||
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const account = useOwnAccount();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
const singleUserMode = soapboxConfig.get('singleUserMode');
|
||||
const node = useRef(null);
|
||||
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
|
@ -66,7 +63,7 @@ const Navbar = () => {
|
|||
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
|
||||
|
||||
return (
|
||||
<nav className='bg-white dark:bg-primary-900 shadow z-50 sticky top-0' ref={node}>
|
||||
<nav className='bg-white dark:bg-primary-900 shadow z-50 sticky top-0' ref={node} data-testid='navbar'>
|
||||
<div className='max-w-7xl mx-auto px-2 sm:px-6 lg:px-8'>
|
||||
<div className='relative flex justify-between h-12 lg:h-16'>
|
||||
{account && (
|
||||
|
@ -151,7 +148,7 @@ const Navbar = () => {
|
|||
<FormattedMessage id='account.login' defaultMessage='Log In' />
|
||||
</Button>
|
||||
|
||||
{!singleUserMode && (
|
||||
{isOpen && (
|
||||
<Button theme='primary' to='/signup' size='sm'>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
import { storeOpen, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import SignUpPanel from '../sign-up-panel';
|
||||
|
||||
describe('<SignUpPanel />', () => {
|
||||
it('doesn\'t render by default', () => {
|
||||
render(<SignUpPanel />);
|
||||
expect(screen.queryByTestId('sign-up-panel')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('with registrations enabled', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<SignUpPanel />, undefined, storeOpen);
|
||||
expect(screen.getByTestId('sign-up-panel')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('with registrations closed, Pepe enabled', () => {
|
||||
it('successfully renders', () => {
|
||||
render(<SignUpPanel />, undefined, storePepeOpen);
|
||||
expect(screen.getByTestId('sign-up-panel')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,17 +2,17 @@ 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, useRegistrationStatus } from 'soapbox/hooks';
|
||||
|
||||
const SignUpPanel = () => {
|
||||
const instance = useInstance();
|
||||
const { singleUserMode } = useSoapboxConfig();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
if (me || singleUserMode) return null;
|
||||
if (me || !isOpen) return null;
|
||||
|
||||
return (
|
||||
<Stack space={2}>
|
||||
<Stack space={2} data-testid='sign-up-panel'>
|
||||
<Stack>
|
||||
<Text size='lg' weight='bold'>
|
||||
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
|
||||
|
|
46
app/soapbox/hooks/__tests__/useRegistrationStatus.test.ts
Normal file
46
app/soapbox/hooks/__tests__/useRegistrationStatus.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { storeClosed, storeOpen, storePepeClosed, storePepeOpen } from 'soapbox/jest/mock-stores';
|
||||
import { renderHook } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { useRegistrationStatus } from '../useRegistrationStatus';
|
||||
|
||||
describe('useRegistrationStatus()', () => {
|
||||
test('Registrations open', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storeOpen);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: true,
|
||||
pepeEnabled: false,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storeClosed);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: false,
|
||||
pepeEnabled: false,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed, Pepe enabled & open', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storePepeOpen);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: true,
|
||||
pepeEnabled: true,
|
||||
pepeOpen: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('Registrations closed, Pepe enabled & closed', () => {
|
||||
const { result } = renderHook(useRegistrationStatus, undefined, storePepeClosed);
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
isOpen: false,
|
||||
pepeEnabled: true,
|
||||
pepeOpen: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,7 +12,8 @@ export { useOnScreen } from './useOnScreen';
|
|||
export { useOwnAccount } from './useOwnAccount';
|
||||
export { usePrevious } from './usePrevious';
|
||||
export { useRefEventHandler } from './useRefEventHandler';
|
||||
export { useRegistrationStatus } from './useRegistrationStatus';
|
||||
export { useSettings } from './useSettings';
|
||||
export { useSoapboxConfig } from './useSoapboxConfig';
|
||||
export { useSystemTheme } from './useSystemTheme';
|
||||
export { useTheme } from './useTheme';
|
||||
export { useTheme } from './useTheme';
|
22
app/soapbox/hooks/useRegistrationStatus.ts
Normal file
22
app/soapbox/hooks/useRegistrationStatus.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useAppSelector } from './useAppSelector';
|
||||
import { useFeatures } from './useFeatures';
|
||||
import { useInstance } from './useInstance';
|
||||
import { useSoapboxConfig } from './useSoapboxConfig';
|
||||
|
||||
export const useRegistrationStatus = () => {
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const pepeOpen = useAppSelector(state => state.verification.instance.get('registrations') === true);
|
||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||
|
||||
return {
|
||||
/** Registrations are open, either through Pepe or traditional account creation. */
|
||||
isOpen: (features.accountCreation && instance.registrations) || (pepeEnabled && pepeOpen),
|
||||
/** Whether Pepe is open. */
|
||||
pepeOpen,
|
||||
/** Whether Pepe is enabled. */
|
||||
pepeEnabled,
|
||||
};
|
||||
};
|
40
app/soapbox/jest/mock-stores.tsx
Normal file
40
app/soapbox/jest/mock-stores.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import alexJson from 'soapbox/__fixtures__/pleroma-account.json';
|
||||
import { normalizeAccount, normalizeInstance } from 'soapbox/normalizers';
|
||||
|
||||
/** Store with registrations open. */
|
||||
const storeOpen = { instance: normalizeInstance({ registrations: true }) };
|
||||
|
||||
/** Store with registrations closed. */
|
||||
const storeClosed = { instance: normalizeInstance({ registrations: false }) };
|
||||
|
||||
/** Store with registrations closed, and Pepe enabled & open. */
|
||||
const storePepeOpen = {
|
||||
instance: normalizeInstance({ registrations: false }),
|
||||
soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
|
||||
verification: { instance: fromJS({ registrations: true }) },
|
||||
};
|
||||
|
||||
/** Store with registrations closed, and Pepe enabled & closed. */
|
||||
const storePepeClosed = {
|
||||
instance: normalizeInstance({ registrations: false }),
|
||||
soapbox: fromJS({ extensions: { pepe: { enabled: true } } }),
|
||||
verification: { instance: fromJS({ registrations: false }) },
|
||||
};
|
||||
|
||||
/** Store with a logged-in user. */
|
||||
const storeLoggedIn = {
|
||||
me: alexJson.id,
|
||||
accounts: ImmutableMap({
|
||||
[alexJson.id]: normalizeAccount(alexJson),
|
||||
}),
|
||||
};
|
||||
|
||||
export {
|
||||
storeOpen,
|
||||
storeClosed,
|
||||
storePepeOpen,
|
||||
storePepeClosed,
|
||||
storeLoggedIn,
|
||||
};
|
|
@ -1198,12 +1198,10 @@
|
|||
"soapbox_config.raw_json_hint": "Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click Save to apply your changes.",
|
||||
"soapbox_config.raw_json_invalid": "is invalid",
|
||||
"soapbox_config.raw_json_label": "Advanced: Edit raw JSON data",
|
||||
"soapbox_config.redirect_root_no_login_hint": "Path to redirect the homepage when a user is not logged in.",
|
||||
"soapbox_config.redirect_root_no_login_label": "Redirect homepage",
|
||||
"soapbox_config.save": "Save",
|
||||
"soapbox_config.saved": "Soapbox config saved!",
|
||||
"soapbox_config.single_user_mode_hint": "Front page will redirect to a given user profile.",
|
||||
"soapbox_config.single_user_mode_label": "Single user mode",
|
||||
"soapbox_config.single_user_mode_profile_hint": "@handle",
|
||||
"soapbox_config.single_user_mode_profile_label": "Main user handle",
|
||||
"soapbox_config.tile_server_attribution_label": "Map tiles attribution",
|
||||
"soapbox_config.tile_server_label": "Map tile server",
|
||||
"soapbox_config.verified_can_edit_name_label": "Allow verified users to edit their own display name.",
|
||||
|
|
|
@ -34,4 +34,17 @@ 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: true, singleUserModeProfile: '@alex' }).redirectRootNoLogin).toBe('/@alex');
|
||||
expect(normalizeSoapboxConfig({ singleUserMode: true, singleUserModeProfile: 'alex@gleasonator.com' }).redirectRootNoLogin).toBe('/@alex@gleasonator.com');
|
||||
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('');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from 'immutable';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
|
||||
import { normalizeUsername } from 'soapbox/utils/input';
|
||||
import { toTailwind } from 'soapbox/utils/tailwind';
|
||||
import { generateAccent } from 'soapbox/utils/theme';
|
||||
|
||||
|
@ -106,8 +107,6 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
|||
}),
|
||||
aboutPages: ImmutableMap<string, ImmutableMap<string, unknown>>(),
|
||||
authenticatedProfile: true,
|
||||
singleUserMode: false,
|
||||
singleUserModeProfile: '',
|
||||
linkFooterMessage: '',
|
||||
links: ImmutableMap<string, string>(),
|
||||
displayCta: true,
|
||||
|
@ -115,6 +114,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
|||
feedInjection: true,
|
||||
tileServer: '',
|
||||
tileServerAttribution: '',
|
||||
redirectRootNoLogin: '',
|
||||
/**
|
||||
* Whether to use the preview URL for media thumbnails.
|
||||
* On some platforms this can be too blurry without additional configuration.
|
||||
|
@ -197,6 +197,45 @@ const normalizeAdsAlgorithm = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMa
|
|||
}
|
||||
};
|
||||
|
||||
/** Single user mode is now managed by `redirectRootNoLogin`. */
|
||||
const upgradeSingleUserMode = (soapboxConfig: SoapboxConfigMap): SoapboxConfigMap => {
|
||||
const singleUserMode = soapboxConfig.get('singleUserMode') as boolean | undefined;
|
||||
const singleUserModeProfile = soapboxConfig.get('singleUserModeProfile') as string | undefined;
|
||||
const redirectRootNoLogin = soapboxConfig.get('redirectRootNoLogin') as string | undefined;
|
||||
|
||||
if (!redirectRootNoLogin && singleUserMode && singleUserModeProfile) {
|
||||
return soapboxConfig
|
||||
.set('redirectRootNoLogin', `/@${normalizeUsername(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, 'http://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>) => {
|
||||
return SoapboxConfigRecord(
|
||||
ImmutableMap(fromJS(soapboxConfig)).withMutations(soapboxConfig => {
|
||||
|
@ -209,6 +248,8 @@ export const normalizeSoapboxConfig = (soapboxConfig: Record<string, any>) => {
|
|||
normalizeCryptoAddresses(soapboxConfig);
|
||||
normalizeAds(soapboxConfig);
|
||||
normalizeAdsAlgorithm(soapboxConfig);
|
||||
upgradeSingleUserMode(soapboxConfig);
|
||||
normalizeRedirectRootNoLogin(soapboxConfig);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue