Merge branch 'next-public-layout' into 'next'

Next: improve public pages, feature detection, etc

See merge request soapbox-pub/soapbox-fe!1243
revert-5af0e40a
Alex Gleason 2022-04-20 13:19:35 +00:00
commit 9d03b8ab93
38 zmienionych plików z 483 dodań i 413 usunięć

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -52,7 +52,7 @@ class BoostModal extends ImmutablePureComponent {
return (
<Modal
title='ReTruth?'
title='Repost?'
confirmationAction={this.handleReblog}
confirmationText={intl.formatMessage(buttonText)}
>

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -117,6 +117,7 @@ export const SoapboxConfigRecord = ImmutableRecord({
singleUserMode: false,
singleUserModeProfile: '',
linkFooterMessage: '',
links: ImmutableMap<string, string>(),
}, 'SoapboxConfig');
type SoapboxConfigMap = ImmutableMap<string, any>;

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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