Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,9 @@ async function abortLogin(): Promise<void> {
}

/** Attempt to restore the session from localStorage or indexeddb.
*
* If the credentials are found, and the session is successfully restored,
* emits {@link Action.OnLoggedIn}, {@link Action.WillStartClient} and {@link Action.StartedClient}.
*
* @returns true if a session was found; false if no existing session was found.
*
Expand Down Expand Up @@ -787,6 +790,8 @@ async function createOidcTokenRefresher(credentials: IMatrixClientCreds): Promis
* optionally clears localstorage, persists new credentials
* to localstorage, starts the new client.
*
* Emits {@link Action.OnLoggedIn}, {@link Action.WillStartClient} and {@link Action.StartedClient}.
*
* @param {IMatrixClientCreds} credentials The credentials to use
* @param {Boolean} clearStorageEnabled True to clear storage before starting the new client
* @param {Boolean} isFreshLogin True if this is a fresh login, false if it is previous session being restored
Expand Down Expand Up @@ -1019,6 +1024,12 @@ export function isLoggingOut(): boolean {
* Starts the matrix client and all other react-sdk services that
* listen for events while a session is logged in.
*
* By the time this method is called, we have successfully logged in if necessary, and the client has been set up with
* the access token.
*
* Emits {@link Acction.WillStartClient} before starting the client, and {@link Action.ClientStarted} when the client has
* been started.
*
* @param client the matrix client to start
* @param startSyncing - `true` to actually start syncing the client.
* @param clientPegOpts - Options to pass through to {@link MatrixClientPeg.start}.
Expand Down
71 changes: 70 additions & 1 deletion src/Views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,76 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/

/** constants for MatrixChat.state.view */
/**
* Constants for MatrixChat.state.view.
*
* The `View` is the primary state machine of the application: it has different states for the various setup flows
* that the user may find themselves in. Once we have a functioning client, we can transition to the `LOGGED_IN` state
* which is the "normal" state of the application.
*
* An incomplete state transition diagram follows.
*
* (initial state)
* ┌─────────────────┐ Lock held by other instance ┌─────────────────┐
* │ LOADING │─────────────────────────────►│ CONFIRM_LOCK_ │
* │ │◄─────────────────────────────│ THEFT │
* └─────────────────┘ Lock theft confirmed └─────────────────┘
* Session recovered │ │ │
* ┌──────────────┘ │ └────────────────┐
* │ ┌─────────────┘ │ No previous session
* │ │ Token/OIDC login succeeded │
* │ │ ▼
* │ │ ┌─────────────────┐
* │ │ │ WELCOME │ (from all other states
* │ │ │ │ except LOCK_STOLEN)
* │ │ └─────────────────┘ │
* │ │ "Create Account" │ │ "Sign in" │ Client logged out
* │ │ ┌────────────────────────┘ │ │
* │ │ │ │ ┌────────────────────┘
* │ │ │ │ │
* │ │ ▼ "Create an ▼ ▼ "Forgot
* │ │ ┌─────────────────┐ account" ┌─────────────────┐ password" ┌─────────────────┐
* │ │ │ REGISTER │◄───────────────│ LOGIN │───────────────►│ FORGOT_PASSWORD │
* │ │ │ │───────────────►│ │◄───────────────│ │
* │ │ └─────────────────┘ "Sign in here" └─────────────────┘ Complete / └─────────────────┘
* │ │ │ │ "Sign in instead" ▲
* │ │ └────────────────────────────────┐ │ │
* │ └────────────────────────────────────────┐ │ │ │
* │ ▼ ▼ ▼ │
* │ ┌──────────────────┐ │
* │ │ (postLoginSetup) │ │
* │ └──────────────────┘ │
* │ ┌────────────────────────────────────┘ │ │ │
* │ │ E2EE not enabled ┌─────────────┘ └──────┐ │
* │ │ │ Account has │ Account lacks │
* │ │ │ cross-signing │ cross-signing │
* │ │ │ keys │ keys │
* │ │ Client started and ▼ ▼ │
* │ │ force_verification ┌─────────────────┐ ┌─────────────────┐ │
* │ │ pending │ COMPLETE_ │ │ E2E_SETUP │ │
* │ │ ┌─────────────────►│ SECURITY │ │ │ │
* │ │ │ └─────────────────┘ └─────────────────┘ │ "Forgotten
* │ │ │ ┌───────────────────────┘ │ │ your
* │ │ │ │ ┌───────────────────────────────────────────────┘ │ password?"
* │ │ │ │ │ │
* │ │ │ │ │ (from all other states │
* │ │ │ │ │ except LOCK_STOLEN) │
* │ │ │ │ │ └──────────────┐ │
* ▼ ▼ │ ▼ ▼ Soft logout error ▼ │
* ┌─────────────────┐ ┌─────────────────┐
* │ LOGGED_IN │ Re-authentication succeeded │ SOFT_LOGOUT │
* │ │◄────────────────────────────────────────────────────────│ │
* └─────────────────┘ └─────────────────┘
*
* (from all other states)
* │
* │ Session lock stolen
* ▼
* ┌─────────────────┐
* │ LOCK_STOLEN │
* │ │
* └─────────────────┘
*/
enum Views {
// a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest.
Expand Down
100 changes: 64 additions & 36 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import PageType from "../../PageTypes";
import createRoom, { type IOpts } from "../../createRoom";
import { _t, _td } from "../../languageHandler";
import SettingsStore from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
import { startAnyRegistrationFlow } from "../../Registration";
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
import { calculateRoomVia, makeRoomPermalink } from "../../utils/permalinks/Permalinks";
Expand Down Expand Up @@ -202,7 +201,10 @@ interface IState {
hideToSRUsers: boolean;
syncError: Error | null;
serverConfig?: ValidatedServerConfig;

/** Has our MatrixClient started? */
ready: boolean;

threepidInvite?: IThreepidInvite;
roomOobData?: object;
pendingInitialSync?: boolean;
Expand All @@ -225,7 +227,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private firstSyncPromise: PromiseWithResolvers<void>;

private screenAfterLogin?: IScreen;

/** True if we have successfully completed an OIDC or token login.
*
* XXX it's unclear if this is ever cleared, so what happens if the user logs out and then logs back in?
*/
private tokenLogin?: boolean;

// What to focus on next component update, if anything
private focusNext: FocusNextType;
private subTitleStatus: string;
Expand Down Expand Up @@ -386,11 +394,30 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
await Lifecycle.onSessionLockStolen();
}

/**
* Perform actions that are specific to a user that has just logged in.
*
* 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
* registration.
*
* In both cases, {@link Action.OnLoggedIn} will already have been emitted, but the call to {@link onShowPostLoginScreen} will
* have been suppressed (by either {@link tokenLogin} being set, or the view being set to {@link Views.LOGIN} or
* {@link Views.REGISTER}).
*
* {@link onWillStartClient} and {@link onClientStarted} will already have been called (but not necessarily
* completed).
*
* This method either calls {@link onLiggedIn} directly, or switches to {@link Views.E2E_SETUP} or
* {@link Views.COMPLETE_SECURITY}, which will later call {@link onCompleteSecurityE2eSetupFinished}.
*/
private async postLoginSetup(): Promise<void> {
const cli = MatrixClientPeg.safeGet();
const cryptoEnabled = Boolean(cli.getCrypto());
if (!cryptoEnabled) {
this.onLoggedIn();
this.onShowPostLoginScreen();
}

const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
Expand Down Expand Up @@ -423,7 +450,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {

const cryptoExtension = ModuleRunner.instance.extensions.cryptoSetup;
if (cryptoExtension.SHOW_ENCRYPTION_SETUP_UI == false) {
this.onLoggedIn();
this.onShowPostLoginScreen();
} else {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
Expand All @@ -435,7 +462,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
);
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
this.onShowPostLoginScreen();
}
this.setState({ pendingInitialSync: false });
}
Expand Down Expand Up @@ -836,18 +863,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
break;
case Action.OnLoggedIn:
this.stores.client = MatrixClientPeg.safeGet();
if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
!Lifecycle.isSoftLogout() &&
this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER &&
this.state.view !== Views.COMPLETE_SECURITY &&
this.state.view !== Views.E2E_SETUP
) {
this.onLoggedIn();
}
this.onLoggedIn();
break;
case Action.ClientNotViable:
this.onSoftLogout();
Expand Down Expand Up @@ -983,8 +999,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
newState.isMobileRegistration = isMobileRegistration;

this.setStateForNewView(newState);
ThemeController.isLogin = true;
this.themeWatcher?.recheck();
this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register");
}

Expand Down Expand Up @@ -1056,8 +1070,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
roomJustCreatedOpts: roomInfo.justCreatedOpts,
},
() => {
ThemeController.isLogin = false;
this.themeWatcher?.recheck();
this.notifyNewScreen("room/" + presentedId, replaceLast);
},
);
Expand All @@ -1081,8 +1093,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view: Views.WELCOME,
});
this.notifyNewScreen("welcome");
ThemeController.isLogin = true;
this.themeWatcher?.recheck();
}

private viewLogin(otherState?: any): void {
Expand All @@ -1091,8 +1101,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
...otherState,
});
this.notifyNewScreen("login");
ThemeController.isLogin = true;
this.themeWatcher?.recheck();
}

private viewHome(justRegistered = false): void {
Expand All @@ -1104,8 +1112,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
this.setPage(PageType.HomePage);
this.notifyNewScreen("home");
ThemeController.isLogin = false;
this.themeWatcher?.recheck();
}

private viewUser(userId: string, subAction: string): void {
Expand Down Expand Up @@ -1369,16 +1375,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}

/**
* Called when a new logged in session has started
* Show the first screen after the application is successfully loaded in a logged-in state.
*
* Called:
*
* - on {@link Action.OnLoggedIn}, but only when we don't expect a separate call to {@link postLoginSetup}.
* - from {@link postLoginSetup}, when we don't have crypto setup tasks to perform after the login.
* - by {@link onCompleteSecurityE2eSetupFinished}
*
* In other words, whenever we think we have completed the login and E2E setup tasks.
*/
private async onLoggedIn(): Promise<void> {
ThemeController.isLogin = false;
this.themeWatcher?.recheck();
StorageManager.tryPersistStorage();

await this.onShowPostLoginScreen();
}

private async onShowPostLoginScreen(): Promise<void> {
this.setStateForNewView({ view: Views.LOGGED_IN });
// If a specific screen is set to be shown after login, show that above
Expand Down Expand Up @@ -1488,6 +1494,28 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}

/**
* Handle an {@link Action.OnLoggedIn} action (i.e, we now have a client with working credentials).
*/
private onLoggedIn(): void {
this.stores.client = MatrixClientPeg.safeGet();
StorageManager.tryPersistStorage();

// If we're in the middle of a login/registration, we wait for it to complete before transitioning to the logged
// in view the login flow will call `postLoginSetup` when it's done, which will arrange for `onShowPostLoginScreen`
// to be called.
if (
!this.tokenLogin &&
!Lifecycle.isSoftLogout() &&
this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER &&
this.state.view !== Views.COMPLETE_SECURITY &&
this.state.view !== Views.E2E_SETUP
) {
this.onShowPostLoginScreen();
}
}

/**
* Called when the session is logged out
*/
Expand Down Expand Up @@ -2043,7 +2071,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
};

// complete security / e2e setup has finished
/** Called when {@link Views.E2E_SETUP} or {@link Views.COMPLETE_SECURITY} have completed. */
private onCompleteSecurityE2eSetupFinished = async (): Promise<void> => {
const forceVerify = await this.shouldForceVerification();
if (forceVerify) {
Expand Down
4 changes: 0 additions & 4 deletions src/settings/controllers/ThemeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { DEFAULT_THEME, enumerateThemes } from "../../theme";
import { type SettingLevel } from "../SettingLevel";

export default class ThemeController extends SettingController {
public static isLogin = false;

public getValueOverride(
level: SettingLevel,
roomId: string,
Expand All @@ -22,8 +20,6 @@ export default class ThemeController extends SettingController {
): any {
if (!calculatedValue) return null; // Don't override null themes

if (ThemeController.isLogin) return "light";

const themes = enumerateThemes();
// Override in case some no longer supported theme is stored here
if (!themes[calculatedValue]) {
Expand Down
Loading
Loading