Merge pull request #3512 from matrix-org/jryans/error-boundary
Add error boundaries to catch rendering errors
This commit is contained in:
commit
ae7ecf7f8f
7 changed files with 242 additions and 99 deletions
|
@ -1808,28 +1808,26 @@ export default createReactClass({
|
|||
render: function() {
|
||||
// console.log(`Rendering MatrixChat with view ${this.state.view}`);
|
||||
|
||||
let view;
|
||||
|
||||
if (
|
||||
this.state.view === VIEWS.LOADING ||
|
||||
this.state.view === VIEWS.LOGGING_IN
|
||||
) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return (
|
||||
view = (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||
} else if (this.state.view === VIEWS.POST_REGISTRATION) {
|
||||
// needs to be before normal PageTypes as you are logged in technically
|
||||
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
|
||||
return (
|
||||
view = (
|
||||
<PostRegistration
|
||||
onComplete={this.onFinishPostRegistration} />
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
} else if (this.state.view === VIEWS.LOGGED_IN) {
|
||||
// store errors stop the client syncing and require user intervention, so we'll
|
||||
// be showing a dialog. Don't show anything else.
|
||||
const isStoreError = this.state.syncError && this.state.syncError instanceof Matrix.InvalidStoreError;
|
||||
|
@ -1843,8 +1841,8 @@ export default createReactClass({
|
|||
* as using something like redux to avoid having a billion bits of state kicking around.
|
||||
*/
|
||||
const LoggedInView = sdk.getComponent('structures.LoggedInView');
|
||||
return (
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
view = (
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
|
@ -1863,26 +1861,22 @@ export default createReactClass({
|
|||
{messageForSyncError(this.state.syncError)}
|
||||
</div>;
|
||||
}
|
||||
return (
|
||||
view = (
|
||||
<div className="mx_MatrixChat_splash">
|
||||
{errorBox}
|
||||
<Spinner />
|
||||
<a href="#" className="mx_MatrixChat_splashButtons" onClick={this.onLogoutClick}>
|
||||
{ _t('Logout') }
|
||||
{_t('Logout')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.WELCOME) {
|
||||
} else if (this.state.view === VIEWS.WELCOME) {
|
||||
const Welcome = sdk.getComponent('auth.Welcome');
|
||||
return <Welcome />;
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.REGISTER) {
|
||||
view = <Welcome />;
|
||||
} else if (this.state.view === VIEWS.REGISTER) {
|
||||
const Registration = sdk.getComponent('structures.auth.Registration');
|
||||
return (
|
||||
view = (
|
||||
<Registration
|
||||
clientSecret={this.state.register_client_secret}
|
||||
sessionId={this.state.register_session_id}
|
||||
|
@ -1896,12 +1890,9 @@ export default createReactClass({
|
|||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (this.state.view === VIEWS.FORGOT_PASSWORD) {
|
||||
} else if (this.state.view === VIEWS.FORGOT_PASSWORD) {
|
||||
const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword');
|
||||
return (
|
||||
view = (
|
||||
<ForgotPassword
|
||||
onComplete={this.onLoginClick}
|
||||
onLoginClick={this.onLoginClick}
|
||||
|
@ -1909,11 +1900,9 @@ export default createReactClass({
|
|||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.LOGIN) {
|
||||
} else if (this.state.view === VIEWS.LOGIN) {
|
||||
const Login = sdk.getComponent('structures.auth.Login');
|
||||
return (
|
||||
view = (
|
||||
<Login
|
||||
onLoggedIn={Lifecycle.setLoggedIn}
|
||||
onRegisterClick={this.onRegisterClick}
|
||||
|
@ -1924,18 +1913,21 @@ export default createReactClass({
|
|||
{...this.getServerProperties()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.state.view === VIEWS.SOFT_LOGOUT) {
|
||||
} else if (this.state.view === VIEWS.SOFT_LOGOUT) {
|
||||
const SoftLogout = sdk.getComponent('structures.auth.SoftLogout');
|
||||
return (
|
||||
view = (
|
||||
<SoftLogout
|
||||
realQueryParams={this.props.realQueryParams}
|
||||
onTokenLoginCompleted={this.props.onTokenLoginCompleted}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
console.error(`Unknown view ${this.state.view}`);
|
||||
}
|
||||
|
||||
console.error(`Unknown view ${this.state.view}`);
|
||||
const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
|
||||
return <ErrorBoundary>
|
||||
{view}
|
||||
</ErrorBoundary>;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1566,20 +1566,23 @@ module.exports = createReactClass({
|
|||
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
|
||||
const RoomUpgradeWarningBar = sdk.getComponent("rooms.RoomUpgradeWarningBar");
|
||||
const RoomRecoveryReminder = sdk.getComponent("rooms.RoomRecoveryReminder");
|
||||
const ErrorBoundary = sdk.getComponent("elements.ErrorBoundary");
|
||||
|
||||
if (!this.state.room) {
|
||||
const loading = this.state.roomLoading || this.state.peekLoading;
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
previewLoading={this.state.peekLoading}
|
||||
error={this.state.roomLoadError}
|
||||
loading={loading}
|
||||
joining={this.state.joining}
|
||||
oobData={this.props.oobData}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
@ -1597,18 +1600,20 @@ module.exports = createReactClass({
|
|||
const roomAlias = this.state.roomAlias;
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
oobData={this.props.oobData}
|
||||
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectThreepidInviteButtonClicked}
|
||||
canPreview={false} error={this.state.roomLoadError}
|
||||
roomAlias={roomAlias}
|
||||
joining={this.state.joining}
|
||||
inviterName={inviterName}
|
||||
invitedEmail={invitedEmail}
|
||||
oobData={this.props.oobData}
|
||||
signUrl={this.props.thirdPartyInvite ? this.props.thirdPartyInvite.inviteSignUrl : null}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1618,12 +1623,14 @@ module.exports = createReactClass({
|
|||
if (myMembership == 'invite') {
|
||||
if (this.state.joining || this.state.rejecting) {
|
||||
return (
|
||||
<RoomPreviewBar
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar
|
||||
canPreview={false}
|
||||
error={this.state.roomLoadError}
|
||||
joining={this.state.joining}
|
||||
rejecting={this.state.rejecting}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
} else {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
@ -1638,14 +1645,16 @@ module.exports = createReactClass({
|
|||
// We have a regular invite for this room.
|
||||
return (
|
||||
<div className="mx_RoomView">
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<RoomPreviewBar onJoinClick={this.onJoinButtonClicked}
|
||||
onForgetClick={this.onForgetClick}
|
||||
onRejectClick={this.onRejectButtonClicked}
|
||||
inviterName={inviterName}
|
||||
canPreview={false}
|
||||
joining={this.state.joining}
|
||||
room={this.state.room}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1942,41 +1951,43 @@ module.exports = createReactClass({
|
|||
|
||||
return (
|
||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{ auxPanel }
|
||||
<div className="mx_RoomView_timeline">
|
||||
{ topUnreadMessagesBar }
|
||||
{ jumpToBottom }
|
||||
{ messagePanel }
|
||||
{ searchResultsPanel }
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{ statusBar }
|
||||
<ErrorBoundary>
|
||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === 'join'}
|
||||
collapsedRhs={collapsedRhs}
|
||||
onSearchClick={this.onSearchClick}
|
||||
onSettingsClick={this.onSettingsClick}
|
||||
onPinnedClick={this.onPinnedClick}
|
||||
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
|
||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
/>
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
collapsedRhs={collapsedRhs}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
<div className={fadableSectionClasses}>
|
||||
{auxPanel}
|
||||
<div className="mx_RoomView_timeline">
|
||||
{topUnreadMessagesBar}
|
||||
{jumpToBottom}
|
||||
{messagePanel}
|
||||
{searchResultsPanel}
|
||||
</div>
|
||||
<div className={statusBarAreaClass}>
|
||||
<div className="mx_RoomView_statusAreaBox">
|
||||
<div className="mx_RoomView_statusAreaBox_line"></div>
|
||||
{statusBar}
|
||||
</div>
|
||||
</div>
|
||||
{previewBar}
|
||||
{messageComposer}
|
||||
</div>
|
||||
{ previewBar }
|
||||
{ messageComposer }
|
||||
</div>
|
||||
</MainSplit>
|
||||
</MainSplit>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
);
|
||||
},
|
||||
|
|
104
src/components/views/elements/ErrorBoundary.js
Normal file
104
src/components/views/elements/ErrorBoundary.js
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
||||
/**
|
||||
* This error boundary component can be used to wrap large content areas and
|
||||
* catch exceptions during rendering in the component tree below them.
|
||||
*/
|
||||
export default class ErrorBoundary extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
// Side effects are not permitted here, so we only update the state so
|
||||
// that the next render shows an error message.
|
||||
return { error };
|
||||
}
|
||||
|
||||
componentDidCatch(error, { componentStack }) {
|
||||
// Browser consoles are better at formatting output when native errors are passed
|
||||
// in their own `console.error` invocation.
|
||||
console.error(error);
|
||||
console.error(
|
||||
"The above error occured while React was rendering the following components:",
|
||||
componentStack,
|
||||
);
|
||||
}
|
||||
|
||||
_onClearCacheAndReload = () => {
|
||||
if (!PlatformPeg.get()) return;
|
||||
|
||||
MatrixClientPeg.get().stopClient();
|
||||
MatrixClientPeg.get().store.deleteAllData().done(() => {
|
||||
PlatformPeg.get().reload();
|
||||
});
|
||||
};
|
||||
|
||||
_onBugReport = () => {
|
||||
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
|
||||
if (!BugReportDialog) {
|
||||
return;
|
||||
}
|
||||
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new";
|
||||
return <div className="mx_ErrorBoundary">
|
||||
<div className="mx_ErrorBoundary_body">
|
||||
<h1>{_t("Something went wrong!")}</h1>
|
||||
<p>{_t(
|
||||
"Please <newIssueLink>create a new issue</newIssueLink> " +
|
||||
"on GitHub so that we can investigate this bug.", {}, {
|
||||
newIssueLink: (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener" href={newIssueUrl}>{ sub }</a>;
|
||||
},
|
||||
},
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"If you've submitted a bug via GitHub, debug logs can help " +
|
||||
"us track down the problem. Debug logs contain application " +
|
||||
"usage data including your username, the IDs or aliases of " +
|
||||
"the rooms or groups you have visited and the usernames of " +
|
||||
"other users. They do not contain messages.",
|
||||
)}</p>
|
||||
<AccessibleButton onClick={this._onBugReport} kind='primary'>
|
||||
{_t("Submit debug logs")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
|
@ -226,7 +226,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
|||
</div>
|
||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
|
||||
{_t("Clear Cache and Reload")}
|
||||
{_t("Clear cache and reload")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue