diff --git a/app/soapbox/components/ui/card/card.tsx b/app/soapbox/components/ui/card/card.tsx index 9dd22bebe..f8ed33c1e 100644 --- a/app/soapbox/components/ui/card/card.tsx +++ b/app/soapbox/components/ui/card/card.tsx @@ -47,7 +47,10 @@ interface ICardHeader { onBackClick?: (event: React.MouseEvent) => void } -/** Typically holds a CardTitle. */ +/** + * Card header container with back button. + * Typically holds a CardTitle. + */ const CardHeader: React.FC = ({ children, backHref, onBackClick }): JSX.Element => { const intl = useIntl(); diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index 54de440a4..b78dfe823 100644 --- a/app/soapbox/components/ui/modal/modal.tsx +++ b/app/soapbox/components/ui/modal/modal.tsx @@ -20,7 +20,7 @@ interface IModal { /** Whether the confirmation button is disabled. */ confirmationDisabled?: boolean, /** Confirmation button text. */ - confirmationText?: string, + confirmationText?: React.ReactNode, /** Confirmation button theme. */ confirmationTheme?: 'danger', /** Callback when the modal is closed. */ @@ -28,7 +28,7 @@ interface IModal { /** Callback when the secondary action is chosen. */ secondaryAction?: () => void, /** Secondary button text. */ - secondaryText?: string, + secondaryText?: React.ReactNode, /** Don't focus the "confirm" button on mount. */ skipFocus?: boolean, /** Title text for the modal. */ diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 028f47ef5..66541e8ed 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -85,6 +85,7 @@ const SoapboxMount = () => { const systemTheme = useSystemTheme(); const userTheme = settings.get('themeMode'); const darkMode = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark'); + const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true; const themeCss = generateThemeCss(soapboxConfig); @@ -160,20 +161,38 @@ const SoapboxMount = () => { - {waitlisted && } />} + {/* Redirect signup route depending on Pepe enablement. */} + {/* We should prefer using /signup in components. */} + {pepeEnabled ? ( + + ) : ( + + )} + + {waitlisted && ( + } /> + )} {!me && (singleUserMode ? : )} - {!me && } + {!me && ( + + )} + + {(features.accountCreation && instance.registrations) && ( )} - + + {pepeEnabled && ( + + )} + diff --git a/app/soapbox/features/public_layout/components/header.tsx b/app/soapbox/features/public_layout/components/header.tsx index 2691df1e8..098994613 100644 --- a/app/soapbox/features/public_layout/components/header.tsx +++ b/app/soapbox/features/public_layout/components/header.tsx @@ -112,7 +112,7 @@ const Header = () => { {(isOpen || pepeEnabled && pepeOpen) && ( + + + + ); +}; + +export default ThreadLoginCta; diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 58d9c3b21..25247ac6f 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -19,7 +19,7 @@ import { getSettings } from 'soapbox/actions/settings'; import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import ScrollableList from 'soapbox/components/scrollable_list'; 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 PendingStatus from 'soapbox/features/ui/components/pending_status'; @@ -60,6 +60,7 @@ import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from import ActionBar from './components/action-bar'; import DetailedStatus from './components/detailed-status'; +import ThreadLoginCta from './components/thread-login-cta'; import ThreadStatus from './components/thread-status'; import type { AxiosError } from 'axios'; @@ -72,6 +73,7 @@ import type { Attachment as AttachmentEntity, Status as StatusEntity, } from 'soapbox/types/entities'; +import type { Me } from 'soapbox/types/soapbox'; const messages = defineMessages({ title: { id: 'status.title', defaultMessage: '@{username}\'s Post' }, @@ -181,6 +183,7 @@ interface IStatus extends RouteComponentProps, IntlComponentProps { allowedEmoji: ImmutableList, onOpenMedia: (media: ImmutableList, index: number) => void, onOpenVideo: (video: AttachmentEntity, time: number) => void, + me: Me, } interface IStatusState { @@ -669,7 +672,7 @@ class Status extends ImmutablePureComponent { } render() { - const { status, ancestorsIds, descendantsIds, intl } = this.props; + const { me, status, ancestorsIds, descendantsIds, intl } = this.props; const hasAncestors = ancestorsIds && ancestorsIds.size > 0; const hasDescendants = descendantsIds && descendantsIds.size > 0; @@ -782,16 +785,20 @@ class Status extends ImmutablePureComponent { -
- } - > - {children} - -
+ +
+ } + > + {children} + +
+ + {!me && } +
); } diff --git a/app/soapbox/features/ui/components/modals/landing-page-modal.tsx b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx index 44ded2ae6..4f3156ef5 100644 --- a/app/soapbox/features/ui/components/modals/landing-page-modal.tsx +++ b/app/soapbox/features/ui/components/modals/landing-page-modal.tsx @@ -63,7 +63,7 @@ const LandingPageModal: React.FC = ({ onClose }) => { {(isOpen || pepeEnabled && pepeOpen) && ( - )} diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index 7e76e9844..4b2b69889 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -70,7 +70,7 @@ const Navbar = () => { {!singleUserMode && ( - )} diff --git a/app/soapbox/features/ui/components/panels/sign-up-panel.tsx b/app/soapbox/features/ui/components/panels/sign-up-panel.tsx index 4cabf17e8..cafcdc5c4 100644 --- a/app/soapbox/features/ui/components/panels/sign-up-panel.tsx +++ b/app/soapbox/features/ui/components/panels/sign-up-panel.tsx @@ -23,7 +23,7 @@ const SignUpPanel = () => { - diff --git a/app/soapbox/features/ui/components/unauthorized_modal.js b/app/soapbox/features/ui/components/unauthorized_modal.js deleted file mode 100644 index cf2c4d79e..000000000 --- a/app/soapbox/features/ui/components/unauthorized_modal.js +++ /dev/null @@ -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 = ; - button = ; - } else if (action === 'REPLY') { - header = ; - button = ; - } else if (action === 'REBLOG') { - header = ; - button = ; - } else if (action === 'FAVOURITE') { - header = ; - button = ; - } else if (action === 'POLL_VOTE') { - header = ; - button = ; - } - - return ( - } - secondaryAction={this.onRegister} - secondaryText={} - > -
-
- - -
-
- - - -
- {!singleUserMode && ( - - - - )} -
-
- ); - } - - render() { - const { features, siteTitle, action } = this.props; - - if (action && features.remoteInteractionsAPI && features.federating) return this.renderRemoteInteractions(); - - return ( - } - onClose={this.onClickClose} - confirmationAction={this.onLogin} - confirmationText={} - secondaryAction={this.onRegister} - secondaryText={} - > - - - - - - - ); - } - -} - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(UnauthorizedModal)); diff --git a/app/soapbox/features/ui/components/unauthorized_modal.tsx b/app/soapbox/features/ui/components/unauthorized_modal.tsx new file mode 100644 index 000000000..41faf7c1a --- /dev/null +++ b/app/soapbox/features/ui/components/unauthorized_modal.tsx @@ -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 = ({ 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 = 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 = ; + button = ; + } else if (action === 'REPLY') { + header = ; + button = ; + } else if (action === 'REBLOG') { + header = ; + button = ; + } else if (action === 'FAVOURITE') { + header = ; + button = ; + } else if (action === 'POLL_VOTE') { + header = ; + button = ; + } + + return ( + } + secondaryAction={onRegister} + secondaryText={} + > +
+
+ + +
+
+ + + +
+ {!singleUserMode && ( + + + + )} +
+
+ ); + }; + + if (action && features.remoteInteractionsAPI && features.federating) { + return renderRemoteInteractions(); + } + + return ( + } + onClose={onClickClose} + confirmationAction={onLogin} + confirmationText={} + secondaryAction={onRegister} + secondaryText={} + > + + + + + + + ); +}; + +export default UnauthorizedModal;