Merge branch 'next-public-layout' into 'next'
Next: improve public pages, feature detection, etc See merge request soapbox-pub/soapbox-fe!1243
This commit is contained in:
commit
9d03b8ab93
|
@ -249,10 +249,12 @@ export function logOut(intl) {
|
|||
const account = getLoggedInAccount(state);
|
||||
const standalone = isStandalone(state);
|
||||
|
||||
if (!account) return dispatch(noOp);
|
||||
|
||||
const params = {
|
||||
client_id: state.getIn(['auth', 'app', 'client_id']),
|
||||
client_secret: state.getIn(['auth', 'app', 'client_secret']),
|
||||
token: state.getIn(['auth', 'users', account.get('url'), 'access_token']),
|
||||
token: state.getIn(['auth', 'users', account.url, 'access_token']),
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
|
|
|
@ -42,7 +42,7 @@ function createExternalApp(instance, baseURL) {
|
|||
|
||||
const params = {
|
||||
client_name: sourceCode.displayName,
|
||||
redirect_uris: `${window.location.origin}/auth/external`,
|
||||
redirect_uris: `${window.location.origin}/login/external`,
|
||||
website: sourceCode.homepage,
|
||||
scopes,
|
||||
};
|
||||
|
|
|
@ -47,21 +47,34 @@ export function rememberSoapboxConfig(host) {
|
|||
};
|
||||
}
|
||||
|
||||
export function fetchSoapboxConfig(host) {
|
||||
export function fetchFrontendConfigurations() {
|
||||
return (dispatch, getState) => {
|
||||
api(getState).get('/api/pleroma/frontend_configurations').then(response => {
|
||||
if (response.data.soapbox_fe) {
|
||||
dispatch(importSoapboxConfig(response.data.soapbox_fe, host));
|
||||
} else {
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
});
|
||||
return api(getState)
|
||||
.get('/api/pleroma/frontend_configurations')
|
||||
.then(({ data }) => data);
|
||||
};
|
||||
}
|
||||
|
||||
// Tries to remember the config from browser storage before fetching it
|
||||
/** Conditionally fetches Soapbox config depending on backend features */
|
||||
export function fetchSoapboxConfig(host) {
|
||||
return (dispatch, getState) => {
|
||||
const features = getFeatures(getState().instance);
|
||||
|
||||
if (features.frontendConfigurations) {
|
||||
return dispatch(fetchFrontendConfigurations()).then(data => {
|
||||
if (data.soapbox_fe) {
|
||||
dispatch(importSoapboxConfig(data.soapbox_fe, host));
|
||||
} else {
|
||||
dispatch(fetchSoapboxJson(host));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return dispatch(fetchSoapboxJson(host));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Tries to remember the config from browser storage before fetching it */
|
||||
export function loadSoapboxConfig() {
|
||||
return (dispatch, getState) => {
|
||||
const host = getHost(getState());
|
||||
|
|
|
@ -1,43 +1,58 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { NODE_ENV } from 'soapbox/build_config';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import BuildConfig from 'soapbox/build_config';
|
||||
import { Text, Stack } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { captureException } from 'soapbox/monitoring';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
import { getSoapboxConfig } from '../actions/soapbox';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
const goHome = () => location.href = '/';
|
||||
|
||||
/** Unregister the ServiceWorker */
|
||||
// https://stackoverflow.com/a/49771828/8811886
|
||||
const unregisterSw = async() => {
|
||||
if (!navigator.serviceWorker) return;
|
||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||
const unregisterAll = registrations.map(r => r.unregister());
|
||||
await Promise.all(unregisterAll);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: RootState) => {
|
||||
const { links, logo } = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
siteTitle: state.instance.title,
|
||||
helpLink: soapboxConfig.getIn(['links', 'help']),
|
||||
supportLink: soapboxConfig.getIn(['links', 'support']),
|
||||
statusLink: soapboxConfig.getIn(['links', 'status']),
|
||||
logo,
|
||||
links,
|
||||
};
|
||||
};
|
||||
|
||||
@connect(mapStateToProps)
|
||||
class ErrorBoundary extends React.PureComponent {
|
||||
type Props = ReturnType<typeof mapStateToProps>;
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
siteTitle: PropTypes.string,
|
||||
supportLink: PropTypes.string,
|
||||
helpLink: PropTypes.string,
|
||||
statusLink: PropTypes.string,
|
||||
};
|
||||
type State = {
|
||||
hasError: boolean,
|
||||
error: any,
|
||||
componentStack: any,
|
||||
browser?: Bowser.Parser.Parser,
|
||||
}
|
||||
|
||||
state = {
|
||||
class ErrorBoundary extends React.PureComponent<Props, State> {
|
||||
|
||||
state: State = {
|
||||
hasError: false,
|
||||
error: undefined,
|
||||
componentStack: undefined,
|
||||
browser: undefined,
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
componentDidCatch(error: any, info: any): void {
|
||||
captureException(error);
|
||||
|
||||
this.setState({
|
||||
|
@ -55,11 +70,11 @@ class ErrorBoundary extends React.PureComponent {
|
|||
.catch(() => {});
|
||||
}
|
||||
|
||||
setTextareaRef = c => {
|
||||
setTextareaRef: React.RefCallback<HTMLTextAreaElement> = c => {
|
||||
this.textarea = c;
|
||||
}
|
||||
|
||||
handleCopy = e => {
|
||||
handleCopy: React.MouseEventHandler = () => {
|
||||
if (!this.textarea) return;
|
||||
|
||||
this.textarea.select();
|
||||
|
@ -68,25 +83,30 @@ class ErrorBoundary extends React.PureComponent {
|
|||
document.execCommand('copy');
|
||||
}
|
||||
|
||||
getErrorText = () => {
|
||||
getErrorText = (): string => {
|
||||
const { error, componentStack } = this.state;
|
||||
return error + componentStack;
|
||||
}
|
||||
|
||||
clearCookies = e => {
|
||||
clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
e.preventDefault();
|
||||
unregisterSw().then(goHome).catch(goHome);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { browser, hasError } = this.state;
|
||||
const { children, siteTitle, helpLink, statusLink, supportLink } = this.props;
|
||||
const { children, siteTitle, logo, links } = this.props;
|
||||
|
||||
if (!hasError) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
const isProduction = BuildConfig.NODE_ENV === 'production';
|
||||
|
||||
const errorText = this.getErrorText();
|
||||
|
||||
|
@ -95,7 +115,11 @@ class ErrorBoundary extends React.PureComponent {
|
|||
<main className='flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex-shrink-0 flex justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<img className='h-12 w-12' src='/instance/images/app-icon.png' alt={siteTitle} />
|
||||
{logo ? (
|
||||
<img className='h-12 w-12' src={logo} alt={siteTitle} />
|
||||
) : (
|
||||
<SvgIcon className='h-12 w-12' src={require('@tabler/icons/icons/home.svg')} alt={siteTitle} />
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -105,14 +129,18 @@ class ErrorBoundary extends React.PureComponent {
|
|||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-500'>
|
||||
We're sorry for the interruption. If the problem persists, please reach out to our support team. You
|
||||
may also try to <a href='/' onClick={this.clearCookies} className='text-gray-700 hover:underline'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
{' ' }(this will log you out).
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{ clearCookies: (
|
||||
<a href='/' onClick={this.clearCookies} className='text-gray-700 hover:underline'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
) }}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Text theme='muted'>
|
||||
|
@ -144,7 +172,7 @@ class ErrorBoundary extends React.PureComponent {
|
|||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'>Browser</Text>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
|
@ -155,28 +183,28 @@ class ErrorBoundary extends React.PureComponent {
|
|||
|
||||
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<nav className='flex justify-center space-x-4'>
|
||||
{statusLink && (
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={statusLink} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
Status
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{helpLink && (
|
||||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={helpLink} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
Help Center
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{supportLink && (
|
||||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={supportLink} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
Support
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-500 hover:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
@ -188,4 +216,4 @@ class ErrorBoundary extends React.PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
export default connect(mapStateToProps)(ErrorBoundary as any);
|
|
@ -0,0 +1,28 @@
|
|||
import lottie from 'lottie-web';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
interface LottieProps {
|
||||
animationData: any
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
/** Wrapper around lottie-web */
|
||||
// https://github.com/chenqingspring/react-lottie/issues/139
|
||||
const Lottie: React.FC<LottieProps> = ({ animationData, width, height }) => {
|
||||
const element = useRef<HTMLDivElement>(null);
|
||||
const lottieInstance = useRef<any>();
|
||||
|
||||
useEffect(() => {
|
||||
if (element.current) {
|
||||
lottieInstance.current = lottie.loadAnimation({
|
||||
animationData,
|
||||
container: element.current,
|
||||
});
|
||||
}
|
||||
}, [animationData]);
|
||||
|
||||
return <div style={{ width, height }} ref={element} />;
|
||||
};
|
||||
|
||||
export default Lottie;
|
|
@ -285,7 +285,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
<hr />
|
||||
|
||||
<SidebarLink
|
||||
to='/auth/sign_out'
|
||||
to='/logout'
|
||||
icon={require('@tabler/icons/icons/logout.svg')}
|
||||
text={intl.formatMessage(messages.logout)}
|
||||
onClick={onClickLogOut}
|
||||
|
|
|
@ -2,6 +2,7 @@ import * as React from 'react';
|
|||
|
||||
interface IForm {
|
||||
onSubmit?: (event: React.FormEvent) => void,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
const Form: React.FC<IForm> = ({ onSubmit, children, ...filteredProps }) => {
|
||||
|
|
|
@ -11,7 +11,7 @@ const messages = defineMessages({
|
|||
hidePassword: { id: 'input.password.hide_password', defaultMessage: 'Hide password' },
|
||||
});
|
||||
|
||||
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type'> {
|
||||
interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'type' | 'autoComplete' | 'autoCorrect' | 'autoCapitalize' | 'required'> {
|
||||
autoFocus?: boolean,
|
||||
defaultValue?: string,
|
||||
className?: string,
|
||||
|
@ -20,7 +20,7 @@ interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onCh
|
|||
placeholder?: string,
|
||||
value?: string,
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
type: 'text' | 'email' | 'tel' | 'password'
|
||||
type: 'text' | 'email' | 'tel' | 'password',
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, IInput>(
|
||||
|
|
|
@ -24,6 +24,7 @@ import WaitlistPage from 'soapbox/features/verification/waitlist_page';
|
|||
import { createGlobals } from 'soapbox/globals';
|
||||
import messages from 'soapbox/locales/messages';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
import SoapboxPropTypes from 'soapbox/utils/soapbox_prop_types';
|
||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||
|
||||
|
@ -36,30 +37,34 @@ import { store } from '../store';
|
|||
|
||||
const validLocale = locale => Object.keys(messages).includes(locale);
|
||||
|
||||
// Delay rendering until instance has loaded or failed (for feature detection)
|
||||
const isInstanceLoaded = state => {
|
||||
const v = state.getIn(['instance', 'version'], '0.0.0');
|
||||
const fetchFailed = state.getIn(['meta', 'instance_fetch_failed'], false);
|
||||
|
||||
return v !== '0.0.0' || fetchFailed;
|
||||
};
|
||||
|
||||
// Configure global functions for developers
|
||||
createGlobals(store);
|
||||
|
||||
// Preload happens synchronously
|
||||
store.dispatch(preload());
|
||||
|
||||
store.dispatch(fetchMe())
|
||||
.then(account => {
|
||||
// Postpone for authenticated fetch
|
||||
store.dispatch(loadInstance());
|
||||
store.dispatch(loadSoapboxConfig());
|
||||
/** Load initial data from the backend */
|
||||
const loadInitial = () => {
|
||||
return async(dispatch, getState) => {
|
||||
// Await for authenticated fetch
|
||||
await dispatch(fetchMe());
|
||||
// Await for feature detection
|
||||
await dispatch(loadInstance());
|
||||
|
||||
if (!account) {
|
||||
store.dispatch(fetchVerificationConfig());
|
||||
const promises = [];
|
||||
|
||||
promises.push(dispatch(loadSoapboxConfig()));
|
||||
|
||||
const state = getState();
|
||||
const features = getFeatures(state.instance);
|
||||
|
||||
if (features.pepe && !state.me) {
|
||||
promises.push(dispatch(fetchVerificationConfig()));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
};
|
||||
|
||||
const makeAccount = makeGetAccount();
|
||||
|
||||
|
@ -77,7 +82,6 @@ const mapStateToProps = (state) => {
|
|||
showIntroduction,
|
||||
me,
|
||||
account,
|
||||
instanceLoaded: isInstanceLoaded(state),
|
||||
reduceMotion: settings.get('reduceMotion'),
|
||||
underlineLinks: settings.get('underlineLinks'),
|
||||
systemFont: settings.get('systemFont'),
|
||||
|
@ -99,7 +103,6 @@ class SoapboxMount extends React.PureComponent {
|
|||
showIntroduction: PropTypes.bool,
|
||||
me: SoapboxPropTypes.me,
|
||||
account: ImmutablePropTypes.record,
|
||||
instanceLoaded: PropTypes.bool,
|
||||
reduceMotion: PropTypes.bool,
|
||||
underlineLinks: PropTypes.bool,
|
||||
systemFont: PropTypes.bool,
|
||||
|
@ -117,6 +120,7 @@ class SoapboxMount extends React.PureComponent {
|
|||
state = {
|
||||
messages: {},
|
||||
localeLoading: true,
|
||||
isLoaded: false,
|
||||
}
|
||||
|
||||
setMessages = () => {
|
||||
|
@ -133,6 +137,12 @@ class SoapboxMount extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.setMessages();
|
||||
|
||||
this.props.dispatch(loadInitial()).then(() => {
|
||||
this.setState({ isLoaded: true });
|
||||
}).catch(() => {
|
||||
this.setState({ isLoaded: false });
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -144,21 +154,14 @@ class SoapboxMount extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { me, account, instanceLoaded, themeCss, locale, singleUserMode } = this.props;
|
||||
const { me, account, themeCss, locale, singleUserMode } = this.props;
|
||||
if (me === null) return null;
|
||||
if (me && !account) return null;
|
||||
if (!instanceLoaded) return null;
|
||||
if (!this.state.isLoaded) return null;
|
||||
if (this.state.localeLoading) return null;
|
||||
|
||||
const waitlisted = account && !account.getIn(['source', 'approved'], true);
|
||||
|
||||
// Disabling introduction for launch
|
||||
// const { showIntroduction } = this.props;
|
||||
//
|
||||
// if (showIntroduction) {
|
||||
// return <Introduction />;
|
||||
// }
|
||||
|
||||
const bodyClass = classNames('bg-white dark:bg-slate-900 text-base', {
|
||||
'no-reduce-motion': !this.props.reduceMotion,
|
||||
'underline-links': this.props.underlineLinks,
|
||||
|
@ -201,10 +204,6 @@ class SoapboxMount extends React.PureComponent {
|
|||
<Route path='/reset-password' component={AuthLayout} />
|
||||
<Route path='/edit-password' component={AuthLayout} />
|
||||
|
||||
<Redirect from='/auth/reset_password' to='/reset-password' />
|
||||
<Redirect from='/auth/edit_password' to='/edit-password' />
|
||||
<Redirect from='/auth/sign_in' to='/login' />
|
||||
|
||||
<Route path='/' component={UI} />
|
||||
</Switch>
|
||||
</ScrollContext>
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import { Card, CardBody } from '../../components/ui';
|
||||
import LoginPage from '../auth_login/components/login_page';
|
||||
import PasswordReset from '../auth_login/components/password_reset';
|
||||
import PasswordResetConfirm from '../auth_login/components/password_reset_confirm';
|
||||
// import EmailConfirmation from '../email_confirmation';
|
||||
import Verification from '../verification';
|
||||
import EmailPassthru from '../verification/email_passthru';
|
||||
|
||||
const AuthLayout = () => (
|
||||
<div>
|
||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-cyan-50' />
|
||||
|
||||
<main className='relative flex flex-col h-screen'>
|
||||
<header className='pt-10 flex justify-center relative'>
|
||||
<Link to='/' className='cursor-pointer'>
|
||||
<img src='/instance/images/truth-logo.svg' alt='Logo' className='h-7' />
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className='-mt-10 flex flex-col justify-center items-center h-full'>
|
||||
<div className='sm:mx-auto w-full sm:max-w-lg md:max-w-2xl'>
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Switch>
|
||||
<Route exact path='/auth/verify' component={Verification} />
|
||||
<Route exact path='/auth/verify/email/:token' component={EmailPassthru} />
|
||||
<Route exact path='/login' component={LoginPage} />
|
||||
<Route exact path='/reset-password' component={PasswordReset} />
|
||||
<Route exact path='/edit-password' component={PasswordResetConfirm} />
|
||||
{/* <Route exact path='/auth/confirmation' component={EmailConfirmation} /> */}
|
||||
|
||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||
<Redirect from='/auth/password/edit' to='/edit-password' />
|
||||
</Switch>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<BundleContainer fetchComponent={NotificationsContainer}>
|
||||
{(Component) => <Component />}
|
||||
</BundleContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AuthLayout;
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
import { NotificationsContainer } from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
import { Card, CardBody } from '../../components/ui';
|
||||
import LoginPage from '../auth_login/components/login_page';
|
||||
import PasswordReset from '../auth_login/components/password_reset';
|
||||
import PasswordResetConfirm from '../auth_login/components/password_reset_confirm';
|
||||
// import EmailConfirmation from '../email_confirmation';
|
||||
import Verification from '../verification';
|
||||
import EmailPassthru from '../verification/email_passthru';
|
||||
|
||||
const AuthLayout = () => {
|
||||
const { logo } = useSoapboxConfig();
|
||||
const siteTitle = useAppSelector(state => state.instance.title);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-cyan-50' />
|
||||
|
||||
<main className='relative flex flex-col h-screen'>
|
||||
<header className='pt-10 flex justify-center relative'>
|
||||
<Link to='/' className='cursor-pointer'>
|
||||
{logo ? (
|
||||
<img src={logo} alt={siteTitle} className='h-7' />
|
||||
) : (
|
||||
<SvgIcon
|
||||
className='w-7 h-7'
|
||||
alt={siteTitle}
|
||||
src={require('@tabler/icons/icons/home.svg')}
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
<div className='-mt-10 flex flex-col justify-center items-center h-full'>
|
||||
<div className='sm:mx-auto w-full sm:max-w-lg md:max-w-2xl'>
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Switch>
|
||||
<Route exact path='/auth/verify' component={Verification} />
|
||||
<Route exact path='/auth/verify/email/:token' component={EmailPassthru} />
|
||||
<Route exact path='/login' component={LoginPage} />
|
||||
<Route exact path='/reset-password' component={PasswordReset} />
|
||||
<Route exact path='/edit-password' component={PasswordResetConfirm} />
|
||||
{/* <Route exact path='/auth/confirmation' component={EmailConfirmation} /> */}
|
||||
|
||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||
<Redirect from='/auth/password/edit' to='/edit-password' />
|
||||
</Switch>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<BundleContainer fetchComponent={NotificationsContainer}>
|
||||
{(Component) => <Component />}
|
||||
</BundleContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthLayout;
|
|
@ -1,18 +1,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Button, Form, FormActions, FormGroup, Input } from '../../../components/ui';
|
||||
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
username: {
|
||||
id: 'login.fields.username_placeholder',
|
||||
defaultMessage: 'Username',
|
||||
},
|
||||
email: {
|
||||
id: 'login.fields.email_placeholder',
|
||||
defaultMessage: 'Email address',
|
||||
id: 'login.fields.username_label',
|
||||
defaultMessage: 'Email or username',
|
||||
},
|
||||
password: {
|
||||
id: 'login.fields.password_placeholder',
|
||||
|
@ -20,7 +15,12 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
const LoginForm = ({ isLoading, handleSubmit }) => {
|
||||
interface ILoginForm {
|
||||
isLoading: boolean,
|
||||
handleSubmit: React.FormEventHandler,
|
||||
}
|
||||
|
||||
const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
|
@ -31,10 +31,10 @@ const LoginForm = ({ isLoading, handleSubmit }) => {
|
|||
|
||||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.email)}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.username)}>
|
||||
<Input
|
||||
aria-label={intl.formatMessage(messages.email)}
|
||||
placeholder={intl.formatMessage(messages.email)}
|
||||
aria-label={intl.formatMessage(messages.username)}
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
type='text'
|
||||
name='username'
|
||||
autoComplete='off'
|
||||
|
@ -82,9 +82,4 @@ const LoginForm = ({ isLoading, handleSubmit }) => {
|
|||
);
|
||||
};
|
||||
|
||||
LoginForm.propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default LoginForm;
|
|
@ -76,7 +76,7 @@ class LoginPage extends ImmutablePureComponent {
|
|||
const { standalone } = this.props;
|
||||
const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state;
|
||||
|
||||
if (standalone) return <Redirect to='/auth/external' />;
|
||||
if (standalone) return <Redirect to='/login/external' />;
|
||||
|
||||
if (shouldRedirect) return <Redirect to='/' />;
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logOut } from 'soapbox/actions/auth';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
|
||||
/** Component that logs the user out when rendered */
|
||||
const Logout: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(logOut(intl) as any)
|
||||
.then(() => setDone(true))
|
||||
.catch(console.warn);
|
||||
});
|
||||
|
||||
if (done) {
|
||||
return <Redirect to='/' />;
|
||||
} else {
|
||||
return <Spinner />;
|
||||
}
|
||||
};
|
||||
|
||||
export default Logout;
|
|
@ -39,7 +39,7 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20
|
|||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' },
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Truth' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
message: { id: 'compose_form.message', defaultMessage: 'Message' },
|
||||
schedule: { id: 'compose_form.schedule', defaultMessage: 'Schedule' },
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth';
|
||||
import { Button, Form, FormActions, FormGroup, Input, Spinner } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' },
|
||||
instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' },
|
||||
});
|
||||
|
||||
/** Form for logging into a remote instance */
|
||||
const ExternalLoginForm: React.FC = () => {
|
||||
const code = new URLSearchParams(window.location.search).get('code');
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [host, setHost] = useState('');
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
const handleHostChange: React.ChangeEventHandler<HTMLInputElement> = ({ currentTarget }) => {
|
||||
setHost(currentTarget.value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setLoading(true);
|
||||
|
||||
dispatch(externalLogin(host) as any)
|
||||
.then(() => setLoading(false))
|
||||
.catch(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
dispatch(loginWithCode(code));
|
||||
}
|
||||
});
|
||||
|
||||
if (code) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText={intl.formatMessage(messages.instanceLabel)}>
|
||||
<Input
|
||||
aria-label={intl.formatMessage(messages.instancePlaceholder)}
|
||||
placeholder={intl.formatMessage(messages.instancePlaceholder)}
|
||||
type='text'
|
||||
name='host'
|
||||
onChange={handleHostChange}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='primary' type='submit' disabled={isLoading}>
|
||||
<FormattedMessage id='login.log_in' defaultMessage='Log in' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExternalLoginForm;
|
|
@ -1,82 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { externalLogin, loginWithCode } from 'soapbox/actions/external_auth';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { SimpleForm, FieldsGroup, TextInput } from 'soapbox/features/forms';
|
||||
|
||||
const messages = defineMessages({
|
||||
instanceLabel: { id: 'login.fields.instance_label', defaultMessage: 'Instance' },
|
||||
instancePlaceholder: { id: 'login.fields.instance_placeholder', defaultMessage: 'example.com' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class ExternalLoginForm extends ImmutablePureComponent {
|
||||
|
||||
state = {
|
||||
host: '',
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
handleHostChange = ({ target }) => {
|
||||
this.setState({ host: target.value });
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
const { dispatch } = this.props;
|
||||
const { host } = this.state;
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
dispatch(externalLogin(host))
|
||||
.then(() => this.setState({ isLoading: false }))
|
||||
.catch(() => this.setState({ isLoading: false }));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const code = new URLSearchParams(window.location.search).get('code');
|
||||
|
||||
if (code) {
|
||||
this.setState({ code });
|
||||
this.props.dispatch(loginWithCode(code));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
const { isLoading, code } = this.state;
|
||||
|
||||
if (code) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SimpleForm onSubmit={this.handleSubmit} className='external-login'>
|
||||
<fieldset disabled={isLoading}>
|
||||
<FieldsGroup>
|
||||
<TextInput
|
||||
label={intl.formatMessage(messages.instanceLabel)}
|
||||
placeholder={intl.formatMessage(messages.instancePlaceholder)}
|
||||
name='host'
|
||||
value={this.state.host}
|
||||
onChange={this.handleHostChange}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
required
|
||||
/>
|
||||
</FieldsGroup>
|
||||
</fieldset>
|
||||
<div className='actions'>
|
||||
<button name='button' type='submit' className='btn button button-primary'>
|
||||
<FormattedMessage id='login.log_in' defaultMessage='Log in' />
|
||||
</button>
|
||||
</div>
|
||||
</SimpleForm>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import ExternalLoginForm from './components/external_login_form';
|
||||
|
||||
export default class ExternalLoginPage extends ImmutablePureComponent {
|
||||
|
||||
render() {
|
||||
return <ExternalLoginForm />;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
import ExternalLoginForm from './components/external-login-form';
|
||||
|
||||
/** Page for logging into a remote instance */
|
||||
const ExternalLoginPage: React.FC = () => {
|
||||
return <ExternalLoginForm />;
|
||||
};
|
||||
|
||||
export default ExternalLoginPage;
|
|
@ -68,8 +68,8 @@ const LandingPage = () => {
|
|||
return (
|
||||
<main className='mt-16 sm:mt-24'>
|
||||
<div className='mx-auto max-w-7xl'>
|
||||
<div className='lg:grid lg:grid-cols-12 lg:gap-8 items-center py-24'>
|
||||
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex lg:items-center'>
|
||||
<div className='lg:grid lg:grid-cols-12 lg:gap-8 py-12'>
|
||||
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex'>
|
||||
<div>
|
||||
<Stack space={3}>
|
||||
<h1 className='text-5xl font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-pink-600 via-primary-500 to-blue-600 sm:mt-5 sm:leading-none lg:mt-6 lg:text-6xl xl:text-7xl'>
|
||||
|
@ -81,7 +81,7 @@ const LandingPage = () => {
|
|||
</Stack>
|
||||
</div>
|
||||
</div>
|
||||
<div className='hidden lg:block sm:mt-24 lg:mt-0 lg:col-span-6'>
|
||||
<div className='hidden lg:block sm:mt-24 lg:mt-0 lg:col-span-6 self-center'>
|
||||
<Card size='xl' variant='rounded' className='sm:max-w-md sm:w-full sm:mx-auto'>
|
||||
<CardBody>
|
||||
{renderBody()}
|
||||
|
|
|
@ -112,7 +112,7 @@ const Preferences = () => {
|
|||
<Form>
|
||||
<List>
|
||||
<ListItem
|
||||
label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reTRUTHs' />}
|
||||
label={<FormattedMessage id='home.column_settings.show_reblogs' defaultMessage='Show reposts' />}
|
||||
hint={<FormattedMessage id='preferences.hints.feed' defaultMessage='In your home feed' />}
|
||||
>
|
||||
<SettingToggle settings={settings} settingPath={['home', 'shows', 'reblog']} onChange={onToggleChange} />
|
||||
|
|
|
@ -1,66 +1,64 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import Lottie from 'react-lottie';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link, Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
import animationData from '../../../../images/circles.json';
|
||||
import { openModal } from '../../../actions/modals';
|
||||
import { Button, Form, HStack, IconButton, Input, Tooltip } from '../../../components/ui';
|
||||
|
||||
import Pulse from './pulse';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
home: { id: 'header.home.label', defaultMessage: 'Home' },
|
||||
login: { id: 'header.login.label', defaultMessage: 'Log in' },
|
||||
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
||||
emailAddress: { id: 'header.login.email.placeholder', defaultMessage: 'Email address' },
|
||||
username: { id: 'header.login.username.placeholder', defaultMessage: 'Email or username' },
|
||||
password: { id: 'header.login.password.label', defaultMessage: 'Password' },
|
||||
forgotPassword: { id: 'header.login.forgot_password', defaultMessage: 'Forgot password?' },
|
||||
});
|
||||
|
||||
const defaultOptions = {
|
||||
renderer: 'svg',
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
animationData: animationData,
|
||||
};
|
||||
|
||||
const Header = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const logo = useSelector((state) => getSoapboxConfig(state).get('logo'));
|
||||
const instance = useSelector((state) => state.get('instance'));
|
||||
const isOpen = instance.get('registrations', false) === true;
|
||||
const { logo } = useSoapboxConfig();
|
||||
const features = useFeatures();
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const isOpen = instance.get('registrations', false) === true;
|
||||
const pepeOpen = useAppSelector(state => state.verification.getIn(['instance', 'registrations'], false) === true);
|
||||
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [email, setEmail] = React.useState('');
|
||||
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 = (event) => {
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
dispatch(logIn(intl, email, password))
|
||||
.then(({ access_token }) => {
|
||||
dispatch(logIn(intl, username, password) as any)
|
||||
.then(({ access_token }: { access_token: string }) => {
|
||||
return (
|
||||
dispatch(verifyCredentials(access_token))
|
||||
dispatch(verifyCredentials(access_token) as any)
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()))
|
||||
.then(() => setShouldRedirect(true))
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: AxiosError) => {
|
||||
setLoading(false);
|
||||
|
||||
const data = error.response && error.response.data;
|
||||
if (data && data.error === 'mfa_required') {
|
||||
const data = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
});
|
||||
|
@ -73,14 +71,9 @@ const Header = () => {
|
|||
<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 relative'>
|
||||
<div className='flex items-center justify-center relative w-36'>
|
||||
<div className='hidden sm:block absolute z-0 left-0 top-0 -ml-[330px] -mt-[400px]'>
|
||||
<Lottie
|
||||
options={defaultOptions}
|
||||
height={800}
|
||||
width={800}
|
||||
isClickToPauseDisabled
|
||||
/>
|
||||
<Pulse />
|
||||
</div>
|
||||
<Link to='/' className='z-10'>
|
||||
<img alt='Logo' src={logo} className='h-6 w-auto cursor-pointer' />
|
||||
|
@ -101,9 +94,9 @@ const Header = () => {
|
|||
{intl.formatMessage(messages.login)}
|
||||
</Button>
|
||||
|
||||
{isOpen && (
|
||||
{(isOpen || features.pepe && pepeOpen) && (
|
||||
<Button
|
||||
to='/auth/verify'
|
||||
to={features.pepe ? '/auth/verify' : '/signup'} // FIXME: actually route this somewhere
|
||||
theme='primary'
|
||||
>
|
||||
{intl.formatMessage(messages.register)}
|
||||
|
@ -115,10 +108,10 @@ const Header = () => {
|
|||
<Form className='hidden xl:flex space-x-2 items-center' onSubmit={handleSubmit}>
|
||||
<Input
|
||||
required
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.emailAddress)}
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
className='max-w-[200px]'
|
||||
/>
|
||||
|
||||
|
@ -132,7 +125,7 @@ const Header = () => {
|
|||
/>
|
||||
|
||||
<Link to='/reset-password'>
|
||||
<Tooltip text='Forgot password?'>
|
||||
<Tooltip text={intl.formatMessage(messages.forgotPassword)}>
|
||||
<IconButton
|
||||
src={require('@tabler/icons/icons/help.svg')}
|
||||
className='bg-transparent text-gray-400 hover:text-gray-700 cursor-pointer'
|
|
@ -0,0 +1,38 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||
|
||||
const LottieAsync = () => {
|
||||
return import(/* webpackChunkName: "lottie" */'soapbox/components/lottie');
|
||||
};
|
||||
|
||||
const fetchAnimationData = () => {
|
||||
return import(/* webpackChunkName: "lottie" */'images/circles.json');
|
||||
};
|
||||
|
||||
/** Homepage pulse animation chunked to not bloat the entrypoint */
|
||||
const Pulse: React.FC = () => {
|
||||
const [animationData, setAnimationData] = useState<any>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnimationData()
|
||||
.then(({ default: json }) => {
|
||||
setAnimationData(json);
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
if (animationData) {
|
||||
return (
|
||||
<BundleContainer fetchComponent={LottieAsync}>
|
||||
{Component => (
|
||||
<Component animationData={animationData} width={800} height={800} />
|
||||
)}
|
||||
</BundleContainer>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default Pulse;
|
|
@ -30,7 +30,7 @@ class PublicLayout extends ImmutablePureComponent {
|
|||
const { standalone } = this.props;
|
||||
|
||||
if (standalone) {
|
||||
return <Redirect to='/auth/external' />;
|
||||
return <Redirect to='/login/external' />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormAction
|
|||
|
||||
/*
|
||||
Security settings page for user account
|
||||
Routed to /auth/mfa
|
||||
Routed to /settings/mfa
|
||||
Includes following features:
|
||||
- Set up Multi-factor Auth
|
||||
*/
|
||||
|
|
|
@ -33,7 +33,7 @@ const Settings = () => {
|
|||
|
||||
const navigateToChangeEmail = React.useCallback(() => history.push('/settings/email'), [history]);
|
||||
const navigateToChangePassword = React.useCallback(() => history.push('/settings/password'), [history]);
|
||||
const navigateToMfa = React.useCallback(() => history.push('/auth/mfa'), [history]);
|
||||
const navigateToMfa = React.useCallback(() => history.push('/settings/mfa'), [history]);
|
||||
const navigateToEditProfile = React.useCallback(() => history.push('/settings/profile'), [history]);
|
||||
|
||||
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
||||
|
|
|
@ -52,7 +52,7 @@ class BoostModal extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
title='ReTruth?'
|
||||
title='Repost?'
|
||||
confirmationAction={this.handleReblog}
|
||||
confirmationText={intl.formatMessage(buttonText)}
|
||||
>
|
||||
|
|
|
@ -59,7 +59,7 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
|||
{(features.federating && features.accountMoving) && (
|
||||
<FooterLink to='/settings/migration'><FormattedMessage id='navigation_bar.account_migration' defaultMessage='Move account' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/auth/sign_out' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink>
|
||||
<FooterLink to='/logout' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ const Navbar = () => {
|
|||
const singleUserMode = soapboxConfig.get('singleUserMode');
|
||||
|
||||
// In demo mode, use the Soapbox logo
|
||||
const logo = settings.get('demo') ? require('images/soapbox-logo.svg') : soapboxConfig.get('logo');
|
||||
const logo = settings.get('demo') ? require('images/soapbox-logo.svg') : soapboxConfig.logo;
|
||||
|
||||
const onOpenSidebar = () => dispatch(openSidebar());
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
|
|||
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.logout, { acct: account.acct }),
|
||||
to: '/auth/sign_out',
|
||||
to: '/logout',
|
||||
action: handleLogOut,
|
||||
icon: require('@tabler/icons/icons/logout.svg'),
|
||||
});
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openComposeWithText } from 'soapbox/actions/compose';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
|
||||
const buildWelcomeMessage = account => (
|
||||
`Yo @${account.get('acct')} nice to have you on TRUTH!
|
||||
|
||||
Here's the lay of the land...
|
||||
|
||||
Got suggestions? Post a TRUTH (<- this is what we call a post) & tag the @suggestions account.
|
||||
|
||||
Come across a bug? Feel free to let us know by tagging the @Bug account in a TRUTH! Screenshots encouraged!
|
||||
|
||||
Also, if you want to just chat about the product... feel free to drop some 💥 TRUTH 💣 on me! Tag @Billy!
|
||||
|
||||
Finally, make sure to invite only your favorite peeps by hitting Invites in the sidebar.`
|
||||
);
|
||||
|
||||
const messages = defineMessages({
|
||||
welcome: { id: 'account.welcome', defaultMessage: 'Welcome' },
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onClick(account) {
|
||||
const text = buildWelcomeMessage(account);
|
||||
dispatch(openComposeWithText(text));
|
||||
},
|
||||
});
|
||||
|
||||
export default @connect(undefined, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class WelcomeButton extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick(this.props.account);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Button className='logo-button button--welcome' onClick={this.handleClick}>
|
||||
<div dangerouslySetInnerHTML={{ __html: emojify('👋') }} />
|
||||
{intl.formatMessage(messages.welcome)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -120,6 +120,7 @@ import {
|
|||
CreateApp,
|
||||
SettingsStore,
|
||||
TestTimeline,
|
||||
LogoutPage,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react_router_helpers';
|
||||
|
||||
|
@ -221,11 +222,16 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
const authenticatedProfile = soapbox.get('authenticatedProfile');
|
||||
const hasCrypto = soapbox.get('cryptoAddresses').size > 0;
|
||||
|
||||
// NOTE: Mastodon and Pleroma route some basenames to the backend.
|
||||
// When adding new routes, use a basename that does NOT conflict
|
||||
// with a known backend route, but DO redirect the backend route
|
||||
// to the corresponding component as a fallback.
|
||||
// Ex: use /login instead of /auth, but redirect /auth to /login
|
||||
return (
|
||||
<Switch>
|
||||
<WrappedRoute path='/auth/external' component={ExternalLogin} publicRoute exact />
|
||||
<WrappedRoute path='/auth/mfa' page={DefaultPage} component={MfaForm} exact />
|
||||
<WrappedRoute path='/auth/confirmation' page={EmptyPage} component={EmailConfirmation} publicRoute exact />
|
||||
<WrappedRoute path='/login/external' component={ExternalLogin} publicRoute exact />
|
||||
<WrappedRoute path='/email-confirmation' page={EmptyPage} component={EmailConfirmation} publicRoute exact />
|
||||
<WrappedRoute path='/logout' page={EmptyPage} component={LogoutPage} publicRoute exact />
|
||||
|
||||
<WrappedRoute path='/' exact page={HomePage} component={HomeTimeline} content={children} />
|
||||
|
||||
|
@ -240,6 +246,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/conversations' page={DefaultPage} component={Conversations} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
<WrappedRoute path='/messages' page={DefaultPage} component={features.directTimeline ? DirectTimeline : Conversations} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
|
||||
{/* Gab groups */}
|
||||
{/*
|
||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} componentParams={{ activeTab: 'featured' }} />
|
||||
<WrappedRoute path='/groups/create' page={GroupsPage} component={Groups} content={children} componentParams={{ showCreateForm: true, activeTab: 'featured' }} />
|
||||
|
@ -251,7 +258,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/groups/:id' page={GroupPage} component={GroupTimeline} content={children} />
|
||||
*/}
|
||||
|
||||
{/* Redirects from Mastodon, Pleroma FE, etc. to fix old bookmarks */}
|
||||
{/* Mastodon web routes */}
|
||||
<Redirect from='/web/:path1/:path2/:path3' to='/:path1/:path2/:path3' />
|
||||
<Redirect from='/web/:path1/:path2' to='/:path1/:path2' />
|
||||
<Redirect from='/web/:path' to='/:path' />
|
||||
|
@ -259,6 +266,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<Redirect from='/timelines/public/local' to='/timeline/local' />
|
||||
<Redirect from='/timelines/public' to='/timeline/fediverse' />
|
||||
<Redirect from='/timelines/direct' to='/messages' />
|
||||
|
||||
{/* Pleroma FE web routes */}
|
||||
<Redirect from='/main/all' to='/timeline/fediverse' />
|
||||
<Redirect from='/main/public' to='/timeline/local' />
|
||||
<Redirect from='/main/friends' to='/' />
|
||||
|
@ -268,12 +277,35 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<Redirect from='/users/:username/statuses/:statusId' to='/@:username/posts/:statusId' />
|
||||
<Redirect from='/users/:username/chats' to='/chats' />
|
||||
<Redirect from='/users/:username' to='/@:username' />
|
||||
<Redirect from='/terms' to='/about' />
|
||||
<Redirect from='/registration' to='/' exact />
|
||||
|
||||
{/* Gab */}
|
||||
<Redirect from='/home' to='/' />
|
||||
|
||||
{/* Mastodon rendered pages */}
|
||||
<Redirect from='/admin/dashboard' to='/admin' exact />
|
||||
<Redirect from='/terms' to='/about' />
|
||||
<Redirect from='/settings/preferences' to='/settings' />
|
||||
<Redirect from='/settings/two_factor_authentication_methods' to='/settings/mfa' />
|
||||
<Redirect from='/settings/otp_authentication' to='/settings/mfa' />
|
||||
<Redirect from='/settings/applications' to='/developers' />
|
||||
<Redirect from='/auth/edit' to='/settings' />
|
||||
<Redirect from='/auth/confirmation' to={`/email-confirmation${this.props.location.search}`} />
|
||||
<Redirect from='/auth/reset_password' to='/reset-password' />
|
||||
<Redirect from='/auth/edit_password' to='/edit-password' />
|
||||
<Redirect from='/auth/sign_in' to='/login' />
|
||||
<Redirect from='/auth/sign_out' to='/logout' />
|
||||
|
||||
{/* Pleroma hard-coded email URLs */}
|
||||
<Redirect from='/registration/:token' to='/invite/:token' />
|
||||
|
||||
{/* Soapbox Legacy redirects */}
|
||||
<Redirect from='/canary' to='/about/canary' />
|
||||
<Redirect from='/canary.txt' to='/about/canary' />
|
||||
<Redirect from='/auth/external' to='/login/external' />
|
||||
<Redirect from='/auth/mfa' to='/settings/mfa' />
|
||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||
<Redirect from='/auth/password/edit' to='/edit-password' />
|
||||
|
||||
<WrappedRoute path='/tags/:id' publicRoute page={DefaultPage} component={HashtagTimeline} content={children} />
|
||||
|
||||
|
@ -310,14 +342,8 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
|
||||
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
|
||||
|
||||
<Redirect from='/registration/:token' to='/invite/:token' />
|
||||
<Redirect from='/registration' to='/' />
|
||||
<WrappedRoute path='/invite/:token' component={RegisterInvite} content={children} publicRoute />
|
||||
|
||||
<Redirect from='/auth/edit' to='/settings' />
|
||||
<Redirect from='/settings/preferences' to='/settings' />
|
||||
<Redirect from='/auth/password/new' to='/reset-password' />
|
||||
<Redirect from='/auth/password/edit' to='/edit-password' />
|
||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
||||
<WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />
|
||||
<WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />
|
||||
|
@ -327,11 +353,11 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
<WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} />
|
||||
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
|
||||
<WrappedRoute path='/settings/media_display' page={DefaultPage} component={MediaDisplay} content={children} />
|
||||
<WrappedRoute path='/settings/mfa' page={DefaultPage} component={MfaForm} exact />
|
||||
<WrappedRoute path='/settings' page={DefaultPage} component={Settings} content={children} />
|
||||
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
|
||||
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
|
||||
|
||||
<Redirect from='/admin/dashboard' to='/admin' exact />
|
||||
<WrappedRoute path='/admin' staffOnly page={AdminPage} component={Dashboard} content={children} exact />
|
||||
<WrappedRoute path='/admin/approval' staffOnly page={AdminPage} component={AwaitingApproval} content={children} exact />
|
||||
<WrappedRoute path='/admin/reports' staffOnly page={AdminPage} component={Reports} content={children} exact />
|
||||
|
|
|
@ -250,6 +250,10 @@ export function ExternalLogin() {
|
|||
return import(/* webpackChunkName: "features/external_login" */'../../external_login');
|
||||
}
|
||||
|
||||
export function LogoutPage() {
|
||||
return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/logout');
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
return import(/* webpackChunkName: "features/settings" */'../../settings');
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ const WaitlistPage = ({ account }) => {
|
|||
</Link>
|
||||
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-2 space-x-3'>
|
||||
<Button onClick={onClickLogOut} theme='primary' to='/auth/sign_out'>
|
||||
<Button onClick={onClickLogOut} theme='primary' to='/logout'>
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -117,6 +117,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
|
|||
singleUserMode: false,
|
||||
singleUserModeProfile: '',
|
||||
linkFooterMessage: '',
|
||||
links: ImmutableMap<string, string>(),
|
||||
}, 'SoapboxConfig');
|
||||
|
||||
type SoapboxConfigMap = ImmutableMap<string, any>;
|
||||
|
|
|
@ -144,6 +144,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
pepe: v.software === TRUTHSOCIAL,
|
||||
accountLocation: v.software === TRUTHSOCIAL,
|
||||
accountWebsite: v.software === TRUTHSOCIAL,
|
||||
frontendConfigurations: v.software === PLEROMA,
|
||||
|
||||
// FIXME: long-term this shouldn't be a feature,
|
||||
// but for now we want it to be overrideable in the build
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
"line-awesome": "^1.3.0",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.7.11",
|
||||
"lottie-web": "^5.9.2",
|
||||
"mark-loader": "^0.1.6",
|
||||
"marky": "^1.2.1",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
|
@ -154,7 +155,6 @@
|
|||
"react-immutable-pure-component": "^2.0.0",
|
||||
"react-inlinesvg": "^2.3.0",
|
||||
"react-intl": "^5.0.0",
|
||||
"react-lottie": "^1.2.3",
|
||||
"react-motion": "^0.5.2",
|
||||
"react-notification": "^6.8.4",
|
||||
"react-otp-input": "^2.4.0",
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -3062,14 +3062,6 @@ babel-preset-jest@^27.5.1:
|
|||
babel-plugin-jest-hoist "^27.5.1"
|
||||
babel-preset-current-node-syntax "^1.0.0"
|
||||
|
||||
babel-runtime@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
|
||||
integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
|
||||
dependencies:
|
||||
core-js "^2.4.0"
|
||||
regenerator-runtime "^0.11.0"
|
||||
|
||||
bail@^1.0.0:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
|
||||
|
@ -3704,11 +3696,6 @@ core-js-pure@^3.16.0:
|
|||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.17.3.tgz#98ea3587188ab7ef4695db6518eeb71aec42604a"
|
||||
integrity sha512-YusrqwiOTTn8058JDa0cv9unbXdIiIgcgI9gXso0ey4WgkFLd3lYlV9rp9n7nDCsYxXsMDTjA4m1h3T348mdlQ==
|
||||
|
||||
core-js@^2.4.0:
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.1.3, core-js@^3.15.2:
|
||||
version "3.18.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.0.tgz#9af3f4a6df9ba3428a3fb1b171f1503b3f40cc49"
|
||||
|
@ -7138,10 +7125,10 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3
|
|||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
lottie-web@^5.1.3:
|
||||
version "5.7.13"
|
||||
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.7.13.tgz#c4087e4742c485fc2c4034adad65d1f3fcd438b0"
|
||||
integrity sha512-6iy93BGPkdk39b0jRgJ8Zosxi8QqcMP5XcDvg1f0XAvEkke6EMCl6BUO4Lu78dpgvfG2tzut4QJ+0vCrfbrldQ==
|
||||
lottie-web@^5.9.2:
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.9.2.tgz#38db3f3f3655802c465d725a359fc9d303e31335"
|
||||
integrity sha512-YnoJIKCdKIzno8G/kONOpADW6H/ORZV9puy3vWOhWmHtbDcpISFGVvvdKKa2jwAcsVqXK4xSi0po730kAPIfBw==
|
||||
|
||||
lower-case@^2.0.2:
|
||||
version "2.0.2"
|
||||
|
@ -8807,14 +8794,6 @@ react-lifecycles-compat@^3.0.4:
|
|||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-lottie@^1.2.3:
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-lottie/-/react-lottie-1.2.3.tgz#8544b96939e088658072eea5e12d912cdaa3acc1"
|
||||
integrity sha512-qLCERxUr8M+4mm1LU0Ruxw5Y5Fn/OmYkGfnA+JDM/dZb3oKwVAJCjwnjkj9TMHtzR2U6sMEUD3ZZ1RaHagM7kA==
|
||||
dependencies:
|
||||
babel-runtime "^6.26.0"
|
||||
lottie-web "^5.1.3"
|
||||
|
||||
react-motion@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
|
||||
|
@ -9121,11 +9100,6 @@ regenerate@^1.4.2:
|
|||
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
|
||||
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
|
||||
|
||||
regenerator-runtime@^0.11.0:
|
||||
version "0.11.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.12.0:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
|
|
Loading…
Reference in New Issue