From 1315c149c0c6159cf7b7091b31accedb714a648f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 22 Oct 2022 23:18:32 +0200 Subject: [PATCH] [Glitch] Add error boundary around routes in web UI Port a43a8237681187f6e56524aa3effcfc998a877de to glitch-soc Co-authored-by: Yamagishi Kazutoshi Signed-off-by: Claire --- .../ui/components/bundle_column_error.js | 164 +++++++++++++++--- .../flavours/glitch/features/ui/index.js | 6 +- .../features/ui/util/react_router_helpers.js | 34 +++- .../glitch/styles/components/columns.scss | 41 ++++- .../glitch/styles/components/index.scss | 9 + .../styles/components/single_column.scss | 3 +- 6 files changed, 222 insertions(+), 35 deletions(-) diff --git a/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js index 382481905..7cbe1413d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js +++ b/app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js @@ -1,45 +1,155 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; - +import { injectIntl, FormattedMessage } from 'react-intl'; import Column from 'flavours/glitch/components/column'; -import ColumnHeader from 'flavours/glitch/components/column_header'; -import IconButton from 'flavours/glitch/components/icon_button'; +import Button from 'flavours/glitch/components/button'; import { Helmet } from 'react-helmet'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/initial_state'; -const messages = defineMessages({ - title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' }, - body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this component.' }, - retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' }, -}); - -class BundleColumnError extends React.Component { +class GIF extends React.PureComponent { static propTypes = { - onRetry: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - multiColumn: PropTypes.bool, + src: PropTypes.string.isRequired, + staticSrc: PropTypes.string.isRequired, + className: PropTypes.string, + animate: PropTypes.bool, + }; + + static defaultProps = { + animate: autoPlayGif, + }; + + state = { + hovering: false, + }; + + handleMouseEnter = () => { + const { animate } = this.props; + + if (!animate) { + this.setState({ hovering: true }); + } } - handleRetry = () => { - this.props.onRetry(); + handleMouseLeave = () => { + const { animate } = this.props; + + if (!animate) { + this.setState({ hovering: false }); + } } render () { - const { multiColumn, intl: { formatMessage } } = this.props; + const { src, staticSrc, className, animate } = this.props; + const { hovering } = this.state; return ( - - + + ); + } +} + +class CopyButton extends React.PureComponent { + + static propTypes = { + children: PropTypes.node.isRequired, + value: PropTypes.string.isRequired, + }; + + state = { + copied: false, + }; + + handleClick = () => { + const { value } = this.props; + navigator.clipboard.writeText(value); + this.setState({ copied: true }); + this.timeout = setTimeout(() => this.setState({ copied: false }), 700); + } + + componentWillUnmount () { + if (this.timeout) clearTimeout(this.timeout); + } + + render () { + const { children } = this.props; + const { copied } = this.state; + + return ( + + ); + } + +} + +export default @injectIntl +class BundleColumnError extends React.PureComponent { + + static propTypes = { + errorType: PropTypes.oneOf(['routing', 'network', 'error']), + onRetry: PropTypes.func, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + stacktrace: PropTypes.string, + }; + + static defaultProps = { + errorType: 'routing', + }; + + handleRetry = () => { + const { onRetry } = this.props; + + if (onRetry) { + onRetry(); + } + } + + render () { + const { errorType, multiColumn, stacktrace } = this.props; + + let title, body; + + switch(errorType) { + case 'routing': + title = ; + body = ; + break; + case 'network': + title = ; + body = ; + break; + case 'error': + title = ; + body = ; + break; + } + + return ( +
- - {formatMessage(messages.body)} + + +
+

{title}

+

{body}

+ +
+ {errorType === 'network' && } + {errorType === 'error' && } + +
+
@@ -50,5 +160,3 @@ class BundleColumnError extends React.Component { } } - -export default injectIntl(BundleColumnError); diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 5c567be42..9ca946142 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import LoadingBarContainer from './containers/loading_bar_container'; import ModalContainer from './containers/modal_container'; import { connect } from 'react-redux'; -import { Redirect, withRouter } from 'react-router-dom'; +import { Redirect, Route, withRouter } from 'react-router-dom'; import { layoutFromWindow } from 'flavours/glitch/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from 'flavours/glitch/actions/compose'; @@ -15,6 +15,7 @@ import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { changeLayout } from 'flavours/glitch/actions/app'; import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; +import BundleColumnError from './components/bundle_column_error'; import UploadArea from './components/upload_area'; import PermaLink from 'flavours/glitch/components/permalink'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -39,7 +40,6 @@ import { HashtagTimeline, Notifications, FollowRequests, - GenericNotFound, FavouritedStatuses, BookmarkedStatuses, ListTimeline, @@ -233,7 +233,7 @@ class SwitchingColumnsArea extends React.PureComponent { - + ); diff --git a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js index 60a81a581..8946c8252 100644 --- a/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js +++ b/app/javascript/flavours/glitch/features/ui/util/react_router_helpers.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Switch, Route } from 'react-router-dom'; - +import StackTrace from 'stacktrace-js'; import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading'; import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error'; import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container'; @@ -42,8 +42,38 @@ export class WrappedRoute extends React.Component { componentParams: {}, }; + static getDerivedStateFromError () { + return { + hasError: true, + }; + }; + + state = { + hasError: false, + stacktrace: '', + }; + + componentDidCatch (error) { + StackTrace.fromError(error).then(stackframes => { + this.setState({ stacktrace: error.toString() + '\n' + stackframes.map(frame => frame.toString()).join('\n') }); + }).catch(err => { + console.error(err); + }); + } + renderComponent = ({ match }) => { const { component, content, multiColumn, componentParams } = this.props; + const { hasError, stacktrace } = this.state; + + if (hasError) { + return ( + + ); + } return ( @@ -59,7 +89,7 @@ export class WrappedRoute extends React.Component { } renderError = (props) => { - return ; + return ; } render () { diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 75891ad2e..5de8547e9 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -627,7 +627,6 @@ $ui-header-height: 55px; } .empty-column-indicator, -.error-column, .follow_requests-unlocked_explanation { color: $dark-text-color; background: $ui-base-color; @@ -664,7 +663,47 @@ $ui-header-height: 55px; } .error-column { + padding: 20px; + background: $ui-base-color; + border-radius: 4px; + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; flex-direction: column; + cursor: default; + + &__image { + max-width: 350px; + margin-top: -50px; + } + + &__message { + text-align: center; + color: $darker-text-color; + font-size: 15px; + line-height: 22px; + + h1 { + font-size: 28px; + line-height: 33px; + font-weight: 700; + margin-bottom: 15px; + color: $primary-text-color; + } + + p { + max-width: 48ch; + } + + &__actions { + margin-top: 30px; + display: flex; + gap: 10px; + align-items: center; + justify-content: center; + } + } } // more fixes for the navbar-under mode diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 81d90f442..8877626b8 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -189,6 +189,15 @@ color: $highlight-text-color; } + &.copyable { + transition: background 300ms linear; + } + + &.copied { + background: $valid-value-color; + transition: none; + } + &::-moz-focus-inner { border: 0; } diff --git a/app/javascript/flavours/glitch/styles/components/single_column.scss b/app/javascript/flavours/glitch/styles/components/single_column.scss index 3befca567..1725a5480 100644 --- a/app/javascript/flavours/glitch/styles/components/single_column.scss +++ b/app/javascript/flavours/glitch/styles/components/single_column.scss @@ -322,7 +322,8 @@ .column-header, .column-back-button, - .scrollable { + .scrollable, + .error-column { border-radius: 0 !important; } }