@ -95,6 +95,7 @@ interface IAccount {
|
||||
emojiUrl?: string;
|
||||
note?: string;
|
||||
items?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Account = ({
|
||||
@ -122,6 +123,7 @@ const Account = ({
|
||||
emojiUrl,
|
||||
note,
|
||||
items,
|
||||
disabled,
|
||||
}: IAccount) => {
|
||||
const overflowRef = useRef<HTMLDivElement>(null);
|
||||
const actionRef = useRef<HTMLDivElement>(null);
|
||||
@ -187,6 +189,56 @@ const Account = ({
|
||||
onClick: (event: React.MouseEvent) => event.stopPropagation(),
|
||||
} : {};
|
||||
|
||||
if (disabled) return (
|
||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||
<HStack alignItems={actionAlignment} space={3} justifyContent='between'>
|
||||
<HStack alignItems='center' space={3} className='overflow-hidden'>
|
||||
<div className='rounded-full'>
|
||||
<Avatar src={account.avatar} size={avatarSize} />
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='absolute -right-1.5 bottom-0 h-5 w-5'
|
||||
emoji={emoji}
|
||||
src={emojiUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='grow overflow-hidden'>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text
|
||||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
|
||||
{account.bot && <Badge slug='bot' title={intl.formatMessage(messages.bot)} />}
|
||||
</HStack>
|
||||
|
||||
<Stack space={withAccountNote || note ? 1 : 0}>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='muted' size='sm' direction='ltr' truncate>@{username}</Text>
|
||||
|
||||
{account.pleroma?.favicon && (
|
||||
<InstanceFavicon account={account} disabled={!withLinkToProfile} />
|
||||
)}
|
||||
|
||||
{items}
|
||||
</HStack>
|
||||
</Stack>
|
||||
</div>
|
||||
</HStack>
|
||||
|
||||
<div ref={actionRef}>
|
||||
{renderAction()}
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||
<HStack alignItems={actionAlignment} space={3} justifyContent='between'>
|
||||
|
||||
@ -31,6 +31,8 @@ const messages = defineMessages({
|
||||
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
login: { id: 'account.login', defaultMessage: 'Log in' },
|
||||
register: { id: 'account.register', defaultMessage: 'Sign up' },
|
||||
});
|
||||
|
||||
interface ISidebarLink {
|
||||
@ -128,8 +130,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||
});
|
||||
}, [sidebarOpen]);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-expanded={sidebarOpen}
|
||||
@ -169,167 +169,185 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
||||
|
||||
<div className='relative h-full w-full overflow-auto overflow-y-scroll'>
|
||||
<div className='p-4'>
|
||||
<Stack space={4}>
|
||||
<Link to={`/@${account.acct}`} onClick={onClose}>
|
||||
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} />
|
||||
</Link>
|
||||
|
||||
<ProfileStats
|
||||
account={account}
|
||||
onClickHandler={handleClose}
|
||||
/>
|
||||
|
||||
{account ? (
|
||||
<Stack space={4}>
|
||||
<Divider />
|
||||
<Link to={`/@${account.acct}`} onClick={onClose}>
|
||||
<Account account={account} showProfileHoverCard={false} withLinkToProfile={false} />
|
||||
</Link>
|
||||
|
||||
<SidebarLink
|
||||
to={`/@${account.acct}`}
|
||||
icon={require('@tabler/icons/outline/user.svg')}
|
||||
text={intl.formatMessage(messages.profile)}
|
||||
onClick={onClose}
|
||||
<ProfileStats
|
||||
account={account}
|
||||
onClickHandler={handleClose}
|
||||
/>
|
||||
|
||||
{(account.locked || followRequestsCount > 0) && (
|
||||
<SidebarLink
|
||||
to='/follow_requests'
|
||||
icon={require('@tabler/icons/outline/user-plus.svg')}
|
||||
text={intl.formatMessage(messages.followRequests)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.bookmarks && (
|
||||
<SidebarLink
|
||||
to='/bookmarks'
|
||||
icon={require('@tabler/icons/outline/bookmark.svg')}
|
||||
text={intl.formatMessage(messages.bookmarks)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.groups && (
|
||||
<SidebarLink
|
||||
to='/groups'
|
||||
icon={require('@tabler/icons/outline/circles.svg')}
|
||||
text={intl.formatMessage(messages.groups)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.lists && (
|
||||
<SidebarLink
|
||||
to='/lists'
|
||||
icon={require('@tabler/icons/outline/list.svg')}
|
||||
text={intl.formatMessage(messages.lists)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.events && (
|
||||
<SidebarLink
|
||||
to='/events'
|
||||
icon={require('@tabler/icons/outline/calendar-event.svg')}
|
||||
text={intl.formatMessage(messages.events)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{draftCount > 0 && (
|
||||
<SidebarLink
|
||||
to='/draft_statuses'
|
||||
icon={require('@tabler/icons/outline/notes.svg')}
|
||||
text={intl.formatMessage(messages.drafts)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.publicTimeline && <>
|
||||
<Stack space={4}>
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
to={`/@${account.acct}`}
|
||||
icon={require('@tabler/icons/outline/user.svg')}
|
||||
text={intl.formatMessage(messages.profile)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
{(account.locked || followRequestsCount > 0) && (
|
||||
<SidebarLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
to='/follow_requests'
|
||||
icon={require('@tabler/icons/outline/user-plus.svg')}
|
||||
text={intl.formatMessage(messages.followRequests)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</>}
|
||||
|
||||
<Divider />
|
||||
{features.bookmarks && (
|
||||
<SidebarLink
|
||||
to='/bookmarks'
|
||||
icon={require('@tabler/icons/outline/bookmark.svg')}
|
||||
text={intl.formatMessage(messages.bookmarks)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.groups && (
|
||||
<SidebarLink
|
||||
to='/groups'
|
||||
icon={require('@tabler/icons/outline/circles.svg')}
|
||||
text={intl.formatMessage(messages.groups)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.lists && (
|
||||
<SidebarLink
|
||||
to='/lists'
|
||||
icon={require('@tabler/icons/outline/list.svg')}
|
||||
text={intl.formatMessage(messages.lists)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.events && (
|
||||
<SidebarLink
|
||||
to='/events'
|
||||
icon={require('@tabler/icons/outline/calendar-event.svg')}
|
||||
text={intl.formatMessage(messages.events)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{draftCount > 0 && (
|
||||
<SidebarLink
|
||||
to='/draft_statuses'
|
||||
icon={require('@tabler/icons/outline/notes.svg')}
|
||||
text={intl.formatMessage(messages.drafts)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.publicTimeline && <>
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/outline/affiliate.svg') : require('@tabler/icons/outline/world.svg')}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
<SidebarLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('@tabler/icons/outline/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</>}
|
||||
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/settings/preferences'
|
||||
icon={require('@tabler/icons/outline/settings.svg')}
|
||||
text={intl.formatMessage(messages.preferences)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.followedHashtagsList && (
|
||||
<SidebarLink
|
||||
to='/followed_tags'
|
||||
icon={require('@tabler/icons/outline/hash.svg')}
|
||||
text={intl.formatMessage(messages.followedTags)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settings.get('isDeveloper') && (
|
||||
<SidebarLink
|
||||
to='/developers'
|
||||
icon={require('@tabler/icons/outline/code.svg')}
|
||||
text={intl.formatMessage(messages.developers)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/logout'
|
||||
icon={require('@tabler/icons/outline/logout.svg')}
|
||||
text={intl.formatMessage(messages.logout)}
|
||||
onClick={onClickLogOut}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack space={4}>
|
||||
<button type='button' onClick={handleSwitcherClick} className='py-1'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text tag='span'>
|
||||
<FormattedMessage id='profile_dropdown.switch_account' defaultMessage='Switch accounts' />
|
||||
</Text>
|
||||
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
className={clsx('h-4 w-4 text-gray-900 transition-transform dark:text-gray-100', {
|
||||
'rotate-180': switcher,
|
||||
})}
|
||||
/>
|
||||
</HStack>
|
||||
</button>
|
||||
|
||||
{switcher && (
|
||||
<div className='border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800'>
|
||||
{otherAccounts.map(account => renderAccount(account))}
|
||||
|
||||
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
||||
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/outline/plus.svg')} />
|
||||
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack space={4}>
|
||||
<SidebarLink
|
||||
to='/settings/preferences'
|
||||
icon={require('@tabler/icons/outline/settings.svg')}
|
||||
text={intl.formatMessage(messages.preferences)}
|
||||
to='/login'
|
||||
icon={require('@tabler/icons/outline/login.svg')}
|
||||
text={intl.formatMessage(messages.login)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.followedHashtagsList && (
|
||||
<SidebarLink
|
||||
to='/followed_tags'
|
||||
icon={require('@tabler/icons/outline/hash.svg')}
|
||||
text={intl.formatMessage(messages.followedTags)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{settings.get('isDeveloper') && (
|
||||
<SidebarLink
|
||||
to='/developers'
|
||||
icon={require('@tabler/icons/outline/code.svg')}
|
||||
text={intl.formatMessage(messages.developers)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
to='/logout'
|
||||
icon={require('@tabler/icons/outline/logout.svg')}
|
||||
text={intl.formatMessage(messages.logout)}
|
||||
onClick={onClickLogOut}
|
||||
to='/signup'
|
||||
icon={require('@tabler/icons/outline/user-plus.svg')}
|
||||
text={intl.formatMessage(messages.register)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack space={4}>
|
||||
<button type='button' onClick={handleSwitcherClick} className='py-1'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text tag='span'>
|
||||
<FormattedMessage id='profile_dropdown.switch_account' defaultMessage='Switch accounts' />
|
||||
</Text>
|
||||
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
className={clsx('h-4 w-4 text-gray-900 transition-transform dark:text-gray-100', {
|
||||
'rotate-180': switcher,
|
||||
})}
|
||||
/>
|
||||
</HStack>
|
||||
</button>
|
||||
|
||||
{switcher && (
|
||||
<div className='border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800'>
|
||||
{otherAccounts.map(account => renderAccount(account))}
|
||||
|
||||
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
||||
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/outline/plus.svg')} />
|
||||
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import { Icon, Stack } from 'soapbox/components/ui';
|
||||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import Search from 'soapbox/features/compose/components/search';
|
||||
import ComposeButton from 'soapbox/features/ui/components/compose-button';
|
||||
import ProfileDropdown from 'soapbox/features/ui/components/profile-dropdown';
|
||||
import { useAppSelector, useFeatures, useOwnAccount, useSettings, useInstance } from 'soapbox/hooks';
|
||||
|
||||
import Account from './account';
|
||||
import DropdownMenu, { Menu } from './dropdown-menu';
|
||||
import SidebarNavigationLink from './sidebar-navigation-link';
|
||||
|
||||
@ -142,6 +145,24 @@ const SidebarNavigation = () => {
|
||||
|
||||
return (
|
||||
<Stack space={4}>
|
||||
|
||||
{account && (
|
||||
<Stack space={4}>
|
||||
<div className='relative flex items-center'>
|
||||
<ProfileDropdown account={account}>
|
||||
<Account
|
||||
account={account}
|
||||
action={<Icon src={require('@tabler/icons/outline/chevron-down.svg')} className='text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500' />}
|
||||
disabled
|
||||
/>
|
||||
</ProfileDropdown>
|
||||
</div>
|
||||
<div className='block w-full max-w-xs'>
|
||||
<Search openInRoute autosuggest />
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Stack space={1.5}>
|
||||
<SidebarNavigationLink
|
||||
to='/'
|
||||
@ -231,6 +252,22 @@ const SidebarNavigation = () => {
|
||||
/>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
|
||||
{!account && (
|
||||
<Stack className='xl:hidden' space={1.5}>
|
||||
<SidebarNavigationLink
|
||||
to='/login'
|
||||
icon={require('@tabler/icons/outline/login.svg')}
|
||||
text={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||
/>
|
||||
|
||||
<SidebarNavigationLink
|
||||
to='/signup'
|
||||
icon={require('@tabler/icons/outline/user-plus.svg')}
|
||||
text={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{account && (
|
||||
|
||||
@ -102,7 +102,7 @@ const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedR
|
||||
backHref={backHref}
|
||||
className={clsx({
|
||||
'rounded-t-3xl': !isScrolled && !transparent,
|
||||
'sticky top-0 z-10 bg-white/90 dark:bg-primary-900/90 black:bg-black/80 backdrop-blur lg:top-16': !transparent,
|
||||
'sticky top-0 z-10 bg-white/90 dark:bg-primary-900/90 black:bg-black/80 backdrop-blur': !transparent,
|
||||
'p-4 sm:p-0 sm:pb-4 black:p-4': transparent,
|
||||
'-mt-4 -mx-4 p-4': size !== 'lg' && !transparent,
|
||||
'-mt-4 -mx-4 p-4 sm:-mt-6 sm:-mx-6 sm:p-6': size === 'lg' && !transparent,
|
||||
|
||||
@ -31,7 +31,7 @@ const Layout: LayoutComponent = ({ children }) => (
|
||||
/** Left sidebar container in the UI. */
|
||||
const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
||||
<div className='hidden lg:col-span-3 lg:block'>
|
||||
<StickyBox offsetTop={80} className='pb-4'>
|
||||
<StickyBox offsetTop={16} className='pb-4'>
|
||||
{children}
|
||||
</StickyBox>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
|
||||
/** Right sidebar container in the UI. */
|
||||
const Aside: React.FC<IAside> = ({ children }) => (
|
||||
<aside className='hidden xl:col-span-3 xl:block'>
|
||||
<StickyBox offsetTop={80} className='space-y-6 pb-12'>
|
||||
<StickyBox offsetTop={16} className='space-y-6 pb-12'>
|
||||
<Suspense>
|
||||
{children}
|
||||
</Suspense>
|
||||
|
||||
@ -1,161 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState } 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 { Avatar, Button, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
|
||||
import Search from 'soapbox/features/compose/components/search';
|
||||
import { useAppDispatch, useFeatures, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
|
||||
|
||||
import ProfileDropdown from './profile-dropdown';
|
||||
|
||||
import type { PlfeResponse } from 'soapbox/api';
|
||||
|
||||
const messages = defineMessages({
|
||||
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
|
||||
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' },
|
||||
email: { id: 'navbar.login.email.placeholder', defaultMessage: 'E-mail address' },
|
||||
password: { id: 'navbar.login.password.label', defaultMessage: 'Password' },
|
||||
forgotPassword: { id: 'navbar.login.forgot_password', defaultMessage: 'Forgot password?' },
|
||||
});
|
||||
|
||||
const Navbar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const { account } = useOwnAccount();
|
||||
const node = useRef(null);
|
||||
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [mfaToken, setMfaToken] = useState<boolean>(false);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
dispatch(logIn(username, password) as any)
|
||||
.then(({ access_token }: { access_token: string }) => {
|
||||
setLoading(false);
|
||||
|
||||
return (
|
||||
dispatch(verifyCredentials(access_token) as any)
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()))
|
||||
);
|
||||
})
|
||||
.catch((error: { response: PlfeResponse }) => {
|
||||
setLoading(false);
|
||||
|
||||
const data: any = error.response?.json;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={clsx(
|
||||
'sticky top-0 z-50 hidden border-gray-200 bg-white shadow black:border-b black:border-b-gray-800 black:bg-black lg:block dark:border-gray-800 dark:bg-primary-900',
|
||||
)}
|
||||
ref={node}
|
||||
data-testid='navbar'
|
||||
>
|
||||
<div className='mx-auto max-w-7xl px-8'>
|
||||
<div className='relative flex h-16 justify-between'>
|
||||
<HStack
|
||||
space={4}
|
||||
alignItems='center'
|
||||
className={clsx('enter flex-1 items-stretch', {
|
||||
'justify-center justify-start': account,
|
||||
'justify-start': !account,
|
||||
})}
|
||||
>
|
||||
{account && (
|
||||
<div className='flex flex-1 items-center justify-start pl-0'>
|
||||
<div className='block w-full max-w-xs'>
|
||||
<Search openInRoute autosuggest />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack space={3} alignItems='center'>
|
||||
{account ? (
|
||||
<div className='relative flex items-center'>
|
||||
<ProfileDropdown account={account}>
|
||||
<Avatar src={account.avatar} size={34} />
|
||||
</ProfileDropdown>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Form className='hidden items-center space-x-2 xl:flex rtl:space-x-reverse' onSubmit={handleSubmit}>
|
||||
<Input
|
||||
required
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(features.logInWithUsername ? messages.username : messages.email)}
|
||||
className='max-w-[200px]'
|
||||
/>
|
||||
|
||||
<Input
|
||||
required
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
type='password'
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
className='max-w-[200px]'
|
||||
/>
|
||||
|
||||
<Link to='/reset-password'>
|
||||
<Tooltip text={intl.formatMessage(messages.forgotPassword)}>
|
||||
<IconButton
|
||||
src={require('@tabler/icons/outline/help.svg')}
|
||||
className='cursor-pointer bg-transparent text-gray-400 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-200'
|
||||
iconClassName='h-5 w-5'
|
||||
/>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
>
|
||||
{intl.formatMessage(messages.login)}
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
<div className='space-x-1.5 xl:hidden'>
|
||||
<Button
|
||||
theme='tertiary'
|
||||
size='sm'
|
||||
to='/login'
|
||||
>
|
||||
<FormattedMessage id='account.login' defaultMessage='Log in' />
|
||||
</Button>
|
||||
|
||||
{(isOpen) && (
|
||||
<Button theme='primary' to='/signup' size='sm'>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export { Navbar as default };
|
||||
@ -4,7 +4,7 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { submitAccountNote } from 'soapbox/actions/account-notes';
|
||||
import { HStack, Text, Textarea, Widget } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { Account as AccountEntity } from 'soapbox/schemas';
|
||||
import type { AppDispatch } from 'soapbox/store';
|
||||
@ -27,6 +27,7 @@ interface IAccountNotePanel {
|
||||
const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const textarea = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@ -46,7 +47,7 @@ const AccountNotePanel: React.FC<IAccountNotePanel> = ({ account }) => {
|
||||
setValue(account.relationship?.note);
|
||||
}, [account.relationship?.note]);
|
||||
|
||||
if (!account) {
|
||||
if (!me || !account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -1,35 +1,103 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, switchAccount, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { Button, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useInstance, useRegistrationStatus } from 'soapbox/hooks';
|
||||
import LoginForm from 'soapbox/features/auth-login/components/login-form';
|
||||
import OtpAuthForm from 'soapbox/features/auth-login/components/otp-auth-form';
|
||||
import { useAppDispatch, useAppSelector, useInstance, useRegistrationStatus } from 'soapbox/hooks';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
|
||||
import type { PlfeResponse } from 'soapbox/api';
|
||||
|
||||
|
||||
const SignUpPanel = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const instance = useInstance();
|
||||
const { isOpen } = useRegistrationStatus();
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
if (me || !isOpen) return null;
|
||||
const token = new URLSearchParams(window.location.search).get('token');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [mfaAuthNeeded, setMfaAuthNeeded] = useState(!!token);
|
||||
const [mfaToken, setMfaToken] = useState(token || '');
|
||||
const [shouldRedirect, setShouldRedirect] = useState(false);
|
||||
|
||||
const getFormData = (form: HTMLFormElement) =>
|
||||
Object.fromEntries(
|
||||
Array.from(form).map((i: any) => [i.name, i.value]),
|
||||
);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
const { username, password } = getFormData(event.target as HTMLFormElement);
|
||||
dispatch(logIn(username, password))
|
||||
.then(({ access_token }) => dispatch(verifyCredentials(access_token as string)))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(async (account) => {
|
||||
await dispatch(fetchInstance());
|
||||
return account;
|
||||
})
|
||||
.then((account: { id: string }) => {
|
||||
if (typeof me === 'string') {
|
||||
dispatch(switchAccount(account.id));
|
||||
} else {
|
||||
setShouldRedirect(true);
|
||||
}
|
||||
}).catch((error: { response: PlfeResponse }) => {
|
||||
const data: any = error.response?.json;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaAuthNeeded(true);
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
setIsLoading(true);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
if (shouldRedirect) {
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
if (mfaAuthNeeded) return <OtpAuthForm mfa_token={mfaToken} />;
|
||||
|
||||
if (me) return null;
|
||||
|
||||
return (
|
||||
<Stack space={2} data-testid='sign-up-panel'>
|
||||
{isOpen && (
|
||||
<>
|
||||
<Stack>
|
||||
<Text size='lg' weight='bold'>
|
||||
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='signup_panel.subtitle' defaultMessage="Sign up now to discuss what's happening." />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
theme='primary'
|
||||
to='/signup'
|
||||
block
|
||||
>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Stack>
|
||||
<Text size='lg' weight='bold'>
|
||||
<FormattedMessage id='signup_panel.title' defaultMessage='New to {site_title}?' values={{ site_title: instance.title }} />
|
||||
</Text>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='signup_panel.subtitle' defaultMessage="Sign up now to discuss what's happening." />
|
||||
<FormattedMessage id='signup_panel.sign_in.title' defaultMessage='Already have an account?' />
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
theme='primary'
|
||||
to='/signup'
|
||||
block
|
||||
>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
<LoginForm handleSubmit={handleSubmit} isLoading={isLoading} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@ -38,7 +38,6 @@ import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
import BackgroundShapes from './components/background-shapes';
|
||||
import FloatingActionButton from './components/floating-action-button';
|
||||
import Navbar from './components/navbar';
|
||||
import {
|
||||
Status,
|
||||
CommunityTimeline,
|
||||
@ -470,8 +469,6 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||
<BackgroundShapes />
|
||||
|
||||
<div className='z-10 flex min-h-screen flex-col'>
|
||||
<Navbar />
|
||||
|
||||
<Layout>
|
||||
<Layout.Sidebar>
|
||||
{!standalone && <SidebarNavigation />}
|
||||
@ -488,11 +485,9 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{me && (
|
||||
<Suspense>
|
||||
<SidebarMenu />
|
||||
</Suspense>
|
||||
)}
|
||||
<Suspense>
|
||||
<SidebarMenu />
|
||||
</Suspense>
|
||||
|
||||
{me && features.chats && (
|
||||
<div className='hidden xl:block'>
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"account.posts_with_replies": "Wpisy i odpowiedzi",
|
||||
"account.profile": "Profil",
|
||||
"account.profile_external": "Wyświetl profil na {domain}",
|
||||
"account.register": "Rejestracja",
|
||||
"account.register": "Zarejestruj się",
|
||||
"account.remote_follow": "Obserwuj zdalnie",
|
||||
"account.remove_from_followers": "Usuń tego obserwującego",
|
||||
"account.report": "Zgłoś @{name}",
|
||||
@ -1344,7 +1344,7 @@
|
||||
"settings.settings": "Ustawienia",
|
||||
"shared.tos": "Zasady użytkowania",
|
||||
"signup_panel.subtitle": "Zarejestruj się, aby przyłączyć się do dyskusji.",
|
||||
"signup_panel.title": "Nowi na {site_title}?",
|
||||
"signup_panel.title": "Nowy(-a) na {site_title}?",
|
||||
"site_preview.preview": "Podgląd",
|
||||
"soapbox_config.authenticated_profile_hint": "Użytkownicy muszą być zalogowani, aby zobaczyć odpowiedzi i media na profilach użytkowników.",
|
||||
"soapbox_config.authenticated_profile_label": "Profile wymagają uwierzytelniania",
|
||||
|
||||
Reference in New Issue
Block a user