Merge branch 'redirect-root' into 'develop'

Redirect the homepage to any path

See merge request soapbox-pub/soapbox!2160
environments/review-develop-3zknud/deployments/2309
Alex Gleason 2023-01-15 19:37:19 +00:00
commit d86878e561
26 zmienionych plików z 390 dodań i 96 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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 && (

Wyświetl plik

@ -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'

Wyświetl plik

@ -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

Wyświetl plik

@ -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 = () => {

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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'

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 }),
};
});

Wyświetl plik

@ -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);
});
});
});

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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'>

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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 }} />

Wyświetl plik

@ -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,
});
});
});

Wyświetl plik

@ -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';

Wyświetl plik

@ -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,
};
};

Wyświetl plik

@ -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,
};

Wyświetl plik

@ -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.",

Wyświetl plik

@ -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('');
});
});

Wyświetl plik

@ -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);
}),
);
};