Files
remark42/frontend/app/components/auth/auth.tsx
T
Pavel MineevandUmputun 93fd445bd4 On-demand auth
* auth block was moved to comment form
* if comments on page read only auth form shows in old place
* comment value in form will be saved between refreshes (it is side effect form saving comment value between unauth and auth states)
2020-03-12 15:44:31 -05:00

199 lines
6.2 KiB
TypeScript

/** @jsx createElement */
import { createElement, Component, createRef } from 'preact';
import { useCallback } from 'preact/hooks';
import { IntlShape, FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { AuthProvider, Theme, User } from '@app/common/types';
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE } from '@app/common/constants';
import { getHandleClickProps } from '@app/common/accessibility';
import { Button } from '@app/components/button';
import { Dropdown, DropdownItem } from '@app/components/dropdown';
import debounce from '@app/utils/debounce';
import { ProviderState } from '@app/store/provider/reducers';
import { StaticStore } from '@app/common/static_store';
import { useSelector, useDispatch } from 'react-redux';
import { StoreState } from '@app/store';
import useTheme from '@app/hooks/useTheme';
import { logIn } from '@app/store/user/actions';
import { AnonymousLoginForm } from './__anonymous-login-form';
import { EmailLoginFormConnected, EmailLoginFormRef } from './__email-login-form';
import styles from './auth.module.pcss';
interface Props {
intl: IntlShape;
theme: Theme;
onSignIn(provider: AuthProvider): any; // eslint-disable-line
user: User | null;
provider: ProviderState;
}
interface State {
threshold: number;
}
class Auth extends Component<Props, State> {
emailLoginRef = createRef<EmailLoginFormRef>();
singInMessageAndSortWidth = 255;
state = {
threshold: 3,
};
componentWillMount() {
this.resizeHandler();
window.addEventListener('resize', this.resizeHandler);
}
componentWillUnmount() {
window.removeEventListener('resize', this.resizeHandler);
}
resizeHandler = debounce(() => {
this.setState({
threshold: Math.max(3, Math.round((window.innerWidth - this.singInMessageAndSortWidth) / 80)),
});
}, 100);
onEmailTitleClick = () => {
this.emailLoginRef.current && this.emailLoginRef.current.focus();
};
onEmailSignIn = (token: string) => {
return this.props.onSignIn({ name: 'email', token });
};
handleOAuthLogin = async (e: MouseEvent | KeyboardEvent) => {
const name = (e.target as HTMLButtonElement).dataset.provider! as AuthProvider['name'];
this.props.onSignIn({ name } as AuthProvider);
};
handleAnonymousLoginFormSubmut = async (username: string) => {
this.props.onSignIn({ name: 'anonymous', username });
};
renderOther = (providers: AuthProvider['name'][]) => {
const other = this.props.intl.formatMessage(authPanelMessages.otherProvider);
return (
<Dropdown title={other} theme={this.props.theme} onTitleClick={this.onEmailTitleClick}>
{providers.map(provider => (
<DropdownItem>{this.renderProvider(provider)}</DropdownItem>
))}
</Dropdown>
);
};
renderProvider = (provider: AuthProvider['name']) => {
if (provider === 'anonymous') {
const anonymous = this.props.intl.formatMessage(authPanelMessages.anonymousProvider);
return (
<Dropdown title={anonymous} theme={this.props.theme}>
<DropdownItem>
<AnonymousLoginForm
onSubmit={this.handleAnonymousLoginFormSubmut}
theme={this.props.theme}
intl={this.props.intl}
/>
</DropdownItem>
</Dropdown>
);
}
if (provider === 'email') {
return (
<Dropdown title={PROVIDER_NAMES['email']} theme={this.props.theme} onTitleClick={this.onEmailTitleClick}>
<DropdownItem>
<EmailLoginFormConnected ref={this.emailLoginRef} onSignIn={this.onEmailSignIn} theme={this.props.theme} />
</DropdownItem>
</Dropdown>
);
}
return (
<Button kind="link" data-provider={provider} {...getHandleClickProps(this.handleOAuthLogin)} role="link">
{PROVIDER_NAMES[provider]}
</Button>
);
};
render({ intl }: Props, { threshold }: State) {
if (!IS_STORAGE_AVAILABLE) return null;
const sortedProviders = ((providers): typeof providers => {
if (!this.props.provider.name) return providers;
const lastProviderIndex = providers.indexOf(this.props.provider.name as typeof providers[0]);
if (lastProviderIndex < 1) return providers;
return [
this.props.provider.name as typeof providers[0],
...providers.slice(0, lastProviderIndex),
...providers.slice(lastProviderIndex + 1),
];
})(StaticStore.config.auth_providers);
const isAboveThreshold = sortedProviders.length > threshold;
const or = intl.formatMessage(authPanelMessages.orProvider);
return (
<div className={styles.auth}>
<FormattedMessage id="authPanel.login" defaultMessage="Login:" />{' '}
{!isAboveThreshold &&
sortedProviders.map((provider, i) => {
const comma = i === 0 ? '' : i === sortedProviders.length - 1 ? ` ${or} ` : ', ';
return (
<span>
{comma}
{this.renderProvider(provider)}
</span>
);
})}
{isAboveThreshold &&
sortedProviders.slice(0, threshold - 1).map((provider, i) => {
const comma = i === 0 ? '' : ', ';
return (
<span>
{comma}
{this.renderProvider(provider)}
</span>
);
})}
{isAboveThreshold && (
<span>
{` ${or} `}
{this.renderOther(sortedProviders.slice(threshold - 1))}
</span>
)}
</div>
);
}
}
const authPanelMessages = defineMessages({
otherProvider: {
id: 'authPanel.other-provider',
defaultMessage: 'Other',
},
anonymousProvider: {
id: 'authPanel.anonymous-provider',
defaultMessage: 'Anonymous',
},
orProvider: {
id: 'authPanel.or-provider',
defaultMessage: 'or',
},
});
export default function() {
const dispatch = useDispatch();
const provider = useSelector<StoreState, ProviderState>(store => store.provider);
const user = useSelector<StoreState, User | null>(store => store.user);
const theme = useTheme();
const intl = useIntl();
const handleSignin = useCallback((provider: AuthProvider) => dispatch(logIn(provider)), []);
return <Auth provider={provider} theme={theme} onSignIn={handleSignin} intl={intl} user={user} />;
}