diff --git a/src/containers/soapbox.tsx b/src/containers/soapbox.tsx index fb5a15971..21897ab8c 100644 --- a/src/containers/soapbox.tsx +++ b/src/containers/soapbox.tsx @@ -18,7 +18,6 @@ import Helmet from 'soapbox/components/helmet'; import LoadingScreen from 'soapbox/components/loading-screen'; import { StatProvider } from 'soapbox/contexts/stat-context'; import EmbeddedStatus from 'soapbox/features/embedded-status'; -import PublicLayout from 'soapbox/features/public-layout'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; import { ModalContainer, @@ -95,12 +94,8 @@ const SoapboxMount = () => { /** Render the auth layout or UI. */ const renderSwitch = () => ( - {!me && (redirectRootNoLogin - ? - : )} - - {!me && ( - + {(!me && redirectRootNoLogin) && ( + )} diff --git a/src/features/landing-page/__tests__/landing-page.test.tsx b/src/features/landing-page/__tests__/landing-page.test.tsx deleted file mode 100644 index bbc96deeb..000000000 --- a/src/features/landing-page/__tests__/landing-page.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; - -import { rememberInstance } from 'soapbox/actions/instance'; -import { render, screen, rootReducer } from 'soapbox/jest/test-helpers'; - -import LandingPage from '..'; - -describe('', () => { - it('renders a RegistrationForm for an open Pleroma instance', () => { - - const state = rootReducer(undefined, { - type: rememberInstance.fulfilled.type, - payload: { - version: '2.7.2 (compatible; Pleroma 2.3.0)', - registrations: true, - }, - }); - - render(, undefined, state); - - expect(screen.queryByTestId('registrations-open')).toBeInTheDocument(); - expect(screen.queryByTestId('registrations-closed')).not.toBeInTheDocument(); - }); - - it('renders "closed" message for a closed Pleroma instance', () => { - - const state = rootReducer(undefined, { - type: rememberInstance.fulfilled.type, - payload: { - version: '2.7.2 (compatible; Pleroma 2.3.0)', - registrations: false, - }, - }); - - render(, undefined, state); - - expect(screen.queryByTestId('registrations-closed')).toBeInTheDocument(); - expect(screen.queryByTestId('registrations-open')).not.toBeInTheDocument(); - }); -}); diff --git a/src/features/landing-page/index.tsx b/src/features/landing-page/index.tsx deleted file mode 100644 index a31d7d313..000000000 --- a/src/features/landing-page/index.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { prepareRequest } from 'soapbox/actions/consumer-auth'; -import Markup from 'soapbox/components/markup'; -import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; -import RegistrationForm from 'soapbox/features/auth-login/components/registration-form'; -import { useAppDispatch, useFeatures, useInstance, useSoapboxConfig } from 'soapbox/hooks'; -import { capitalize } from 'soapbox/utils/strings'; - -const LandingPage = () => { - const dispatch = useAppDispatch(); - const features = useFeatures(); - const soapboxConfig = useSoapboxConfig(); - const instance = useInstance(); - - /** Registrations are closed */ - const renderClosed = () => { - return ( - - - - - - - - - ); - }; - - /** Mastodon API registrations are open */ - const renderOpen = () => { - return ; - }; - - /** Display login button for external provider. */ - const renderProvider = () => { - const { authProvider } = soapboxConfig; - - return ( - - - - - - - - - - ); - }; - - // Render registration flow depending on features - const renderBody = () => { - if (soapboxConfig.authProvider) { - return renderProvider(); - } else if (features.accountCreation && instance.registrations) { - return renderOpen(); - } else { - return renderClosed(); - } - }; - - return ( -
-
-
-
-
- -

- {instance.title} -

- - -
-
-
-
- - - {renderBody()} - - -
-
-
-
- ); -}; - -export default LandingPage; diff --git a/src/features/landing-timeline/components/logo-text.tsx b/src/features/landing-timeline/components/logo-text.tsx new file mode 100644 index 000000000..eeaaa0070 --- /dev/null +++ b/src/features/landing-timeline/components/logo-text.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface ILogoText { + children: React.ReactNode +} + +/** Big text in site colors, for displaying the site name. Resizes itself according to the screen size. */ +const LogoText: React.FC = ({ children }) => { + return ( +

+ {children} +

+ ); +}; + +export { LogoText }; \ No newline at end of file diff --git a/src/features/landing-timeline/components/site-banner.tsx b/src/features/landing-timeline/components/site-banner.tsx new file mode 100644 index 000000000..91bd0bcc3 --- /dev/null +++ b/src/features/landing-timeline/components/site-banner.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import Markup from 'soapbox/components/markup'; +import { Stack } from 'soapbox/components/ui'; +import { useInstance } from 'soapbox/hooks'; + +import { LogoText } from './logo-text'; + +const SiteBanner: React.FC = () => { + const instance = useInstance(); + + return ( + + {instance.title} + + + + ); +}; + +export { SiteBanner }; \ No newline at end of file diff --git a/src/features/landing-timeline/index.tsx b/src/features/landing-timeline/index.tsx new file mode 100644 index 000000000..f485a831b --- /dev/null +++ b/src/features/landing-timeline/index.tsx @@ -0,0 +1,57 @@ +import React, { useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { expandCommunityTimeline } from 'soapbox/actions/timelines'; +import { useCommunityStream } from 'soapbox/api/hooks'; +import PullToRefresh from 'soapbox/components/pull-to-refresh'; +import { Column } from 'soapbox/components/ui'; +import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks'; + +import Timeline from '../ui/components/timeline'; + +import { SiteBanner } from './components/site-banner'; + +const LandingTimeline = () => { + const dispatch = useAppDispatch(); + + const settings = useSettings(); + const onlyMedia = !!settings.getIn(['community', 'other', 'onlyMedia'], false); + const next = useAppSelector(state => state.timelines.get('community')?.next); + + const timelineId = 'community'; + + const handleLoadMore = (maxId: string) => { + dispatch(expandCommunityTimeline({ url: next, maxId, onlyMedia })); + }; + + const handleRefresh = () => { + return dispatch(expandCommunityTimeline({ onlyMedia })); + }; + + useCommunityStream({ onlyMedia }); + + useEffect(() => { + dispatch(expandCommunityTimeline({ onlyMedia })); + }, [onlyMedia]); + + return ( + +
+ +
+ + + } + divideType='space' + /> + +
+ ); +}; + +export default LandingTimeline; diff --git a/src/features/public-layout/components/__tests__/header.test.tsx b/src/features/public-layout/components/__tests__/header.test.tsx deleted file mode 100644 index 4472ce7b4..000000000 --- a/src/features/public-layout/components/__tests__/header.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -import { storeOpen } from 'soapbox/jest/mock-stores'; -import { render, screen } from 'soapbox/jest/test-helpers'; - -import Header from '../header'; - -describe('
', () => { - it('successfully renders', () => { - render(
); - expect(screen.getByTestId('public-layout-header')).toBeInTheDocument(); - }); - - it('doesn\'t display the signup button by default', () => { - render(
); - expect(screen.queryByText('Register')).not.toBeInTheDocument(); - }); - - describe('with registrations enabled', () => { - it('displays the signup button', () => { - render(
, undefined, storeOpen); - expect(screen.getByText('Register')).toBeInTheDocument(); - }); - }); -}); \ No newline at end of file diff --git a/src/features/public-layout/components/footer.tsx b/src/features/public-layout/components/footer.tsx deleted file mode 100644 index 4038b5d18..000000000 --- a/src/features/public-layout/components/footer.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { List as ImmutableList } from 'immutable'; -import React from 'react'; -import { Link } from 'react-router-dom'; - -import { getSettings } from 'soapbox/actions/settings'; -import { getSoapboxConfig } from 'soapbox/actions/soapbox'; -import { Text } from 'soapbox/components/ui'; -import { useAppSelector } from 'soapbox/hooks'; - -import type { FooterItem } from 'soapbox/types/soapbox'; - -const Footer = () => { - const { copyright, navlinks, locale } = useAppSelector((state) => { - const soapboxConfig = getSoapboxConfig(state); - - return { - copyright: soapboxConfig.copyright, - navlinks: (soapboxConfig.navlinks.get('homeFooter') || ImmutableList()) as ImmutableList, - locale: getSettings(state).get('locale') as string, - }; - }); - - return ( -
-
- {navlinks.map((link, idx) => { - const url = link.get('url'); - const isExternal = url.startsWith('http'); - const Comp = (isExternal ? 'a' : Link) as 'a'; - const compProps = isExternal ? { href: url, target: '_blank' } : { to: url }; - - return ( -
- - - {(link.getIn(['titleLocales', locale]) || link.get('title')) as string} - - -
- ); - })} -
- -
- {copyright} -
-
- ); -}; - -export default Footer; diff --git a/src/features/public-layout/components/header.tsx b/src/features/public-layout/components/header.tsx deleted file mode 100644 index 1d6edae19..000000000 --- a/src/features/public-layout/components/header.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { Link, Redirect } from 'react-router-dom'; - -import { logIn, verifyCredentials } from 'soapbox/actions/auth'; -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 { useSoapboxConfig, useOwnAccount, useAppDispatch, useRegistrationStatus, useFeatures } from 'soapbox/hooks'; - -import Sonar from './sonar'; - -import type { AxiosError } from 'axios'; - -const messages = defineMessages({ - menu: { id: 'header.menu.title', defaultMessage: 'Open menu' }, - home: { id: 'header.home.label', defaultMessage: 'Home' }, - login: { id: 'header.login.label', defaultMessage: 'Log in' }, - register: { id: 'header.register.label', defaultMessage: 'Register' }, - username: { id: 'header.login.username.placeholder', defaultMessage: 'E-mail or username' }, - email: { id: 'header.login.email.placeholder', defaultMessage: 'E-mail address' }, - password: { id: 'header.login.password.label', defaultMessage: 'Password' }, - forgotPassword: { id: 'header.login.forgot_password', defaultMessage: 'Forgot password?' }, -}); - -const Header = () => { - const dispatch = useAppDispatch(); - const intl = useIntl(); - const features = useFeatures(); - - const { account } = useOwnAccount(); - const soapboxConfig = useSoapboxConfig(); - const { isOpen } = useRegistrationStatus(); - const { links } = soapboxConfig; - - const [isLoading, setLoading] = React.useState(false); - const [username, setUsername] = React.useState(''); - const [password, setPassword] = React.useState(''); - const [shouldRedirect, setShouldRedirect] = React.useState(false); - const [mfaToken, setMfaToken] = React.useState(false); - - const open = () => dispatch(openModal('LANDING_PAGE')); - - const handleSubmit: React.FormEventHandler = (event) => { - event.preventDefault(); - setLoading(true); - - dispatch(logIn(username, password) as any) - .then(({ access_token }: { access_token: string }) => ( - dispatch(verifyCredentials(access_token) as any) - // Refetch the instance for authenticated fetch - .then(() => dispatch(fetchInstance())) - .then(() => setShouldRedirect(true)) - )) - .catch((error: AxiosError) => { - setLoading(false); - - const data: any = error.response?.data; - if (data?.error === 'mfa_required') { - setMfaToken(data.mfa_token); - } - }); - }; - - if (account && shouldRedirect) return ; - if (mfaToken) return ; - - return ( -
- -
- ); -}; - -export default Header; diff --git a/src/features/public-layout/components/sonar.tsx b/src/features/public-layout/components/sonar.tsx deleted file mode 100644 index 1106d5ba5..000000000 --- a/src/features/public-layout/components/sonar.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; - -const Sonar = () => ( -
-
-
-
-
-
- -
-
-
-); - -export default Sonar; diff --git a/src/features/public-layout/index.tsx b/src/features/public-layout/index.tsx deleted file mode 100644 index e39bb7508..000000000 --- a/src/features/public-layout/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { Switch, Route, Redirect } from 'react-router-dom'; - -import LandingGradient from 'soapbox/components/landing-gradient'; -import { useAppSelector } from 'soapbox/hooks'; -import { isStandalone } from 'soapbox/utils/state'; - -import LandingPage from '../landing-page'; - -import Footer from './components/footer'; -import Header from './components/header'; - -const PublicLayout = () => { - const standalone = useAppSelector((state) => isStandalone(state)); - - if (standalone) { - return ; - } - - return ( -
- - -
-
-
- -
- - - -
-
- -
-
-
- ); -}; - -export default PublicLayout; diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 5ff06223e..179f123e2 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -21,16 +21,18 @@ import withHoc from 'soapbox/components/hoc/with-hoc'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; import ThumbNavigation from 'soapbox/components/thumb-navigation'; import { Layout } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles, useInstance } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useDraggedFiles, useInstance, useLoggedIn } from 'soapbox/hooks'; import AdminPage from 'soapbox/pages/admin-page'; import ChatsPage from 'soapbox/pages/chats-page'; import DefaultPage from 'soapbox/pages/default-page'; +import EmptyPage from 'soapbox/pages/empty-page'; import EventPage from 'soapbox/pages/event-page'; import EventsPage from 'soapbox/pages/events-page'; import GroupPage from 'soapbox/pages/group-page'; import GroupsPage from 'soapbox/pages/groups-page'; import GroupsPendingPage from 'soapbox/pages/groups-pending-page'; import HomePage from 'soapbox/pages/home-page'; +import LandingPage from 'soapbox/pages/landing-page'; import ManageGroupsPage from 'soapbox/pages/manage-groups-page'; import ProfilePage from 'soapbox/pages/profile-page'; import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; @@ -139,6 +141,7 @@ import { PasswordResetConfirm, RegisterInvite, ExternalLogin, + LandingTimeline, } from './util/async-components'; import GlobalHotkeys from './util/global-hotkeys'; import { WrappedRoute } from './util/react-router-helpers'; @@ -157,8 +160,6 @@ const EditGroupSlug = withHoc(EditGroup as any, GroupLookupHoc); const GroupBlockedMembersSlug = withHoc(GroupBlockedMembers as any, GroupLookupHoc); const GroupMembershipRequestsSlug = withHoc(GroupMembershipRequests as any, GroupLookupHoc); -const EmptyPage = HomePage; - interface ISwitchingColumnsArea { children: React.ReactNode } @@ -167,6 +168,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => const instance = useInstance(); const features = useFeatures(); const { search } = useLocation(); + const { isLoggedIn } = useLoggedIn(); const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig(); const hasCrypto = cryptoAddresses.size > 0; @@ -181,7 +183,11 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - + {isLoggedIn ? ( + + ) : ( + + )} {/* NOTE: we cannot nest routes in a fragment @@ -254,7 +260,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => - + {features.suggestions && } {features.profileDirectory && } {features.events && } @@ -359,7 +365,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {(features.accountCreation && instance.registrations) && ( - + )} diff --git a/src/features/ui/util/async-components.ts b/src/features/ui/util/async-components.ts index bef8fcf30..120617f49 100644 --- a/src/features/ui/util/async-components.ts +++ b/src/features/ui/util/async-components.ts @@ -10,6 +10,10 @@ export function Notifications() { return import('../../notifications'); } +export function LandingTimeline() { + return import('../../landing-timeline'); +} + export function HomeTimeline() { return import('../../home-timeline'); } diff --git a/src/pages/home-page.tsx b/src/pages/home-page.tsx index 6ed7b8eea..06a9311e2 100644 --- a/src/pages/home-page.tsx +++ b/src/pages/home-page.tsx @@ -108,12 +108,12 @@ const HomePage: React.FC = ({ children }) => { {Component => } )} - {hasPatron && ( + {(hasPatron && me) && ( {Component => } )} - {hasCrypto && cryptoLimit > 0 && ( + {(hasCrypto && cryptoLimit > 0 && me) && ( {Component => } diff --git a/src/pages/landing-page.tsx b/src/pages/landing-page.tsx new file mode 100644 index 000000000..1eba1de91 --- /dev/null +++ b/src/pages/landing-page.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import LinkFooter from 'soapbox/features/ui/components/link-footer'; +import { + TrendsPanel, + SignUpPanel, + CtaBanner, +} from 'soapbox/features/ui/util/async-components'; +import { useAppSelector, useFeatures } from 'soapbox/hooks'; + +import { Layout } from '../components/ui'; +import BundleContainer from '../features/ui/containers/bundle-container'; + +interface ILandingPage { + children: React.ReactNode +} + +const LandingPage: React.FC = ({ children }) => { + const me = useAppSelector(state => state.me); + const features = useFeatures(); + + return ( + <> + + {children} + + {!me && ( + + {Component => } + + )} + + + + {!me && ( + + {Component => } + + )} + {features.trends && ( + + {Component => } + + )} + + + + ); +}; + +export default LandingPage;