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 17:15:19 +00: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 { type QrLoginCredentials } from "../views/auth/LoginWithQR.tsx";
import { configureFromCompletedOAuthLogin } from "../../Lifecycle"; 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"]; 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 // 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: * Called when:
* *
* - We successfully completed an OIDC or token login, via {@link initSession}. * - 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. * registration.
* *
* In both cases, {@link Action.OnLoggedIn} will already have been emitted, but the call to {@link onShowPostLoginScreen} will * 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(); this.viewLogin();
break; break;
case "start_password_recovery": case "start_password_recovery":
this.setStateForNewView({ if (SettingsStore.getValue(UIFeature.PasswordReset)) {
view: Views.FORGOT_PASSWORD, this.setStateForNewView({
}); view: Views.FORGOT_PASSWORD,
this.notifyNewScreen("forgot_password"); });
this.notifyNewScreen("forgot_password");
}
break; break;
case "start_chat": case "start_chat":
createRoom(MatrixClientPeg.safeGet(), { createRoom(MatrixClientPeg.safeGet(), {
@@ -2028,6 +2027,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} else if (ModuleApi.instance.navigation.locationRenderers.get(screen)) { } else if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
this.setState({ page_type: 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; return fragmentAfterLogin;
} }
public render(): React.ReactNode { private getView(): JSX.Element {
const fragmentAfterLogin = this.getFragmentAfterLogin(); const fragmentAfterLogin = this.getFragmentAfterLogin();
let view: JSX.Element;
if (this.state.view === Views.LOADING) { switch (this.state.view) {
view = ( case Views.LOADING:
<div className="mx_MatrixChat_splash"> return (
<Spinner /> <div className="mx_MatrixChat_splash">
</div> <Spinner />
); </div>
} else if (this.state.view === Views.CONFIRM_LOCK_THEFT) { );
view = ( case Views.CONFIRM_LOCK_THEFT:
<ConfirmSessionLockTheftView return (
onConfirm={() => { <ConfirmSessionLockTheftView
this.setState({ view: Views.LOADING }); onConfirm={() => {
this.startInitSession(); 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}
/> />
); );
} 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 // we think we are logged in, but are still waiting for the /sync to complete
view = ( return (
<LoginSplashView <LoginSplashView
matrixClient={MatrixClientPeg.safeGet()} matrixClient={MatrixClientPeg.safeGet()}
onLogoutClick={this.onLogoutClick} onLogoutClick={this.onLogoutClick}
syncError={this.state.syncError} syncError={this.state.syncError}
/> />
); );
} case Views.LOGGED_IN:
} else if (this.state.view === Views.WELCOME) { // `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
view = <Welcome {...this.getServerProperties()} />; // latter is set via the dispatcher). If we don't yet have a `page_type`,
} else if (this.state.view === Views.REGISTER && SettingsStore.getValue(UIFeature.Registration)) { // keep showing the spinner for now.
const email = ThreepidInviteStore.instance.pickBestInvite()?.toEmail; if (this.state.ready && this.state.page_type) {
view = ( /* for now, we stuff the entirety of our props and state into the LoggedInView.
<Registration * we should go through and figure out what we actually need to pass down, as well
clientSecret={this.state.register_client_secret} * as using something like redux to avoid having a billion bits of state kicking around.
sessionId={this.state.register_session_id} */
idSid={this.state.register_id_sid} return (
email={email} <LoggedInView
brand={this.props.config.brand} {...this.props}
onLoggedIn={this.onRegisterFlowComplete} {...this.state}
onLoginClick={this.onLoginClick} ref={this.loggedInView}
onServerConfigChange={this.onServerConfigChange} matrixClient={MatrixClientPeg.safeGet()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} onRegistered={this.onRegistered}
fragmentAfterLogin={fragmentAfterLogin} currentRoomId={this.state.currentRoomId}
mobileRegister={this.state.isMobileRegistration} />
{...this.getServerProperties()} );
/> } else {
); // we think we are logged in, but are still waiting for the /sync to complete
} else if (this.state.view === Views.FORGOT_PASSWORD && SettingsStore.getValue(UIFeature.PasswordReset)) { return (
view = ( <LoginSplashView
<ForgotPassword matrixClient={MatrixClientPeg.safeGet()}
onComplete={this.onLoginClick} onLogoutClick={this.onLogoutClick}
onLoginClick={this.onLoginClick} syncError={this.state.syncError}
{...this.getServerProperties()} />
/> );
); }
} else if (this.state.view === Views.LOGIN) { case Views.WELCOME:
const showPasswordReset = SettingsStore.getValue(UIFeature.PasswordReset); return <Welcome {...this.getServerProperties()} />;
view = ( case Views.REGISTER:
<Login return (
isSyncing={this.state.pendingInitialSync} <Registration
onLoggedIn={this.onUserCompletedLoginFlow} clientSecret={this.state.register_client_secret}
onRegisterClick={this.onRegisterClick} sessionId={this.state.register_session_id}
fallbackHsUrl={this.getFallbackHsUrl()} idSid={this.state.register_id_sid}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} email={ThreepidInviteStore.instance.pickBestInvite()?.toEmail}
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} brand={this.props.config.brand}
onServerConfigChange={this.onServerConfigChange} onLoggedIn={this.onRegisterFlowComplete}
fragmentAfterLogin={fragmentAfterLogin} onLoginClick={this.onLoginClick}
defaultUsername={this.props.urlParams?.defaults?.defaultUsername} onServerConfigChange={this.onServerConfigChange}
{...this.getServerProperties()} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
/> fragmentAfterLogin={fragmentAfterLogin}
); mobileRegister={this.state.isMobileRegistration}
} else if (this.state.view === Views.SOFT_LOGOUT) { {...this.getServerProperties()}
view = ( />
<SoftLogout );
urlParams={this.props.urlParams} case Views.FORGOT_PASSWORD:
onTokenLoginCompleted={this.props.onTokenLoginCompleted} return (
fragmentAfterLogin={fragmentAfterLogin} <ForgotPassword
/> onComplete={this.onLoginClick}
); onLoginClick={this.onLoginClick}
} else if (this.state.view === Views.LOCK_STOLEN) { {...this.getServerProperties()}
view = <SessionLockStolenView />; />
} else { );
logger.error(`Unknown view ${this.state.view}`); case Views.LOGIN:
return null; 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 ( return (
<ErrorBoundary> <ErrorBoundary>