From 9f89c31bd334ece0e64eacf8764fb51daf11e6ed Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 May 2022 14:35:56 -0500 Subject: [PATCH 1/5] Create a logged-out call-to-action on threads --- app/soapbox/components/ui/card/card.tsx | 5 ++- .../status/components/thread-login-cta.tsx | 36 +++++++++++++++++++ app/soapbox/features/status/index.tsx | 31 +++++++++------- 3 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 app/soapbox/features/status/components/thread-login-cta.tsx 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/features/status/components/thread-login-cta.tsx b/app/soapbox/features/status/components/thread-login-cta.tsx new file mode 100644 index 000000000..2626aeb3c --- /dev/null +++ b/app/soapbox/features/status/components/thread-login-cta.tsx @@ -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 ( + + + } /> + + + + + + + + + + + ); +}; + +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 && } +
); } From 64cce966a2a9535c4eaa1ca3827acef52889407e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 May 2022 14:50:53 -0500 Subject: [PATCH 2/5] Simplify signup routing --- app/soapbox/containers/soapbox.tsx | 25 ++++++++++++++++--- .../public_layout/components/header.tsx | 2 +- .../components/modals/landing-page-modal.tsx | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index 500a7b67d..7386db1b6 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -86,6 +86,7 @@ const SoapboxMount = () => { const [isSystemDarkMode, setSystemDarkMode] = useState(colorSchemeQueryList.matches); const userTheme = settings.get('themeMode'); const darkMode = userTheme === 'dark' || (userTheme === 'system' && isSystemDarkMode); + const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true; const themeCss = generateThemeCss(soapboxConfig); @@ -171,20 +172,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 63e03c472..448de77f7 100644 --- a/app/soapbox/features/public_layout/components/header.tsx +++ b/app/soapbox/features/public_layout/components/header.tsx @@ -104,7 +104,7 @@ const Header = () => { {(isOpen || pepeEnabled && pepeOpen) && ( {(isOpen || pepeEnabled && pepeOpen) && ( - )} From 31486b1320525c09f84db0a1fd31a4f740cc0a1c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 May 2022 14:55:40 -0500 Subject: [PATCH 3/5] Link various sign up buttons to /signup --- app/soapbox/features/ui/components/navbar.tsx | 2 +- app/soapbox/features/ui/components/panels/sign-up-panel.tsx | 2 +- app/soapbox/features/ui/components/unauthorized_modal.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/ui/components/navbar.tsx b/app/soapbox/features/ui/components/navbar.tsx index 25085f6d7..27b5f91dc 100644 --- a/app/soapbox/features/ui/components/navbar.tsx +++ b/app/soapbox/features/ui/components/navbar.tsx @@ -85,7 +85,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 index cf2c4d79e..ea32abca6 100644 --- a/app/soapbox/features/ui/components/unauthorized_modal.js +++ b/app/soapbox/features/ui/components/unauthorized_modal.js @@ -102,7 +102,7 @@ class UnauthorizedModal extends ImmutablePureComponent { onRegister = (e) => { e.preventDefault(); - this.props.history.push('/'); + this.props.history.push('/signup'); this.onClickClose(); } From 2a9f1ccb91eba3d426ce09856e6af923fb8d299f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 11 May 2022 15:26:37 -0500 Subject: [PATCH 4/5] UnauthorizedModal: convert to TSX --- app/soapbox/components/ui/modal/modal.tsx | 4 +- .../ui/components/unauthorized_modal.js | 196 ------------------ .../ui/components/unauthorized_modal.tsx | 154 ++++++++++++++ 3 files changed, 156 insertions(+), 198 deletions(-) delete mode 100644 app/soapbox/features/ui/components/unauthorized_modal.js create mode 100644 app/soapbox/features/ui/components/unauthorized_modal.tsx diff --git a/app/soapbox/components/ui/modal/modal.tsx b/app/soapbox/components/ui/modal/modal.tsx index ab714b93c..d633897e1 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/features/ui/components/unauthorized_modal.js b/app/soapbox/features/ui/components/unauthorized_modal.js deleted file mode 100644 index ea32abca6..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('/signup'); - 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; From d94d946216af8b65c96afc1fdfc3db1ceb401b7d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 12 May 2022 14:47:19 -0500 Subject: [PATCH 5/5] Valor pleases you, Crom --- app/soapbox/containers/soapbox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/containers/soapbox.tsx b/app/soapbox/containers/soapbox.tsx index ea7fcac68..66541e8ed 100644 --- a/app/soapbox/containers/soapbox.tsx +++ b/app/soapbox/containers/soapbox.tsx @@ -165,7 +165,7 @@ const SoapboxMount = () => { {/* We should prefer using /signup in components. */} {pepeEnabled ? ( - ): ( + ) : ( )}