Merge branch 'thread-cta' into 'develop'
Display a CTA in threads to log in See merge request soapbox-pub/soapbox-fe!1353
This commit is contained in:
commit
938665f157
|
@ -47,7 +47,10 @@ interface ICardHeader {
|
||||||
onBackClick?: (event: React.MouseEvent) => void
|
onBackClick?: (event: React.MouseEvent) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Typically holds a CardTitle. */
|
/**
|
||||||
|
* Card header container with back button.
|
||||||
|
* Typically holds a CardTitle.
|
||||||
|
*/
|
||||||
const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }): JSX.Element => {
|
const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }): JSX.Element => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface IModal {
|
||||||
/** Whether the confirmation button is disabled. */
|
/** Whether the confirmation button is disabled. */
|
||||||
confirmationDisabled?: boolean,
|
confirmationDisabled?: boolean,
|
||||||
/** Confirmation button text. */
|
/** Confirmation button text. */
|
||||||
confirmationText?: string,
|
confirmationText?: React.ReactNode,
|
||||||
/** Confirmation button theme. */
|
/** Confirmation button theme. */
|
||||||
confirmationTheme?: 'danger',
|
confirmationTheme?: 'danger',
|
||||||
/** Callback when the modal is closed. */
|
/** Callback when the modal is closed. */
|
||||||
|
@ -28,7 +28,7 @@ interface IModal {
|
||||||
/** Callback when the secondary action is chosen. */
|
/** Callback when the secondary action is chosen. */
|
||||||
secondaryAction?: () => void,
|
secondaryAction?: () => void,
|
||||||
/** Secondary button text. */
|
/** Secondary button text. */
|
||||||
secondaryText?: string,
|
secondaryText?: React.ReactNode,
|
||||||
/** Don't focus the "confirm" button on mount. */
|
/** Don't focus the "confirm" button on mount. */
|
||||||
skipFocus?: boolean,
|
skipFocus?: boolean,
|
||||||
/** Title text for the modal. */
|
/** Title text for the modal. */
|
||||||
|
|
|
@ -85,6 +85,7 @@ const SoapboxMount = () => {
|
||||||
const systemTheme = useSystemTheme();
|
const systemTheme = useSystemTheme();
|
||||||
const userTheme = settings.get('themeMode');
|
const userTheme = settings.get('themeMode');
|
||||||
const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
|
const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
|
||||||
|
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||||
|
|
||||||
const themeCss = generateThemeCss(soapboxConfig);
|
const themeCss = generateThemeCss(soapboxConfig);
|
||||||
|
|
||||||
|
@ -160,20 +161,38 @@ const SoapboxMount = () => {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Redirect from='/v1/verify_email/:token' to='/verify/email/:token' />
|
<Redirect from='/v1/verify_email/:token' to='/verify/email/:token' />
|
||||||
|
|
||||||
{waitlisted && <Route render={(props) => <WaitlistPage {...props} account={account} />} />}
|
{/* Redirect signup route depending on Pepe enablement. */}
|
||||||
|
{/* We should prefer using /signup in components. */}
|
||||||
|
{pepeEnabled ? (
|
||||||
|
<Redirect from='/signup' to='/verify' />
|
||||||
|
) : (
|
||||||
|
<Redirect from='/verify' to='/signup' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{waitlisted && (
|
||||||
|
<Route render={(props) => <WaitlistPage {...props} account={account} />} />
|
||||||
|
)}
|
||||||
|
|
||||||
{!me && (singleUserMode
|
{!me && (singleUserMode
|
||||||
? <Redirect exact from='/' to={`/${singleUserMode}`} />
|
? <Redirect exact from='/' to={`/${singleUserMode}`} />
|
||||||
: <Route exact path='/' component={PublicLayout} />)}
|
: <Route exact path='/' component={PublicLayout} />)}
|
||||||
|
|
||||||
{!me && <Route exact path='/' component={PublicLayout} />}
|
{!me && (
|
||||||
|
<Route exact path='/' component={PublicLayout} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Route exact path='/about/:slug?' component={PublicLayout} />
|
<Route exact path='/about/:slug?' component={PublicLayout} />
|
||||||
<Route exact path='/mobile/:slug?' component={PublicLayout} />
|
<Route exact path='/mobile/:slug?' component={PublicLayout} />
|
||||||
<Route path='/login' component={AuthLayout} />
|
<Route path='/login' component={AuthLayout} />
|
||||||
|
|
||||||
{(features.accountCreation && instance.registrations) && (
|
{(features.accountCreation && instance.registrations) && (
|
||||||
<Route exact path='/signup' component={AuthLayout} />
|
<Route exact path='/signup' component={AuthLayout} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pepeEnabled && (
|
||||||
<Route path='/verify' component={AuthLayout} />
|
<Route path='/verify' component={AuthLayout} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Route path='/reset-password' component={AuthLayout} />
|
<Route path='/reset-password' component={AuthLayout} />
|
||||||
<Route path='/edit-password' component={AuthLayout} />
|
<Route path='/edit-password' component={AuthLayout} />
|
||||||
<Route path='/invite/:token' component={AuthLayout} />
|
<Route path='/invite/:token' component={AuthLayout} />
|
||||||
|
|
|
@ -112,7 +112,7 @@ const Header = () => {
|
||||||
|
|
||||||
{(isOpen || pepeEnabled && pepeOpen) && (
|
{(isOpen || pepeEnabled && pepeOpen) && (
|
||||||
<Button
|
<Button
|
||||||
to={pepeEnabled ? '/verify' : '/signup'}
|
to='/signup'
|
||||||
theme='primary'
|
theme='primary'
|
||||||
>
|
>
|
||||||
{intl.formatMessage(messages.register)}
|
{intl.formatMessage(messages.register)}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Card, CardTitle, Text, Stack, Button } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
/** Prompts logged-out users to log in when viewing a thread. */
|
||||||
|
const ThreadLoginCta: React.FC = () => {
|
||||||
|
const siteTitle = useAppSelector(state => state.instance.title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className='px-6 py-12 space-y-6 text-center' variant='rounded'>
|
||||||
|
<Stack>
|
||||||
|
<CardTitle title={<FormattedMessage id='thread_login.title' defaultMessage='Continue the conversation' />} />
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage
|
||||||
|
id='thread_login.message'
|
||||||
|
defaultMessage='Join {siteTitle} to get the full story and details.'
|
||||||
|
values={{ siteTitle }}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack space={4} className='max-w-xs mx-auto'>
|
||||||
|
<Button theme='secondary' to='/login' block>
|
||||||
|
<FormattedMessage id='thread_login.login' defaultMessage='Log in' />
|
||||||
|
</Button>
|
||||||
|
<Button to='/signup' block>
|
||||||
|
<FormattedMessage id='thread_login.signup' defaultMessage='Sign up' />
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadLoginCta;
|
|
@ -19,7 +19,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||||
import { Column } from 'soapbox/components/ui';
|
import { Column, Stack } from 'soapbox/components/ui';
|
||||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||||
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
import PendingStatus from 'soapbox/features/ui/components/pending_status';
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from
|
||||||
|
|
||||||
import ActionBar from './components/action-bar';
|
import ActionBar from './components/action-bar';
|
||||||
import DetailedStatus from './components/detailed-status';
|
import DetailedStatus from './components/detailed-status';
|
||||||
|
import ThreadLoginCta from './components/thread-login-cta';
|
||||||
import ThreadStatus from './components/thread-status';
|
import ThreadStatus from './components/thread-status';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
|
@ -72,6 +73,7 @@ import type {
|
||||||
Attachment as AttachmentEntity,
|
Attachment as AttachmentEntity,
|
||||||
Status as StatusEntity,
|
Status as StatusEntity,
|
||||||
} from 'soapbox/types/entities';
|
} from 'soapbox/types/entities';
|
||||||
|
import type { Me } from 'soapbox/types/soapbox';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'status.title', defaultMessage: '@{username}\'s Post' },
|
title: { id: 'status.title', defaultMessage: '@{username}\'s Post' },
|
||||||
|
@ -181,6 +183,7 @@ interface IStatus extends RouteComponentProps, IntlComponentProps {
|
||||||
allowedEmoji: ImmutableList<string>,
|
allowedEmoji: ImmutableList<string>,
|
||||||
onOpenMedia: (media: ImmutableList<AttachmentEntity>, index: number) => void,
|
onOpenMedia: (media: ImmutableList<AttachmentEntity>, index: number) => void,
|
||||||
onOpenVideo: (video: AttachmentEntity, time: number) => void,
|
onOpenVideo: (video: AttachmentEntity, time: number) => void,
|
||||||
|
me: Me,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStatusState {
|
interface IStatusState {
|
||||||
|
@ -669,7 +672,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, ancestorsIds, descendantsIds, intl } = this.props;
|
const { me, status, ancestorsIds, descendantsIds, intl } = this.props;
|
||||||
|
|
||||||
const hasAncestors = ancestorsIds && ancestorsIds.size > 0;
|
const hasAncestors = ancestorsIds && ancestorsIds.size > 0;
|
||||||
const hasDescendants = descendantsIds && descendantsIds.size > 0;
|
const hasDescendants = descendantsIds && descendantsIds.size > 0;
|
||||||
|
@ -782,6 +785,7 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
<SubNavigation message={intl.formatMessage(titleMessage, { username })} />
|
<SubNavigation message={intl.formatMessage(titleMessage, { username })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Stack space={2}>
|
||||||
<div ref={this.setRef} className='thread'>
|
<div ref={this.setRef} className='thread'>
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
onRefresh={this.handleRefresh}
|
onRefresh={this.handleRefresh}
|
||||||
|
@ -792,6 +796,9 @@ class Status extends ImmutablePureComponent<IStatus, IStatusState> {
|
||||||
{children}
|
{children}
|
||||||
</ScrollableList>
|
</ScrollableList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!me && <ThreadLoginCta />}
|
||||||
|
</Stack>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ const LandingPageModal: React.FC<ILandingPageModal> = ({ onClose }) => {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{(isOpen || pepeEnabled && pepeOpen) && (
|
{(isOpen || pepeEnabled && pepeOpen) && (
|
||||||
<Button to={pepeEnabled ? '/verify' : '/signup'} theme='primary' block>
|
<Button to='/signup' theme='primary' block>
|
||||||
{intl.formatMessage(messages.register)}
|
{intl.formatMessage(messages.register)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -70,7 +70,7 @@ const Navbar = () => {
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{!singleUserMode && (
|
{!singleUserMode && (
|
||||||
<Button theme='primary' to='/' size='sm'>
|
<Button theme='primary' to='/signup' size='sm'>
|
||||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const SignUpPanel = () => {
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Button theme='primary' block to='/'>
|
<Button theme='primary' block to='/signup'>
|
||||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { remoteInteraction } from 'soapbox/actions/interactions';
|
|
||||||
import snackbar from 'soapbox/actions/snackbar';
|
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
|
||||||
import { Button, Modal, Stack, Text } from 'soapbox/components/ui';
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
accountPlaceholder: { id: 'remote_interaction.account_placeholder', defaultMessage: 'Enter your username@domain you want to act from' },
|
|
||||||
userNotFoundError: { id: 'remote_interaction.user_not_found_error', defaultMessage: 'Couldn\'t find given user' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const instance = state.get('instance');
|
|
||||||
const features = getFeatures(instance);
|
|
||||||
const soapboxConfig = getSoapboxConfig(state);
|
|
||||||
|
|
||||||
if (props.action !== 'FOLLOW') {
|
|
||||||
return {
|
|
||||||
features,
|
|
||||||
siteTitle: state.getIn(['instance', 'title']),
|
|
||||||
remoteInteractionsAPI: features.remoteInteractionsAPI,
|
|
||||||
singleUserMode: soapboxConfig.get('singleUserMode'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const userName = state.getIn(['accounts', props.account, 'display_name']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
features,
|
|
||||||
siteTitle: state.getIn(['instance', 'title']),
|
|
||||||
userName,
|
|
||||||
remoteInteractionsAPI: features.remoteInteractionsAPI,
|
|
||||||
singleUserMode: soapboxConfig.get('singleUserMode'),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
dispatch,
|
|
||||||
onRemoteInteraction(ap_id, account) {
|
|
||||||
return dispatch(remoteInteraction(ap_id, account));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
@withRouter
|
|
||||||
class UnauthorizedModal extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
features: PropTypes.object.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onRemoteInteraction: PropTypes.func.isRequired,
|
|
||||||
userName: PropTypes.string,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
singleUserMode: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
account: '',
|
|
||||||
};
|
|
||||||
|
|
||||||
onAccountChange = e => {
|
|
||||||
this.setState({ account: e.target.value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onClickClose = () => {
|
|
||||||
this.props.onClose('UNAUTHORIZED');
|
|
||||||
};
|
|
||||||
|
|
||||||
onClickProceed = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { intl, ap_id, dispatch, onClose, onRemoteInteraction } = this.props;
|
|
||||||
const { account } = this.state;
|
|
||||||
|
|
||||||
onRemoteInteraction(ap_id, account)
|
|
||||||
.then(url => {
|
|
||||||
window.open(url, '_new', 'noopener,noreferrer');
|
|
||||||
onClose('UNAUTHORIZED');
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
if (error.message === 'Couldn\'t find user') {
|
|
||||||
dispatch(snackbar.error(intl.formatMessage(messages.userNotFoundError)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onLogin = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.props.history.push('/login');
|
|
||||||
this.onClickClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
onRegister = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.props.history.push('/');
|
|
||||||
this.onClickClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRemoteInteractions() {
|
|
||||||
const { intl, siteTitle, userName, action, singleUserMode } = this.props;
|
|
||||||
const { account } = this.state;
|
|
||||||
|
|
||||||
let header;
|
|
||||||
let button;
|
|
||||||
|
|
||||||
if (action === 'FOLLOW') {
|
|
||||||
header = <FormattedMessage id='remote_interaction.follow_title' defaultMessage='Follow {user} remotely' values={{ user: userName }} />;
|
|
||||||
button = <FormattedMessage id='remote_interaction.follow' defaultMessage='Proceed to follow' />;
|
|
||||||
} else if (action === 'REPLY') {
|
|
||||||
header = <FormattedMessage id='remote_interaction.reply_title' defaultMessage='Reply to a post remotely' />;
|
|
||||||
button = <FormattedMessage id='remote_interaction.reply' defaultMessage='Proceed to reply' />;
|
|
||||||
} else if (action === 'REBLOG') {
|
|
||||||
header = <FormattedMessage id='remote_interaction.reblog_title' defaultMessage='Reblog a post remotely' />;
|
|
||||||
button = <FormattedMessage id='remote_interaction.reblog' defaultMessage='Proceed to repost' />;
|
|
||||||
} else if (action === 'FAVOURITE') {
|
|
||||||
header = <FormattedMessage id='remote_interaction.favourite_title' defaultMessage='Like a post remotely' />;
|
|
||||||
button = <FormattedMessage id='remote_interaction.favourite' defaultMessage='Proceed to like' />;
|
|
||||||
} else if (action === 'POLL_VOTE') {
|
|
||||||
header = <FormattedMessage id='remote_interaction.poll_vote_title' defaultMessage='Vote in a poll remotely' />;
|
|
||||||
button = <FormattedMessage id='remote_interaction.poll_vote' defaultMessage='Proceed to vote' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={header}
|
|
||||||
onClose={this.onClickClose}
|
|
||||||
confirmationAction={!singleUserMode && this.onLogin}
|
|
||||||
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
|
||||||
secondaryAction={this.onRegister}
|
|
||||||
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
|
||||||
>
|
|
||||||
<div className='remote-interaction-modal__content'>
|
|
||||||
<form className='simple_form remote-interaction-modal__fields'>
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
placeholder={intl.formatMessage(messages.accountPlaceholder)}
|
|
||||||
name='remote_follow[acct]'
|
|
||||||
value={account}
|
|
||||||
autoCorrect='off'
|
|
||||||
autoCapitalize='off'
|
|
||||||
onChange={this.onAccountChange}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button theme='primary' onClick={this.onClickProceed}>{button}</Button>
|
|
||||||
</form>
|
|
||||||
<div className='remote-interaction-modal__divider'>
|
|
||||||
<Text align='center'>
|
|
||||||
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
{!singleUserMode && (
|
|
||||||
<Text size='lg' weight='medium'>
|
|
||||||
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { features, siteTitle, action } = this.props;
|
|
||||||
|
|
||||||
if (action && features.remoteInteractionsAPI && features.federating) return this.renderRemoteInteractions();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />}
|
|
||||||
onClose={this.onClickClose}
|
|
||||||
confirmationAction={this.onLogin}
|
|
||||||
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
|
||||||
secondaryAction={this.onRegister}
|
|
||||||
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
|
||||||
>
|
|
||||||
<Stack>
|
|
||||||
<Text>
|
|
||||||
<FormattedMessage id='unauthorized_modal.text' defaultMessage='You need to be logged in to do that.' />
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UnauthorizedModal));
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { remoteInteraction } from 'soapbox/actions/interactions';
|
||||||
|
import snackbar from 'soapbox/actions/snackbar';
|
||||||
|
import { Button, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector, useAppDispatch, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
accountPlaceholder: { id: 'remote_interaction.account_placeholder', defaultMessage: 'Enter your username@domain you want to act from' },
|
||||||
|
userNotFoundError: { id: 'remote_interaction.user_not_found_error', defaultMessage: 'Couldn\'t find given user' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IUnauthorizedModal {
|
||||||
|
/** Unauthorized action type. */
|
||||||
|
action: 'FOLLOW' | 'REPLY' | 'REBLOG' | 'FAVOURITE' | 'POLL_VOTE',
|
||||||
|
/** Close event handler. */
|
||||||
|
onClose: (modalType: string) => void,
|
||||||
|
/** ActivityPub ID of the account OR status being acted upon. */
|
||||||
|
ap_id?: string,
|
||||||
|
/** Account ID of the account being acted upon. */
|
||||||
|
account?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modal to display when a logged-out user tries to do something that requires login. */
|
||||||
|
const UnauthorizedModal: React.FC<IUnauthorizedModal> = ({ action, onClose, account: accountId, ap_id: apId }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const { singleUserMode } = useSoapboxConfig();
|
||||||
|
const siteTitle = useAppSelector(state => state.instance.title);
|
||||||
|
const username = useAppSelector(state => state.accounts.get(accountId)?.display_name);
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const [account, setAccount] = useState('');
|
||||||
|
|
||||||
|
const onAccountChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||||
|
setAccount(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('UNAUTHORIZED');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickProceed: React.MouseEventHandler = e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
dispatch(remoteInteraction(apId, account))
|
||||||
|
.then(url => {
|
||||||
|
window.open(url, '_new', 'noopener,noreferrer');
|
||||||
|
onClose('UNAUTHORIZED');
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.message === 'Couldn\'t find user') {
|
||||||
|
dispatch(snackbar.error(intl.formatMessage(messages.userNotFoundError)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLogin = () => {
|
||||||
|
history.push('/login');
|
||||||
|
onClickClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRegister = () => {
|
||||||
|
history.push('/signup');
|
||||||
|
onClickClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRemoteInteractions = () => {
|
||||||
|
let header;
|
||||||
|
let button;
|
||||||
|
|
||||||
|
if (action === 'FOLLOW') {
|
||||||
|
header = <FormattedMessage id='remote_interaction.follow_title' defaultMessage='Follow {user} remotely' values={{ user: username }} />;
|
||||||
|
button = <FormattedMessage id='remote_interaction.follow' defaultMessage='Proceed to follow' />;
|
||||||
|
} else if (action === 'REPLY') {
|
||||||
|
header = <FormattedMessage id='remote_interaction.reply_title' defaultMessage='Reply to a post remotely' />;
|
||||||
|
button = <FormattedMessage id='remote_interaction.reply' defaultMessage='Proceed to reply' />;
|
||||||
|
} else if (action === 'REBLOG') {
|
||||||
|
header = <FormattedMessage id='remote_interaction.reblog_title' defaultMessage='Reblog a post remotely' />;
|
||||||
|
button = <FormattedMessage id='remote_interaction.reblog' defaultMessage='Proceed to repost' />;
|
||||||
|
} else if (action === 'FAVOURITE') {
|
||||||
|
header = <FormattedMessage id='remote_interaction.favourite_title' defaultMessage='Like a post remotely' />;
|
||||||
|
button = <FormattedMessage id='remote_interaction.favourite' defaultMessage='Proceed to like' />;
|
||||||
|
} else if (action === 'POLL_VOTE') {
|
||||||
|
header = <FormattedMessage id='remote_interaction.poll_vote_title' defaultMessage='Vote in a poll remotely' />;
|
||||||
|
button = <FormattedMessage id='remote_interaction.poll_vote' defaultMessage='Proceed to vote' />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={header}
|
||||||
|
onClose={onClickClose}
|
||||||
|
confirmationAction={!singleUserMode ? onLogin : undefined}
|
||||||
|
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||||
|
secondaryAction={onRegister}
|
||||||
|
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||||
|
>
|
||||||
|
<div className='remote-interaction-modal__content'>
|
||||||
|
<form className='simple_form remote-interaction-modal__fields'>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder={intl.formatMessage(messages.accountPlaceholder)}
|
||||||
|
name='remote_follow[acct]'
|
||||||
|
value={account}
|
||||||
|
autoCorrect='off'
|
||||||
|
autoCapitalize='off'
|
||||||
|
onChange={onAccountChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button theme='primary' onClick={onClickProceed}>{button}</Button>
|
||||||
|
</form>
|
||||||
|
<div className='remote-interaction-modal__divider'>
|
||||||
|
<Text align='center'>
|
||||||
|
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{!singleUserMode && (
|
||||||
|
<Text size='lg' weight='medium'>
|
||||||
|
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (action && features.remoteInteractionsAPI && features.federating) {
|
||||||
|
return renderRemoteInteractions();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />}
|
||||||
|
onClose={onClickClose}
|
||||||
|
confirmationAction={onLogin}
|
||||||
|
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||||
|
secondaryAction={onRegister}
|
||||||
|
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text>
|
||||||
|
<FormattedMessage id='unauthorized_modal.text' defaultMessage='You need to be logged in to do that.' />
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnauthorizedModal;
|
Loading…
Reference in New Issue