Handle unknown screens better (#33793)

* Remove legacy export

* Extract view map to a switch case statement with typescript checking that all cases are handled

This lets us drop the `default` case as we know we will never reach it.
Additionally move all the settings conditions to the state-setters rather than the renderer so we are never stuck in an undefined state

* Fix comment

* Handle unknown screens better

Redirect to welcome/home rather than getting stuck at spinner
This commit is contained in:
Michael Telatynski
2026-06-09 18:15:19 +01:00
committed by GitHub
parent 6cac2730fd
commit 67295e2334
+118 -115
View File
@@ -145,9 +145,6 @@ import { type URLParams } from "../../vector/url_utils.ts";
import { type QrLoginCredentials } from "../views/auth/LoginWithQR.tsx";
import { configureFromCompletedOAuthLogin } from "../../Lifecycle";
// legacy export
export { default as Views } from "../../Views";
const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
// Actions that are redirected through the onboarding process prior to being
@@ -404,7 +401,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* Called when:
*
* - We successfully completed an OIDC or token login, via {@link initSession}.
* - The {@link Login} or {@link Register} components notify us that we successfully completed a non-OIDC login or
* - The {@link Login} or {@link Registration} components notify us that we successfully completed a non-OIDC login or
* registration.
*
* In both cases, {@link Action.OnLoggedIn} will already have been emitted, but the call to {@link onShowPostLoginScreen} will
@@ -739,10 +736,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewLogin();
break;
case "start_password_recovery":
this.setStateForNewView({
view: Views.FORGOT_PASSWORD,
});
this.notifyNewScreen("forgot_password");
if (SettingsStore.getValue(UIFeature.PasswordReset)) {
this.setStateForNewView({
view: Views.FORGOT_PASSWORD,
});
this.notifyNewScreen("forgot_password");
}
break;
case "start_chat":
createRoom(MatrixClientPeg.safeGet(), {
@@ -2028,6 +2027,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
} else if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
this.setState({ page_type: screen });
} else {
// Unknown screen requested
return this.showScreen(isLoggedOutOrGuest ? "welcome" : "home");
}
}
@@ -2212,125 +2214,126 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return fragmentAfterLogin;
}
public render(): React.ReactNode {
private getView(): JSX.Element {
const fragmentAfterLogin = this.getFragmentAfterLogin();
let view: JSX.Element;
if (this.state.view === Views.LOADING) {
view = (
<div className="mx_MatrixChat_splash">
<Spinner />
</div>
);
} else if (this.state.view === Views.CONFIRM_LOCK_THEFT) {
view = (
<ConfirmSessionLockTheftView
onConfirm={() => {
this.setState({ view: Views.LOADING });
this.startInitSession();
}}
/>
);
} else if (this.state.view === Views.COMPLETE_SECURITY) {
view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.E2E_SETUP) {
view = <E2eSetup onCancelled={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.PENDING_CLIENT_START) {
// we think we are logged in, but are still waiting for the /sync to complete
view = (
<LoginSplashView
matrixClient={MatrixClientPeg.safeGet()}
onLogoutClick={this.onLogoutClick}
syncError={this.state.syncError}
/>
);
} else if (this.state.view === Views.LOGGED_IN) {
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
*/
view = (
<LoggedInView
{...this.props}
{...this.state}
ref={this.loggedInView}
matrixClient={MatrixClientPeg.safeGet()}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
switch (this.state.view) {
case Views.LOADING:
return (
<div className="mx_MatrixChat_splash">
<Spinner />
</div>
);
case Views.CONFIRM_LOCK_THEFT:
return (
<ConfirmSessionLockTheftView
onConfirm={() => {
this.setState({ view: Views.LOADING });
this.startInitSession();
}}
/>
);
} else {
case Views.COMPLETE_SECURITY:
return <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
case Views.E2E_SETUP:
return <E2eSetup onCancelled={this.onCompleteSecurityE2eSetupFinished} />;
case Views.PENDING_CLIENT_START:
// we think we are logged in, but are still waiting for the /sync to complete
view = (
return (
<LoginSplashView
matrixClient={MatrixClientPeg.safeGet()}
onLogoutClick={this.onLogoutClick}
syncError={this.state.syncError}
/>
);
}
} else if (this.state.view === Views.WELCOME) {
view = <Welcome {...this.getServerProperties()} />;
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) {
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail;
view = (
<Registration
clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={email}
brand={this.props.config.brand}
onLoggedIn={this.onRegisterFlowComplete}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
fragmentAfterLogin={fragmentAfterLogin}
mobileRegister={this.state.isMobileRegistration}
{...this.getServerProperties()}
/>
);
} else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) {
view = (
<ForgotPassword
onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick}
{...this.getServerProperties()}
/>
);
} else if (this.state.view === Views.LOGIN) {
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset);
view = (
<Login
isSyncing={this.state.pendingInitialSync}
onLoggedIn={this.onUserCompletedLoginFlow}
onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
defaultUsername={this.props.urlParams?.defaults?.defaultUsername}
{...this.getServerProperties()}
/>
);
} else if (this.state.view === Views.SOFT_LOGOUT) {
view = (
<SoftLogout
urlParams={this.props.urlParams}
onTokenLoginCompleted={this.props.onTokenLoginCompleted}
fragmentAfterLogin={fragmentAfterLogin}
/>
);
} else if (this.state.view === Views.LOCK_STOLEN) {
view = <SessionLockStolenView />;
} else {
logger.error(`Unknown view ${this.state.view}`);
return null;
case Views.LOGGED_IN:
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,
// keep showing the spinner for now.
if (this.state.ready && this.state.page_type) {
/* for now, we stuff the entirety of our props and state into the LoggedInView.
* we should go through and figure out what we actually need to pass down, as well
* as using something like redux to avoid having a billion bits of state kicking around.
*/
return (
<LoggedInView
{...this.props}
{...this.state}
ref={this.loggedInView}
matrixClient={MatrixClientPeg.safeGet()}
onRegistered={this.onRegistered}
currentRoomId={this.state.currentRoomId}
/>
);
} else {
// we think we are logged in, but are still waiting for the /sync to complete
return (
<LoginSplashView
matrixClient={MatrixClientPeg.safeGet()}
onLogoutClick={this.onLogoutClick}
syncError={this.state.syncError}
/>
);
}
case Views.WELCOME:
return <Welcome {...this.getServerProperties()} />;
case Views.REGISTER:
return (
<Registration
clientSecret={this.state.register_client_secret}
sessionId={this.state.register_session_id}
idSid={this.state.register_id_sid}
email={ThreepidInviteStore.instance.pickBestInvite()?.toEmail}
brand={this.props.config.brand}
onLoggedIn={this.onRegisterFlowComplete}
onLoginClick={this.onLoginClick}
onServerConfigChange={this.onServerConfigChange}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
fragmentAfterLogin={fragmentAfterLogin}
mobileRegister={this.state.isMobileRegistration}
{...this.getServerProperties()}
/>
);
case Views.FORGOT_PASSWORD:
return (
<ForgotPassword
onComplete={this.onLoginClick}
onLoginClick={this.onLoginClick}
{...this.getServerProperties()}
/>
);
case Views.LOGIN:
return (
<Login
isSyncing={this.state.pendingInitialSync}
onLoggedIn={this.onUserCompletedLoginFlow}
onRegisterClick={this.onRegisterClick}
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={
SettingsStore.getValue(UIFeature.PasswordReset) ? this.onForgotPasswordClick : undefined
}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
defaultUsername={this.props.urlParams?.defaults?.defaultUsername}
{...this.getServerProperties()}
/>
);
case Views.SOFT_LOGOUT:
return (
<SoftLogout
urlParams={this.props.urlParams}
onTokenLoginCompleted={this.props.onTokenLoginCompleted}
fragmentAfterLogin={fragmentAfterLogin}
/>
);
case Views.LOCK_STOLEN:
return <SessionLockStolenView />;
}
}
public render(): React.ReactNode {
const view = this.getView();
return (
<ErrorBoundary>