You've already forked matrix-react-sdk
							
							
				mirror of
				https://github.com/matrix-org/matrix-react-sdk.git
				synced 2025-11-04 11:51:45 +03:00 
			
		
		
		
	* Enable `@typescript-eslint/explicit-member-accessibility` on /src * Prettier * Enable `@typescript-eslint/explicit-function-return-type` in /src * Fix types * tsc strict fixes * Delint * Fix test * Fix bad merge
		
			
				
	
	
		
			2147 lines
		
	
	
		
			90 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			2147 lines
		
	
	
		
			90 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
/*
 | 
						||
Copyright 2015-2022 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, { ComponentType, createRef } from "react";
 | 
						||
import {
 | 
						||
    ClientEvent,
 | 
						||
    createClient,
 | 
						||
    EventType,
 | 
						||
    HttpApiEvent,
 | 
						||
    MatrixClient,
 | 
						||
    MatrixEventEvent,
 | 
						||
} from "matrix-js-sdk/src/matrix";
 | 
						||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
 | 
						||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
 | 
						||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 | 
						||
import { defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils";
 | 
						||
import { logger } from "matrix-js-sdk/src/logger";
 | 
						||
import { throttle } from "lodash";
 | 
						||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
 | 
						||
import { RoomType } from "matrix-js-sdk/src/@types/event";
 | 
						||
import { DecryptionError } from "matrix-js-sdk/src/crypto/algorithms";
 | 
						||
 | 
						||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by various components
 | 
						||
import "focus-visible";
 | 
						||
// what-input helps improve keyboard accessibility
 | 
						||
import "what-input";
 | 
						||
 | 
						||
import PosthogTrackers from "../../PosthogTrackers";
 | 
						||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
 | 
						||
import { IMatrixClientCreds, MatrixClientPeg } from "../../MatrixClientPeg";
 | 
						||
import PlatformPeg from "../../PlatformPeg";
 | 
						||
import SdkConfig from "../../SdkConfig";
 | 
						||
import dis from "../../dispatcher/dispatcher";
 | 
						||
import Notifier from "../../Notifier";
 | 
						||
import Modal from "../../Modal";
 | 
						||
import { showRoomInviteDialog, showStartChatInviteDialog } from "../../RoomInvite";
 | 
						||
import * as Rooms from "../../Rooms";
 | 
						||
import * as Lifecycle from "../../Lifecycle";
 | 
						||
// LifecycleStore is not used but does listen to and dispatch actions
 | 
						||
import "../../stores/LifecycleStore";
 | 
						||
import "../../stores/AutoRageshakeStore";
 | 
						||
import PageType from "../../PageTypes";
 | 
						||
import createRoom, { IOpts } from "../../createRoom";
 | 
						||
import { _t, _td, getCurrentLanguage } from "../../languageHandler";
 | 
						||
import SettingsStore from "../../settings/SettingsStore";
 | 
						||
import ThemeController from "../../settings/controllers/ThemeController";
 | 
						||
import { startAnyRegistrationFlow } from "../../Registration";
 | 
						||
import { messageForSyncError } from "../../utils/ErrorUtils";
 | 
						||
import ResizeNotifier from "../../utils/ResizeNotifier";
 | 
						||
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils";
 | 
						||
import DMRoomMap from "../../utils/DMRoomMap";
 | 
						||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
 | 
						||
import { FontWatcher } from "../../settings/watchers/FontWatcher";
 | 
						||
import { storeRoomAliasInCache } from "../../RoomAliasCache";
 | 
						||
import ToastStore from "../../stores/ToastStore";
 | 
						||
import * as StorageManager from "../../utils/StorageManager";
 | 
						||
import { UseCase } from "../../settings/enums/UseCase";
 | 
						||
import type LoggedInViewType from "./LoggedInView";
 | 
						||
import LoggedInView from "./LoggedInView";
 | 
						||
import { Action } from "../../dispatcher/actions";
 | 
						||
import { hideToast as hideAnalyticsToast, showToast as showAnalyticsToast } from "../../toasts/AnalyticsToast";
 | 
						||
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
 | 
						||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
 | 
						||
import ErrorDialog from "../views/dialogs/ErrorDialog";
 | 
						||
import {
 | 
						||
    RoomNotificationStateStore,
 | 
						||
    UPDATE_STATUS_INDICATOR,
 | 
						||
} from "../../stores/notifications/RoomNotificationStateStore";
 | 
						||
import { SettingLevel } from "../../settings/SettingLevel";
 | 
						||
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
 | 
						||
import { UIFeature } from "../../settings/UIFeature";
 | 
						||
import DialPadModal from "../views/voip/DialPadModal";
 | 
						||
import { showToast as showMobileGuideToast } from "../../toasts/MobileGuideToast";
 | 
						||
import { shouldUseLoginForWelcome } from "../../utils/pages";
 | 
						||
import RoomListStore from "../../stores/room-list/RoomListStore";
 | 
						||
import { RoomUpdateCause } from "../../stores/room-list/models";
 | 
						||
import SecurityCustomisations from "../../customisations/Security";
 | 
						||
import Spinner from "../views/elements/Spinner";
 | 
						||
import QuestionDialog from "../views/dialogs/QuestionDialog";
 | 
						||
import UserSettingsDialog from "../views/dialogs/UserSettingsDialog";
 | 
						||
import CreateRoomDialog from "../views/dialogs/CreateRoomDialog";
 | 
						||
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
 | 
						||
import IncomingSasDialog from "../views/dialogs/IncomingSasDialog";
 | 
						||
import CompleteSecurity from "./auth/CompleteSecurity";
 | 
						||
import Welcome from "../views/auth/Welcome";
 | 
						||
import ForgotPassword from "./auth/ForgotPassword";
 | 
						||
import E2eSetup from "./auth/E2eSetup";
 | 
						||
import Registration from "./auth/Registration";
 | 
						||
import Login from "./auth/Login";
 | 
						||
import ErrorBoundary from "../views/elements/ErrorBoundary";
 | 
						||
import VerificationRequestToast from "../views/toasts/VerificationRequestToast";
 | 
						||
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
 | 
						||
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
 | 
						||
import SoftLogout from "./auth/SoftLogout";
 | 
						||
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
 | 
						||
import { copyPlaintext } from "../../utils/strings";
 | 
						||
import { PosthogAnalytics } from "../../PosthogAnalytics";
 | 
						||
import { initSentry } from "../../sentry";
 | 
						||
import LegacyCallHandler from "../../LegacyCallHandler";
 | 
						||
import { showSpaceInvite } from "../../utils/space";
 | 
						||
import AccessibleButton from "../views/elements/AccessibleButton";
 | 
						||
import { ActionPayload } from "../../dispatcher/payloads";
 | 
						||
import { SummarizedNotificationState } from "../../stores/notifications/SummarizedNotificationState";
 | 
						||
import Views from "../../Views";
 | 
						||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
 | 
						||
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
 | 
						||
import { AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload";
 | 
						||
import { DoAfterSyncPreparedPayload } from "../../dispatcher/payloads/DoAfterSyncPreparedPayload";
 | 
						||
import { ViewStartChatOrReusePayload } from "../../dispatcher/payloads/ViewStartChatOrReusePayload";
 | 
						||
import { IConfigOptions } from "../../IConfigOptions";
 | 
						||
import { SnakedObject } from "../../utils/SnakedObject";
 | 
						||
import { leaveRoomBehaviour } from "../../utils/leave-behaviour";
 | 
						||
import { CallStore } from "../../stores/CallStore";
 | 
						||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
 | 
						||
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
 | 
						||
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
 | 
						||
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
 | 
						||
import { TimelineRenderingType } from "../../contexts/RoomContext";
 | 
						||
import { UseCaseSelection } from "../views/elements/UseCaseSelection";
 | 
						||
import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
 | 
						||
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
 | 
						||
import { SdkContextClass, SDKContext } from "../../contexts/SDKContext";
 | 
						||
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
 | 
						||
import { VoiceBroadcastResumer } from "../../voice-broadcast";
 | 
						||
import GenericToast from "../views/toasts/GenericToast";
 | 
						||
import { Linkify } from "../views/elements/Linkify";
 | 
						||
import RovingSpotlightDialog, { Filter } from "../views/dialogs/spotlight/SpotlightDialog";
 | 
						||
 | 
						||
// legacy export
 | 
						||
export { default as Views } from "../../Views";
 | 
						||
 | 
						||
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"];
 | 
						||
 | 
						||
// Actions that are redirected through the onboarding process prior to being
 | 
						||
// re-dispatched. NOTE: some actions are non-trivial and would require
 | 
						||
// re-factoring to be included in this list in future.
 | 
						||
const ONBOARDING_FLOW_STARTERS = [Action.ViewUserSettings, "view_create_chat", "view_create_room"];
 | 
						||
 | 
						||
interface IScreen {
 | 
						||
    screen: string;
 | 
						||
    params?: QueryDict;
 | 
						||
}
 | 
						||
 | 
						||
interface IProps {
 | 
						||
    config: IConfigOptions;
 | 
						||
    serverConfig?: ValidatedServerConfig;
 | 
						||
    onNewScreen: (screen: string, replaceLast: boolean) => void;
 | 
						||
    enableGuest?: boolean;
 | 
						||
    // the queryParams extracted from the [real] query-string of the URI
 | 
						||
    realQueryParams?: QueryDict;
 | 
						||
    // the initial queryParams extracted from the hash-fragment of the URI
 | 
						||
    startingFragmentQueryParams?: QueryDict;
 | 
						||
    // called when we have completed a token login
 | 
						||
    onTokenLoginCompleted?: () => void;
 | 
						||
    // Represents the screen to display as a result of parsing the initial window.location
 | 
						||
    initialScreenAfterLogin?: IScreen;
 | 
						||
    // displayname, if any, to set on the device when logging in/registering.
 | 
						||
    defaultDeviceDisplayName?: string;
 | 
						||
    // A function that makes a registration URL
 | 
						||
    makeRegistrationUrl: (params: QueryDict) => string;
 | 
						||
}
 | 
						||
 | 
						||
interface IState {
 | 
						||
    // the master view we are showing.
 | 
						||
    view: Views;
 | 
						||
    // What the LoggedInView would be showing if visible
 | 
						||
    // eslint-disable-next-line camelcase
 | 
						||
    page_type?: PageType;
 | 
						||
    // The ID of the room we're viewing. This is either populated directly
 | 
						||
    // in the case where we view a room by ID or by RoomView when it resolves
 | 
						||
    // what ID an alias points at.
 | 
						||
    currentRoomId?: string;
 | 
						||
    // If we're trying to just view a user ID (i.e. /user URL), this is it
 | 
						||
    currentUserId?: string;
 | 
						||
    // this is persisted as mx_lhs_size, loaded in LoggedInView
 | 
						||
    collapseLhs: boolean;
 | 
						||
    // Parameters used in the registration dance with the IS
 | 
						||
    // eslint-disable-next-line camelcase
 | 
						||
    register_client_secret?: string;
 | 
						||
    // eslint-disable-next-line camelcase
 | 
						||
    register_session_id?: string;
 | 
						||
    // eslint-disable-next-line camelcase
 | 
						||
    register_id_sid?: string;
 | 
						||
    // When showing Modal dialogs we need to set aria-hidden on the root app element
 | 
						||
    // and disable it when there are no dialogs
 | 
						||
    hideToSRUsers: boolean;
 | 
						||
    syncError?: Error;
 | 
						||
    resizeNotifier: ResizeNotifier;
 | 
						||
    serverConfig?: ValidatedServerConfig;
 | 
						||
    ready: boolean;
 | 
						||
    threepidInvite?: IThreepidInvite;
 | 
						||
    roomOobData?: object;
 | 
						||
    pendingInitialSync?: boolean;
 | 
						||
    justRegistered?: boolean;
 | 
						||
    roomJustCreatedOpts?: IOpts;
 | 
						||
    forceTimeline?: boolean; // see props
 | 
						||
}
 | 
						||
 | 
						||
export default class MatrixChat extends React.PureComponent<IProps, IState> {
 | 
						||
    public static displayName = "MatrixChat";
 | 
						||
 | 
						||
    public static defaultProps = {
 | 
						||
        realQueryParams: {},
 | 
						||
        startingFragmentQueryParams: {},
 | 
						||
        config: {},
 | 
						||
        onTokenLoginCompleted: (): void => {},
 | 
						||
    };
 | 
						||
 | 
						||
    private firstSyncComplete = false;
 | 
						||
    private firstSyncPromise: IDeferred<void>;
 | 
						||
 | 
						||
    private screenAfterLogin?: IScreen;
 | 
						||
    private tokenLogin?: boolean;
 | 
						||
    private accountPassword?: string;
 | 
						||
    private accountPasswordTimer?: number;
 | 
						||
    private focusComposer: boolean;
 | 
						||
    private subTitleStatus: string;
 | 
						||
    private prevWindowWidth: number;
 | 
						||
    private voiceBroadcastResumer: VoiceBroadcastResumer;
 | 
						||
 | 
						||
    private readonly loggedInView: React.RefObject<LoggedInViewType>;
 | 
						||
    private readonly dispatcherRef: string;
 | 
						||
    private readonly themeWatcher: ThemeWatcher;
 | 
						||
    private readonly fontWatcher: FontWatcher;
 | 
						||
    private readonly stores: SdkContextClass;
 | 
						||
 | 
						||
    public constructor(props: IProps) {
 | 
						||
        super(props);
 | 
						||
        this.stores = SdkContextClass.instance;
 | 
						||
        this.stores.constructEagerStores();
 | 
						||
 | 
						||
        this.state = {
 | 
						||
            view: Views.LOADING,
 | 
						||
            collapseLhs: false,
 | 
						||
 | 
						||
            hideToSRUsers: false,
 | 
						||
 | 
						||
            syncError: null, // If the current syncing status is ERROR, the error object, otherwise null.
 | 
						||
            resizeNotifier: new ResizeNotifier(),
 | 
						||
            ready: false,
 | 
						||
        };
 | 
						||
 | 
						||
        this.loggedInView = createRef();
 | 
						||
 | 
						||
        SdkConfig.put(this.props.config);
 | 
						||
 | 
						||
        // Used by _viewRoom before getting state from sync
 | 
						||
        this.firstSyncComplete = false;
 | 
						||
        this.firstSyncPromise = defer();
 | 
						||
 | 
						||
        if (this.props.config.sync_timeline_limit) {
 | 
						||
            MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit;
 | 
						||
        }
 | 
						||
 | 
						||
        // a thing to call showScreen with once login completes.  this is kept
 | 
						||
        // outside this.state because updating it should never trigger a
 | 
						||
        // rerender.
 | 
						||
        this.screenAfterLogin = this.props.initialScreenAfterLogin;
 | 
						||
        if (this.screenAfterLogin) {
 | 
						||
            const params = this.screenAfterLogin.params || {};
 | 
						||
            if (this.screenAfterLogin.screen.startsWith("room/") && params["signurl"] && params["email"]) {
 | 
						||
                // probably a threepid invite - try to store it
 | 
						||
                const roomId = this.screenAfterLogin.screen.substring("room/".length);
 | 
						||
                ThreepidInviteStore.instance.storeInvite(roomId, params as unknown as IThreepidInviteWireFormat);
 | 
						||
            }
 | 
						||
        }
 | 
						||
 | 
						||
        this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
 | 
						||
        UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
 | 
						||
 | 
						||
        // For PersistentElement
 | 
						||
        this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
 | 
						||
 | 
						||
        RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator);
 | 
						||
 | 
						||
        // Force users to go through the soft logout page if they're soft logged out
 | 
						||
        if (Lifecycle.isSoftLogout()) {
 | 
						||
            // When the session loads it'll be detected as soft logged out and a dispatch
 | 
						||
            // will be sent out to say that, triggering this MatrixChat to show the soft
 | 
						||
            // logout page.
 | 
						||
            Lifecycle.loadSession();
 | 
						||
        }
 | 
						||
 | 
						||
        this.accountPassword = null;
 | 
						||
        this.accountPasswordTimer = null;
 | 
						||
 | 
						||
        this.dispatcherRef = dis.register(this.onAction);
 | 
						||
 | 
						||
        this.themeWatcher = new ThemeWatcher();
 | 
						||
        this.fontWatcher = new FontWatcher();
 | 
						||
        this.themeWatcher.start();
 | 
						||
        this.fontWatcher.start();
 | 
						||
 | 
						||
        this.focusComposer = false;
 | 
						||
 | 
						||
        // object field used for tracking the status info appended to the title tag.
 | 
						||
        // we don't do it as react state as i'm scared about triggering needless react refreshes.
 | 
						||
        this.subTitleStatus = "";
 | 
						||
 | 
						||
        // the first thing to do is to try the token params in the query-string
 | 
						||
        // if the session isn't soft logged out (ie: is a clean session being logged in)
 | 
						||
        if (!Lifecycle.isSoftLogout()) {
 | 
						||
            Lifecycle.attemptTokenLogin(
 | 
						||
                this.props.realQueryParams,
 | 
						||
                this.props.defaultDeviceDisplayName,
 | 
						||
                this.getFragmentAfterLogin(),
 | 
						||
            ).then(async (loggedIn): Promise<boolean | void> => {
 | 
						||
                if (this.props.realQueryParams?.loginToken) {
 | 
						||
                    // remove the loginToken from the URL regardless
 | 
						||
                    this.props.onTokenLoginCompleted();
 | 
						||
                }
 | 
						||
 | 
						||
                if (loggedIn) {
 | 
						||
                    this.tokenLogin = true;
 | 
						||
 | 
						||
                    // Create and start the client
 | 
						||
                    await Lifecycle.restoreFromLocalStorage({
 | 
						||
                        ignoreGuest: true,
 | 
						||
                    });
 | 
						||
                    return this.postLoginSetup();
 | 
						||
                }
 | 
						||
 | 
						||
                // if the user has followed a login or register link, don't reanimate
 | 
						||
                // the old creds, but rather go straight to the relevant page
 | 
						||
                const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null;
 | 
						||
 | 
						||
                const restoreSuccess = await this.loadSession();
 | 
						||
                if (restoreSuccess) {
 | 
						||
                    return true;
 | 
						||
                }
 | 
						||
 | 
						||
                if (firstScreen === "login" || firstScreen === "register" || firstScreen === "forgot_password") {
 | 
						||
                    this.showScreenAfterLogin();
 | 
						||
                }
 | 
						||
 | 
						||
                return false;
 | 
						||
            });
 | 
						||
        }
 | 
						||
 | 
						||
        initSentry(SdkConfig.get("sentry"));
 | 
						||
    }
 | 
						||
 | 
						||
    private async postLoginSetup(): Promise<void> {
 | 
						||
        const cli = MatrixClientPeg.get();
 | 
						||
        const cryptoEnabled = cli.isCryptoEnabled();
 | 
						||
        if (!cryptoEnabled) {
 | 
						||
            this.onLoggedIn();
 | 
						||
        }
 | 
						||
 | 
						||
        const promisesList: Promise<any>[] = [this.firstSyncPromise.promise];
 | 
						||
        let crossSigningIsSetUp = false;
 | 
						||
        if (cryptoEnabled) {
 | 
						||
            // check if the user has previously published public cross-signing keys,
 | 
						||
            // as a proxy to figure out if it's worth prompting the user to verify
 | 
						||
            // from another device.
 | 
						||
            promisesList.push(
 | 
						||
                (async (): Promise<void> => {
 | 
						||
                    crossSigningIsSetUp = await cli.userHasCrossSigningKeys();
 | 
						||
                })(),
 | 
						||
            );
 | 
						||
        }
 | 
						||
 | 
						||
        // Now update the state to say we're waiting for the first sync to complete rather
 | 
						||
        // than for the login to finish.
 | 
						||
        this.setState({ pendingInitialSync: true });
 | 
						||
 | 
						||
        await Promise.all(promisesList);
 | 
						||
 | 
						||
        if (!cryptoEnabled) {
 | 
						||
            this.setState({ pendingInitialSync: false });
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        if (crossSigningIsSetUp) {
 | 
						||
            // if the user has previously set up cross-signing, verify this device so we can fetch the
 | 
						||
            // private keys.
 | 
						||
            if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {
 | 
						||
                this.onLoggedIn();
 | 
						||
            } else {
 | 
						||
                this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
 | 
						||
            }
 | 
						||
        } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
 | 
						||
            // if cross-signing is not yet set up, do so now if possible.
 | 
						||
            this.setStateForNewView({ view: Views.E2E_SETUP });
 | 
						||
        } else {
 | 
						||
            this.onLoggedIn();
 | 
						||
        }
 | 
						||
        this.setState({ pendingInitialSync: false });
 | 
						||
    }
 | 
						||
 | 
						||
    public setState<K extends keyof IState>(
 | 
						||
        state:
 | 
						||
            | ((prevState: Readonly<IState>, props: Readonly<IProps>) => Pick<IState, K> | IState | null)
 | 
						||
            | (Pick<IState, K> | IState | null),
 | 
						||
        callback?: () => void,
 | 
						||
    ): void {
 | 
						||
        if (this.shouldTrackPageChange(this.state, { ...this.state, ...state })) {
 | 
						||
            this.startPageChangeTimer();
 | 
						||
        }
 | 
						||
        super.setState<K>(state, callback);
 | 
						||
    }
 | 
						||
 | 
						||
    public componentDidMount(): void {
 | 
						||
        window.addEventListener("resize", this.onWindowResized);
 | 
						||
    }
 | 
						||
 | 
						||
    public componentDidUpdate(prevProps, prevState): void {
 | 
						||
        if (this.shouldTrackPageChange(prevState, this.state)) {
 | 
						||
            const durationMs = this.stopPageChangeTimer();
 | 
						||
            PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs);
 | 
						||
        }
 | 
						||
        if (this.focusComposer) {
 | 
						||
            dis.fire(Action.FocusSendMessageComposer);
 | 
						||
            this.focusComposer = false;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    public componentWillUnmount(): void {
 | 
						||
        Lifecycle.stopMatrixClient();
 | 
						||
        dis.unregister(this.dispatcherRef);
 | 
						||
        this.themeWatcher.stop();
 | 
						||
        this.fontWatcher.stop();
 | 
						||
        UIStore.destroy();
 | 
						||
        this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
 | 
						||
        window.removeEventListener("resize", this.onWindowResized);
 | 
						||
 | 
						||
        if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
 | 
						||
        if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
 | 
						||
    }
 | 
						||
 | 
						||
    private onWindowResized = (): void => {
 | 
						||
        // XXX: This is a very unreliable way to detect whether or not the the devtools are open
 | 
						||
        this.warnInConsole();
 | 
						||
    };
 | 
						||
 | 
						||
    private warnInConsole = throttle((): void => {
 | 
						||
        const largeFontSize = "50px";
 | 
						||
        const normalFontSize = "15px";
 | 
						||
 | 
						||
        const waitText = _t("Wait!");
 | 
						||
        const scamText = _t(
 | 
						||
            "If someone told you to copy/paste something here, " + "there is a high likelihood you're being scammed!",
 | 
						||
        );
 | 
						||
        const devText = _t(
 | 
						||
            "If you know what you're doing, Element is open-source, " +
 | 
						||
                "be sure to check out our GitHub (https://github.com/vector-im/element-web/) " +
 | 
						||
                "and contribute!",
 | 
						||
        );
 | 
						||
 | 
						||
        global.mx_rage_logger.bypassRageshake(
 | 
						||
            "log",
 | 
						||
            `%c${waitText}\n%c${scamText}\n%c${devText}`,
 | 
						||
            `font-size:${largeFontSize}; color:blue;`,
 | 
						||
            `font-size:${normalFontSize}; color:red;`,
 | 
						||
            `font-size:${normalFontSize};`,
 | 
						||
        );
 | 
						||
    }, 1000);
 | 
						||
 | 
						||
    private getFallbackHsUrl(): string {
 | 
						||
        if (this.props.serverConfig?.isDefault) {
 | 
						||
            return this.props.config.fallback_hs_url;
 | 
						||
        } else {
 | 
						||
            return null;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private getServerProperties(): { serverConfig: ValidatedServerConfig } {
 | 
						||
        let props = this.state.serverConfig;
 | 
						||
        if (!props) props = this.props.serverConfig; // for unit tests
 | 
						||
        if (!props) props = SdkConfig.get("validated_server_config");
 | 
						||
        return { serverConfig: props };
 | 
						||
    }
 | 
						||
 | 
						||
    private loadSession(): Promise<boolean> {
 | 
						||
        // the extra Promise.resolve() ensures that synchronous exceptions hit the same codepath as
 | 
						||
        // asynchronous ones.
 | 
						||
        return Promise.resolve()
 | 
						||
            .then(() => {
 | 
						||
                return Lifecycle.loadSession({
 | 
						||
                    fragmentQueryParams: this.props.startingFragmentQueryParams,
 | 
						||
                    enableGuest: this.props.enableGuest,
 | 
						||
                    guestHsUrl: this.getServerProperties().serverConfig.hsUrl,
 | 
						||
                    guestIsUrl: this.getServerProperties().serverConfig.isUrl,
 | 
						||
                    defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
 | 
						||
                });
 | 
						||
            })
 | 
						||
            .then((loadedSession) => {
 | 
						||
                if (!loadedSession) {
 | 
						||
                    // fall back to showing the welcome screen... unless we have a 3pid invite pending
 | 
						||
                    if (ThreepidInviteStore.instance.pickBestInvite()) {
 | 
						||
                        dis.dispatch({ action: "start_registration" });
 | 
						||
                    } else {
 | 
						||
                        dis.dispatch({ action: "view_welcome_page" });
 | 
						||
                    }
 | 
						||
                }
 | 
						||
                return loadedSession;
 | 
						||
            });
 | 
						||
        // Note we don't catch errors from this: we catch everything within
 | 
						||
        // loadSession as there's logic there to ask the user if they want
 | 
						||
        // to try logging out.
 | 
						||
    }
 | 
						||
 | 
						||
    private startPageChangeTimer(): void {
 | 
						||
        PerformanceMonitor.instance.start(PerformanceEntryNames.PAGE_CHANGE);
 | 
						||
    }
 | 
						||
 | 
						||
    private stopPageChangeTimer(): number | null {
 | 
						||
        const perfMonitor = PerformanceMonitor.instance;
 | 
						||
 | 
						||
        perfMonitor.stop(PerformanceEntryNames.PAGE_CHANGE);
 | 
						||
 | 
						||
        const entries = perfMonitor.getEntries({
 | 
						||
            name: PerformanceEntryNames.PAGE_CHANGE,
 | 
						||
        });
 | 
						||
        const measurement = entries.pop();
 | 
						||
 | 
						||
        return measurement ? measurement.duration : null;
 | 
						||
    }
 | 
						||
 | 
						||
    private shouldTrackPageChange(prevState: IState, state: IState): boolean {
 | 
						||
        return (
 | 
						||
            prevState.currentRoomId !== state.currentRoomId ||
 | 
						||
            prevState.view !== state.view ||
 | 
						||
            prevState.page_type !== state.page_type
 | 
						||
        );
 | 
						||
    }
 | 
						||
 | 
						||
    private setStateForNewView(state: Partial<IState>): void {
 | 
						||
        if (state.view === undefined) {
 | 
						||
            throw new Error("setStateForNewView with no view!");
 | 
						||
        }
 | 
						||
        const newState = {
 | 
						||
            currentUserId: null,
 | 
						||
            justRegistered: false,
 | 
						||
        };
 | 
						||
        Object.assign(newState, state);
 | 
						||
        this.setState(newState);
 | 
						||
    }
 | 
						||
 | 
						||
    private onAction = (payload: ActionPayload): void => {
 | 
						||
        // Start the onboarding process for certain actions
 | 
						||
        if (MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) {
 | 
						||
            // This will cause `payload` to be dispatched later, once a
 | 
						||
            // sync has reached the "prepared" state. Setting a matrix ID
 | 
						||
            // will cause a full login and sync and finally the deferred
 | 
						||
            // action will be dispatched.
 | 
						||
            dis.dispatch({
 | 
						||
                action: Action.DoAfterSyncPrepared,
 | 
						||
                deferred_action: payload,
 | 
						||
            });
 | 
						||
            dis.dispatch({ action: "require_registration" });
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        switch (payload.action) {
 | 
						||
            case "MatrixActions.accountData":
 | 
						||
                // XXX: This is a collection of several hacks to solve a minor problem. We want to
 | 
						||
                // update our local state when the identity server changes, but don't want to put that in
 | 
						||
                // the js-sdk as we'd be then dictating how all consumers need to behave. However,
 | 
						||
                // this component is already bloated and we probably don't want this tiny logic in
 | 
						||
                // here, but there's no better place in the react-sdk for it. Additionally, we're
 | 
						||
                // abusing the MatrixActionCreator stuff to avoid errors on dispatches.
 | 
						||
                if (payload.event_type === "m.identity_server") {
 | 
						||
                    const fullUrl = payload.event_content ? payload.event_content["base_url"] : null;
 | 
						||
                    if (!fullUrl) {
 | 
						||
                        MatrixClientPeg.get().setIdentityServerUrl(null);
 | 
						||
                        localStorage.removeItem("mx_is_access_token");
 | 
						||
                        localStorage.removeItem("mx_is_url");
 | 
						||
                    } else {
 | 
						||
                        MatrixClientPeg.get().setIdentityServerUrl(fullUrl);
 | 
						||
                        localStorage.removeItem("mx_is_access_token"); // clear token
 | 
						||
                        localStorage.setItem("mx_is_url", fullUrl); // XXX: Do we still need this?
 | 
						||
                    }
 | 
						||
 | 
						||
                    // redispatch the change with a more specific action
 | 
						||
                    dis.dispatch({ action: "id_server_changed" });
 | 
						||
                }
 | 
						||
                break;
 | 
						||
            case "logout":
 | 
						||
                LegacyCallHandler.instance.hangupAllCalls();
 | 
						||
                Promise.all([...CallStore.instance.activeCalls].map((call) => call.disconnect())).finally(() =>
 | 
						||
                    Lifecycle.logout(),
 | 
						||
                );
 | 
						||
                break;
 | 
						||
            case "require_registration":
 | 
						||
                startAnyRegistrationFlow(payload as any);
 | 
						||
                break;
 | 
						||
            case "start_registration":
 | 
						||
                if (Lifecycle.isSoftLogout()) {
 | 
						||
                    this.onSoftLogout();
 | 
						||
                    break;
 | 
						||
                }
 | 
						||
                // This starts the full registration flow
 | 
						||
                if (payload.screenAfterLogin) {
 | 
						||
                    this.screenAfterLogin = payload.screenAfterLogin;
 | 
						||
                }
 | 
						||
                this.startRegistration(payload.params || {});
 | 
						||
                break;
 | 
						||
            case "start_login":
 | 
						||
                if (Lifecycle.isSoftLogout()) {
 | 
						||
                    this.onSoftLogout();
 | 
						||
                    break;
 | 
						||
                }
 | 
						||
                if (payload.screenAfterLogin) {
 | 
						||
                    this.screenAfterLogin = payload.screenAfterLogin;
 | 
						||
                }
 | 
						||
                this.viewLogin();
 | 
						||
                break;
 | 
						||
            case "start_password_recovery":
 | 
						||
                this.setStateForNewView({
 | 
						||
                    view: Views.FORGOT_PASSWORD,
 | 
						||
                });
 | 
						||
                this.notifyNewScreen("forgot_password");
 | 
						||
                break;
 | 
						||
            case "start_chat":
 | 
						||
                createRoom({
 | 
						||
                    dmUserId: payload.user_id,
 | 
						||
                });
 | 
						||
                break;
 | 
						||
            case "leave_room":
 | 
						||
                this.leaveRoom(payload.room_id);
 | 
						||
                break;
 | 
						||
            case "forget_room":
 | 
						||
                this.forgetRoom(payload.room_id);
 | 
						||
                break;
 | 
						||
            case "copy_room":
 | 
						||
                this.copyRoom(payload.room_id);
 | 
						||
                break;
 | 
						||
            case "reject_invite":
 | 
						||
                Modal.createDialog(QuestionDialog, {
 | 
						||
                    title: _t("Reject invitation"),
 | 
						||
                    description: _t("Are you sure you want to reject the invitation?"),
 | 
						||
                    onFinished: (confirm) => {
 | 
						||
                        if (confirm) {
 | 
						||
                            // FIXME: controller shouldn't be loading a view :(
 | 
						||
                            const modal = Modal.createDialog(Spinner, null, "mx_Dialog_spinner");
 | 
						||
 | 
						||
                            MatrixClientPeg.get()
 | 
						||
                                .leave(payload.room_id)
 | 
						||
                                .then(
 | 
						||
                                    () => {
 | 
						||
                                        modal.close();
 | 
						||
                                        if (this.state.currentRoomId === payload.room_id) {
 | 
						||
                                            dis.dispatch({ action: Action.ViewHomePage });
 | 
						||
                                        }
 | 
						||
                                    },
 | 
						||
                                    (err) => {
 | 
						||
                                        modal.close();
 | 
						||
                                        Modal.createDialog(ErrorDialog, {
 | 
						||
                                            title: _t("Failed to reject invitation"),
 | 
						||
                                            description: err.toString(),
 | 
						||
                                        });
 | 
						||
                                    },
 | 
						||
                                );
 | 
						||
                        }
 | 
						||
                    },
 | 
						||
                });
 | 
						||
                break;
 | 
						||
            case "view_user_info":
 | 
						||
                this.viewUser(payload.userId, payload.subAction);
 | 
						||
                break;
 | 
						||
            case "MatrixActions.RoomState.events": {
 | 
						||
                const event = (payload as IRoomStateEventsActionPayload).event;
 | 
						||
                if (
 | 
						||
                    event.getType() === EventType.RoomCanonicalAlias &&
 | 
						||
                    event.getRoomId() === this.state.currentRoomId
 | 
						||
                ) {
 | 
						||
                    // re-view the current room so we can update alias/id in the URL properly
 | 
						||
                    this.viewRoom({
 | 
						||
                        action: Action.ViewRoom,
 | 
						||
                        room_id: this.state.currentRoomId,
 | 
						||
                        metricsTrigger: undefined, // room doesn't change
 | 
						||
                    });
 | 
						||
                }
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case Action.ViewRoom: {
 | 
						||
                // Takes either a room ID or room alias: if switching to a room the client is already
 | 
						||
                // known to be in (eg. user clicks on a room in the recents panel), supply the ID
 | 
						||
                // If the user is clicking on a room in the context of the alias being presented
 | 
						||
                // to them, supply the room alias. If both are supplied, the room ID will be ignored.
 | 
						||
                const promise = this.viewRoom(payload as ViewRoomPayload);
 | 
						||
                if (payload.deferred_action) {
 | 
						||
                    promise.then(() => {
 | 
						||
                        dis.dispatch(payload.deferred_action);
 | 
						||
                    });
 | 
						||
                }
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case Action.ViewUserDeviceSettings: {
 | 
						||
                viewUserDeviceSettings(SettingsStore.getValue("feature_new_device_manager"));
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case Action.ViewUserSettings: {
 | 
						||
                const tabPayload = payload as OpenToTabPayload;
 | 
						||
                Modal.createDialog(
 | 
						||
                    UserSettingsDialog,
 | 
						||
                    { initialTabId: tabPayload.initialTabId },
 | 
						||
                    /*className=*/ null,
 | 
						||
                    /*isPriority=*/ false,
 | 
						||
                    /*isStatic=*/ true,
 | 
						||
                );
 | 
						||
 | 
						||
                // View the welcome or home page if we need something to look at
 | 
						||
                this.viewSomethingBehindModal();
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case "view_create_room":
 | 
						||
                this.createRoom(payload.public, payload.defaultName, payload.type);
 | 
						||
 | 
						||
                // View the welcome or home page if we need something to look at
 | 
						||
                this.viewSomethingBehindModal();
 | 
						||
                break;
 | 
						||
            case Action.ViewRoomDirectory: {
 | 
						||
                Modal.createDialog(
 | 
						||
                    RovingSpotlightDialog,
 | 
						||
                    {
 | 
						||
                        initialText: payload.initialText,
 | 
						||
                        initialFilter: Filter.PublicRooms,
 | 
						||
                    },
 | 
						||
                    "mx_SpotlightDialog_wrapper",
 | 
						||
                    false,
 | 
						||
                    true,
 | 
						||
                );
 | 
						||
 | 
						||
                // View the welcome or home page if we need something to look at
 | 
						||
                this.viewSomethingBehindModal();
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case "view_welcome_page":
 | 
						||
                this.viewWelcome();
 | 
						||
                break;
 | 
						||
            case Action.ViewHomePage:
 | 
						||
                this.viewHome(payload.justRegistered);
 | 
						||
                break;
 | 
						||
            case Action.ViewStartChatOrReuse:
 | 
						||
                this.chatCreateOrReuse(payload.user_id);
 | 
						||
                break;
 | 
						||
            case "view_create_chat":
 | 
						||
                showStartChatInviteDialog(payload.initialText || "");
 | 
						||
 | 
						||
                // View the welcome or home page if we need something to look at
 | 
						||
                this.viewSomethingBehindModal();
 | 
						||
                break;
 | 
						||
            case "view_invite": {
 | 
						||
                const room = MatrixClientPeg.get().getRoom(payload.roomId);
 | 
						||
                if (room?.isSpaceRoom()) {
 | 
						||
                    showSpaceInvite(room);
 | 
						||
                } else {
 | 
						||
                    showRoomInviteDialog(payload.roomId);
 | 
						||
                }
 | 
						||
                break;
 | 
						||
            }
 | 
						||
            case "view_last_screen":
 | 
						||
                // This function does what we want, despite the name. The idea is that it shows
 | 
						||
                // the last room we were looking at or some reasonable default/guess. We don't
 | 
						||
                // have to worry about email invites or similar being re-triggered because the
 | 
						||
                // function will have cleared that state and not execute that path.
 | 
						||
                this.showScreenAfterLogin();
 | 
						||
                break;
 | 
						||
            case "hide_left_panel":
 | 
						||
                this.setState(
 | 
						||
                    {
 | 
						||
                        collapseLhs: true,
 | 
						||
                    },
 | 
						||
                    () => {
 | 
						||
                        this.state.resizeNotifier.notifyLeftHandleResized();
 | 
						||
                    },
 | 
						||
                );
 | 
						||
                break;
 | 
						||
            case "show_left_panel":
 | 
						||
                this.setState(
 | 
						||
                    {
 | 
						||
                        collapseLhs: false,
 | 
						||
                    },
 | 
						||
                    () => {
 | 
						||
                        this.state.resizeNotifier.notifyLeftHandleResized();
 | 
						||
                    },
 | 
						||
                );
 | 
						||
                break;
 | 
						||
            case Action.OpenDialPad:
 | 
						||
                Modal.createDialog(DialPadModal, {}, "mx_Dialog_dialPadWrapper");
 | 
						||
                break;
 | 
						||
            case Action.OnLoggedIn:
 | 
						||
                this.stores.client = MatrixClientPeg.get();
 | 
						||
                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.state.view !== Views.USE_CASE_SELECTION
 | 
						||
                ) {
 | 
						||
                    this.onLoggedIn();
 | 
						||
                }
 | 
						||
                break;
 | 
						||
            case "on_client_not_viable":
 | 
						||
                this.onSoftLogout();
 | 
						||
                break;
 | 
						||
            case Action.OnLoggedOut:
 | 
						||
                this.onLoggedOut();
 | 
						||
                break;
 | 
						||
            case "will_start_client":
 | 
						||
                this.setState({ ready: false }, () => {
 | 
						||
                    // if the client is about to start, we are, by definition, not ready.
 | 
						||
                    // Set ready to false now, then it'll be set to true when the sync
 | 
						||
                    // listener we set below fires.
 | 
						||
                    this.onWillStartClient();
 | 
						||
                });
 | 
						||
                break;
 | 
						||
            case "client_started":
 | 
						||
                this.onClientStarted();
 | 
						||
                break;
 | 
						||
            case "send_event":
 | 
						||
                this.onSendEvent(payload.room_id, payload.event);
 | 
						||
                break;
 | 
						||
            case "aria_hide_main_app":
 | 
						||
                this.setState({
 | 
						||
                    hideToSRUsers: true,
 | 
						||
                });
 | 
						||
                break;
 | 
						||
            case "aria_unhide_main_app":
 | 
						||
                this.setState({
 | 
						||
                    hideToSRUsers: false,
 | 
						||
                });
 | 
						||
                break;
 | 
						||
            case Action.PseudonymousAnalyticsAccept:
 | 
						||
                hideAnalyticsToast();
 | 
						||
                SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, true);
 | 
						||
                break;
 | 
						||
            case Action.PseudonymousAnalyticsReject:
 | 
						||
                hideAnalyticsToast();
 | 
						||
                SettingsStore.setValue("pseudonymousAnalyticsOptIn", null, SettingLevel.ACCOUNT, false);
 | 
						||
                break;
 | 
						||
            case Action.ShowThread: {
 | 
						||
                const { rootEvent, initialEvent, highlighted, scrollIntoView, push } = payload as ShowThreadPayload;
 | 
						||
 | 
						||
                const threadViewCard = {
 | 
						||
                    phase: RightPanelPhases.ThreadView,
 | 
						||
                    state: {
 | 
						||
                        threadHeadEvent: rootEvent,
 | 
						||
                        initialEvent: initialEvent,
 | 
						||
                        isInitialEventHighlighted: highlighted,
 | 
						||
                        initialEventScrollIntoView: scrollIntoView,
 | 
						||
                    },
 | 
						||
                };
 | 
						||
                if (push ?? false) {
 | 
						||
                    RightPanelStore.instance.pushCard(threadViewCard);
 | 
						||
                } else {
 | 
						||
                    RightPanelStore.instance.setCards([{ phase: RightPanelPhases.ThreadPanel }, threadViewCard]);
 | 
						||
                }
 | 
						||
 | 
						||
                // Focus the composer
 | 
						||
                dis.dispatch({
 | 
						||
                    action: Action.FocusSendMessageComposer,
 | 
						||
                    context: TimelineRenderingType.Thread,
 | 
						||
                });
 | 
						||
 | 
						||
                break;
 | 
						||
            }
 | 
						||
        }
 | 
						||
    };
 | 
						||
 | 
						||
    private setPage(pageType: PageType): void {
 | 
						||
        this.setState({
 | 
						||
            page_type: pageType,
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    private async startRegistration(params: { [key: string]: string }): Promise<void> {
 | 
						||
        const newState: Partial<IState> = {
 | 
						||
            view: Views.REGISTER,
 | 
						||
        };
 | 
						||
 | 
						||
        // Only honour params if they are all present, otherwise we reset
 | 
						||
        // HS and IS URLs when switching to registration.
 | 
						||
        if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) {
 | 
						||
            newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(
 | 
						||
                params.hs_url,
 | 
						||
                params.is_url,
 | 
						||
            );
 | 
						||
 | 
						||
            // If the hs url matches then take the hs name we know locally as it is likely prettier
 | 
						||
            const defaultConfig = SdkConfig.get("validated_server_config");
 | 
						||
            if (defaultConfig && defaultConfig.hsUrl === newState.serverConfig.hsUrl) {
 | 
						||
                newState.serverConfig.hsName = defaultConfig.hsName;
 | 
						||
                newState.serverConfig.hsNameIsDifferent = defaultConfig.hsNameIsDifferent;
 | 
						||
                newState.serverConfig.isDefault = defaultConfig.isDefault;
 | 
						||
                newState.serverConfig.isNameResolvable = defaultConfig.isNameResolvable;
 | 
						||
            }
 | 
						||
 | 
						||
            newState.register_client_secret = params.client_secret;
 | 
						||
            newState.register_session_id = params.session_id;
 | 
						||
            newState.register_id_sid = params.sid;
 | 
						||
        }
 | 
						||
 | 
						||
        this.setStateForNewView(newState);
 | 
						||
        ThemeController.isLogin = true;
 | 
						||
        this.themeWatcher.recheck();
 | 
						||
        this.notifyNewScreen("register");
 | 
						||
    }
 | 
						||
 | 
						||
    // switch view to the given room
 | 
						||
    private async viewRoom(roomInfo: ViewRoomPayload): Promise<void> {
 | 
						||
        this.focusComposer = true;
 | 
						||
 | 
						||
        if (roomInfo.room_alias) {
 | 
						||
            logger.log(`Switching to room alias ${roomInfo.room_alias} at event ${roomInfo.event_id}`);
 | 
						||
        } else {
 | 
						||
            logger.log(`Switching to room id ${roomInfo.room_id} at event ${roomInfo.event_id}`);
 | 
						||
        }
 | 
						||
 | 
						||
        // Wait for the first sync to complete so that if a room does have an alias,
 | 
						||
        // it would have been retrieved.
 | 
						||
        if (!this.firstSyncComplete) {
 | 
						||
            if (!this.firstSyncPromise) {
 | 
						||
                logger.warn("Cannot view a room before first sync. room_id:", roomInfo.room_id);
 | 
						||
                return;
 | 
						||
            }
 | 
						||
            await this.firstSyncPromise.promise;
 | 
						||
        }
 | 
						||
 | 
						||
        let presentedId = roomInfo.room_alias || roomInfo.room_id;
 | 
						||
        const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
 | 
						||
        if (room) {
 | 
						||
            // Not all timeline events are decrypted ahead of time anymore
 | 
						||
            // Only the critical ones for a typical UI are
 | 
						||
            // This will start the decryption process for all events when a
 | 
						||
            // user views a room
 | 
						||
            room.decryptAllEvents();
 | 
						||
            const theAlias = Rooms.getDisplayAliasForRoom(room);
 | 
						||
            if (theAlias) {
 | 
						||
                presentedId = theAlias;
 | 
						||
                // Store display alias of the presented room in cache to speed future
 | 
						||
                // navigation.
 | 
						||
                storeRoomAliasInCache(theAlias, room.roomId);
 | 
						||
            }
 | 
						||
 | 
						||
            // Store this as the ID of the last room accessed. This is so that we can
 | 
						||
            // persist which room is being stored across refreshes and browser quits.
 | 
						||
            localStorage?.setItem("mx_last_room_id", room.roomId);
 | 
						||
        }
 | 
						||
 | 
						||
        // If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
 | 
						||
        let replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
 | 
						||
 | 
						||
        if (isLocalRoom(this.state.currentRoomId)) {
 | 
						||
            // Replace local room history items
 | 
						||
            replaceLast = true;
 | 
						||
        }
 | 
						||
 | 
						||
        if (roomInfo.room_id === this.state.currentRoomId) {
 | 
						||
            // if we are re-viewing the same room then copy any state we already know
 | 
						||
            roomInfo.threepid_invite = roomInfo.threepid_invite ?? this.state.threepidInvite;
 | 
						||
            roomInfo.oob_data = roomInfo.oob_data ?? this.state.roomOobData;
 | 
						||
            roomInfo.forceTimeline = roomInfo.forceTimeline ?? this.state.forceTimeline;
 | 
						||
            roomInfo.justCreatedOpts = roomInfo.justCreatedOpts ?? this.state.roomJustCreatedOpts;
 | 
						||
        }
 | 
						||
 | 
						||
        if (roomInfo.event_id && roomInfo.highlighted) {
 | 
						||
            presentedId += "/" + roomInfo.event_id;
 | 
						||
        }
 | 
						||
        this.setState(
 | 
						||
            {
 | 
						||
                view: Views.LOGGED_IN,
 | 
						||
                currentRoomId: roomInfo.room_id || null,
 | 
						||
                page_type: PageType.RoomView,
 | 
						||
                threepidInvite: roomInfo.threepid_invite,
 | 
						||
                roomOobData: roomInfo.oob_data,
 | 
						||
                forceTimeline: roomInfo.forceTimeline,
 | 
						||
                ready: true,
 | 
						||
                roomJustCreatedOpts: roomInfo.justCreatedOpts,
 | 
						||
            },
 | 
						||
            () => {
 | 
						||
                this.notifyNewScreen("room/" + presentedId, replaceLast);
 | 
						||
            },
 | 
						||
        );
 | 
						||
    }
 | 
						||
 | 
						||
    private viewSomethingBehindModal(): void {
 | 
						||
        if (this.state.view !== Views.LOGGED_IN) {
 | 
						||
            this.viewWelcome();
 | 
						||
            return;
 | 
						||
        }
 | 
						||
        if (!this.state.currentRoomId && !this.state.currentUserId) {
 | 
						||
            this.viewHome();
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private viewWelcome(): void {
 | 
						||
        if (shouldUseLoginForWelcome(SdkConfig.get())) {
 | 
						||
            return this.viewLogin();
 | 
						||
        }
 | 
						||
        this.setStateForNewView({
 | 
						||
            view: Views.WELCOME,
 | 
						||
        });
 | 
						||
        this.notifyNewScreen("welcome");
 | 
						||
        ThemeController.isLogin = true;
 | 
						||
        this.themeWatcher.recheck();
 | 
						||
    }
 | 
						||
 | 
						||
    private viewLogin(otherState?: any): void {
 | 
						||
        this.setStateForNewView({
 | 
						||
            view: Views.LOGIN,
 | 
						||
            ...otherState,
 | 
						||
        });
 | 
						||
        this.notifyNewScreen("login");
 | 
						||
        ThemeController.isLogin = true;
 | 
						||
        this.themeWatcher.recheck();
 | 
						||
    }
 | 
						||
 | 
						||
    private viewHome(justRegistered = false): void {
 | 
						||
        // The home page requires the "logged in" view, so we'll set that.
 | 
						||
        this.setStateForNewView({
 | 
						||
            view: Views.LOGGED_IN,
 | 
						||
            justRegistered,
 | 
						||
            currentRoomId: null,
 | 
						||
        });
 | 
						||
        this.setPage(PageType.HomePage);
 | 
						||
        this.notifyNewScreen("home");
 | 
						||
        ThemeController.isLogin = false;
 | 
						||
        this.themeWatcher.recheck();
 | 
						||
    }
 | 
						||
 | 
						||
    private viewUser(userId: string, subAction: string): void {
 | 
						||
        // Wait for the first sync so that `getRoom` gives us a room object if it's
 | 
						||
        // in the sync response
 | 
						||
        const waitForSync = this.firstSyncPromise ? this.firstSyncPromise.promise : Promise.resolve();
 | 
						||
        waitForSync.then(() => {
 | 
						||
            if (subAction === "chat") {
 | 
						||
                this.chatCreateOrReuse(userId);
 | 
						||
                return;
 | 
						||
            }
 | 
						||
            this.notifyNewScreen("user/" + userId);
 | 
						||
            this.setState({ currentUserId: userId });
 | 
						||
            this.setPage(PageType.UserView);
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType): Promise<void> {
 | 
						||
        const modal = Modal.createDialog(CreateRoomDialog, {
 | 
						||
            type,
 | 
						||
            defaultPublic,
 | 
						||
            defaultName,
 | 
						||
        });
 | 
						||
 | 
						||
        const [shouldCreate, opts] = await modal.finished;
 | 
						||
        if (shouldCreate) {
 | 
						||
            createRoom(opts);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private chatCreateOrReuse(userId: string): void {
 | 
						||
        const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
 | 
						||
        // Use a deferred action to reshow the dialog once the user has registered
 | 
						||
        if (MatrixClientPeg.get().isGuest()) {
 | 
						||
            // No point in making 2 DMs with welcome bot. This assumes view_set_mxid will
 | 
						||
            // result in a new DM with the welcome user.
 | 
						||
            if (userId !== snakedConfig.get("welcome_user_id")) {
 | 
						||
                dis.dispatch<DoAfterSyncPreparedPayload<ViewStartChatOrReusePayload>>({
 | 
						||
                    action: Action.DoAfterSyncPrepared,
 | 
						||
                    deferred_action: {
 | 
						||
                        action: Action.ViewStartChatOrReuse,
 | 
						||
                        user_id: userId,
 | 
						||
                    },
 | 
						||
                });
 | 
						||
            }
 | 
						||
            dis.dispatch({
 | 
						||
                action: "require_registration",
 | 
						||
                // If the set_mxid dialog is cancelled, view /welcome because if the
 | 
						||
                // browser was pointing at /user/@someone:domain?action=chat, the URL
 | 
						||
                // needs to be reset so that they can revisit /user/.. // (and trigger
 | 
						||
                // `_chatCreateOrReuse` again)
 | 
						||
                go_welcome_on_cancel: true,
 | 
						||
                screen_after: {
 | 
						||
                    screen: `user/${snakedConfig.get("welcome_user_id")}`,
 | 
						||
                    params: { action: "chat" },
 | 
						||
                },
 | 
						||
            });
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        // TODO: Immutable DMs replaces this
 | 
						||
 | 
						||
        const client = MatrixClientPeg.get();
 | 
						||
        const dmRoomMap = new DMRoomMap(client);
 | 
						||
        const dmRooms = dmRoomMap.getDMRoomsForUserId(userId);
 | 
						||
 | 
						||
        if (dmRooms.length > 0) {
 | 
						||
            dis.dispatch<ViewRoomPayload>({
 | 
						||
                action: Action.ViewRoom,
 | 
						||
                room_id: dmRooms[0],
 | 
						||
                metricsTrigger: "MessageUser",
 | 
						||
            });
 | 
						||
        } else {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "start_chat",
 | 
						||
                user_id: userId,
 | 
						||
            });
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private leaveRoomWarnings(roomId: string): JSX.Element[] {
 | 
						||
        const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
 | 
						||
        const isSpace = roomToLeave?.isSpaceRoom();
 | 
						||
        // Show a warning if there are additional complications.
 | 
						||
        const warnings: JSX.Element[] = [];
 | 
						||
 | 
						||
        const memberCount = roomToLeave.currentState.getJoinedMemberCount();
 | 
						||
        if (memberCount === 1) {
 | 
						||
            warnings.push(
 | 
						||
                <span className="warning" key="only_member_warning">
 | 
						||
                    {" " /* Whitespace, otherwise the sentences get smashed together */}
 | 
						||
                    {_t(
 | 
						||
                        "You are the only person here. " +
 | 
						||
                            "If you leave, no one will be able to join in the future, including you.",
 | 
						||
                    )}
 | 
						||
                </span>,
 | 
						||
            );
 | 
						||
 | 
						||
            return warnings;
 | 
						||
        }
 | 
						||
 | 
						||
        const joinRules = roomToLeave.currentState.getStateEvents("m.room.join_rules", "");
 | 
						||
        if (joinRules) {
 | 
						||
            const rule = joinRules.getContent().join_rule;
 | 
						||
            if (rule !== "public") {
 | 
						||
                warnings.push(
 | 
						||
                    <span className="warning" key="non_public_warning">
 | 
						||
                        {" " /* Whitespace, otherwise the sentences get smashed together */}
 | 
						||
                        {isSpace
 | 
						||
                            ? _t("This space is not public. You will not be able to rejoin without an invite.")
 | 
						||
                            : _t("This room is not public. You will not be able to rejoin without an invite.")}
 | 
						||
                    </span>,
 | 
						||
                );
 | 
						||
            }
 | 
						||
        }
 | 
						||
        return warnings;
 | 
						||
    }
 | 
						||
 | 
						||
    private leaveRoom(roomId: string): void {
 | 
						||
        const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
 | 
						||
        const warnings = this.leaveRoomWarnings(roomId);
 | 
						||
 | 
						||
        const isSpace = roomToLeave?.isSpaceRoom();
 | 
						||
        Modal.createDialog(QuestionDialog, {
 | 
						||
            title: isSpace ? _t("Leave space") : _t("Leave room"),
 | 
						||
            description: (
 | 
						||
                <span>
 | 
						||
                    {isSpace
 | 
						||
                        ? _t("Are you sure you want to leave the space '%(spaceName)s'?", {
 | 
						||
                              spaceName: roomToLeave.name,
 | 
						||
                          })
 | 
						||
                        : _t("Are you sure you want to leave the room '%(roomName)s'?", { roomName: roomToLeave.name })}
 | 
						||
                    {warnings}
 | 
						||
                </span>
 | 
						||
            ),
 | 
						||
            button: _t("Leave"),
 | 
						||
            onFinished: (shouldLeave) => {
 | 
						||
                if (shouldLeave) {
 | 
						||
                    leaveRoomBehaviour(roomId);
 | 
						||
 | 
						||
                    dis.dispatch<AfterLeaveRoomPayload>({
 | 
						||
                        action: Action.AfterLeaveRoom,
 | 
						||
                        room_id: roomId,
 | 
						||
                    });
 | 
						||
                }
 | 
						||
            },
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    private forgetRoom(roomId: string): void {
 | 
						||
        const room = MatrixClientPeg.get().getRoom(roomId);
 | 
						||
        MatrixClientPeg.get()
 | 
						||
            .forget(roomId)
 | 
						||
            .then(() => {
 | 
						||
                // Switch to home page if we're currently viewing the forgotten room
 | 
						||
                if (this.state.currentRoomId === roomId) {
 | 
						||
                    dis.dispatch({ action: Action.ViewHomePage });
 | 
						||
                }
 | 
						||
 | 
						||
                // We have to manually update the room list because the forgotten room will not
 | 
						||
                // be notified to us, therefore the room list will have no other way of knowing
 | 
						||
                // the room is forgotten.
 | 
						||
                RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved);
 | 
						||
            })
 | 
						||
            .catch((err) => {
 | 
						||
                const errCode = err.errcode || _td("unknown error code");
 | 
						||
                Modal.createDialog(ErrorDialog, {
 | 
						||
                    title: _t("Failed to forget room %(errCode)s", { errCode }),
 | 
						||
                    description: err && err.message ? err.message : _t("Operation failed"),
 | 
						||
                });
 | 
						||
            });
 | 
						||
    }
 | 
						||
 | 
						||
    private async copyRoom(roomId: string): Promise<void> {
 | 
						||
        const roomLink = makeRoomPermalink(roomId);
 | 
						||
        const success = await copyPlaintext(roomLink);
 | 
						||
        if (!success) {
 | 
						||
            Modal.createDialog(ErrorDialog, {
 | 
						||
                title: _t("Unable to copy room link"),
 | 
						||
                description: _t("Unable to copy a link to the room to the clipboard."),
 | 
						||
            });
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Starts a chat with the welcome user, if the user doesn't already have one
 | 
						||
     * @returns {string} The room ID of the new room, or null if no room was created
 | 
						||
     */
 | 
						||
    private async startWelcomeUserChat(): Promise<string | null> {
 | 
						||
        // We can end up with multiple tabs post-registration where the user
 | 
						||
        // might then end up with a session and we don't want them all making
 | 
						||
        // a chat with the welcome user: try to de-dupe.
 | 
						||
        // We need to wait for the first sync to complete for this to
 | 
						||
        // work though.
 | 
						||
        let waitFor: Promise<void>;
 | 
						||
        if (!this.firstSyncComplete) {
 | 
						||
            waitFor = this.firstSyncPromise.promise;
 | 
						||
        } else {
 | 
						||
            waitFor = Promise.resolve();
 | 
						||
        }
 | 
						||
        await waitFor;
 | 
						||
 | 
						||
        const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
 | 
						||
        const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(snakedConfig.get("welcome_user_id"));
 | 
						||
        if (welcomeUserRooms.length === 0) {
 | 
						||
            const roomId = await createRoom({
 | 
						||
                dmUserId: snakedConfig.get("welcome_user_id"),
 | 
						||
                // Only view the welcome user if we're NOT looking at a room
 | 
						||
                andView: !this.state.currentRoomId,
 | 
						||
                spinner: false, // we're already showing one: we don't need another one
 | 
						||
            });
 | 
						||
            // This is a bit of a hack, but since the deduplication relies
 | 
						||
            // on m.direct being up to date, we need to force a sync
 | 
						||
            // of the database, otherwise if the user goes to the other
 | 
						||
            // tab before the next save happens (a few minutes), the
 | 
						||
            // saved sync will be restored from the db and this code will
 | 
						||
            // run without the update to m.direct, making another welcome
 | 
						||
            // user room (it doesn't wait for new data from the server, just
 | 
						||
            // the saved sync to be loaded).
 | 
						||
            const saveWelcomeUser = (ev: MatrixEvent): void => {
 | 
						||
                if (ev.getType() === EventType.Direct && ev.getContent()[snakedConfig.get("welcome_user_id")]) {
 | 
						||
                    MatrixClientPeg.get().store.save(true);
 | 
						||
                    MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser);
 | 
						||
                }
 | 
						||
            };
 | 
						||
            MatrixClientPeg.get().on(ClientEvent.AccountData, saveWelcomeUser);
 | 
						||
 | 
						||
            return roomId;
 | 
						||
        }
 | 
						||
        return null;
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Called when a new logged in session has started
 | 
						||
     */
 | 
						||
    private async onLoggedIn(): Promise<void> {
 | 
						||
        ThemeController.isLogin = false;
 | 
						||
        this.themeWatcher.recheck();
 | 
						||
        StorageManager.tryPersistStorage();
 | 
						||
 | 
						||
        if (MatrixClientPeg.currentUserIsJustRegistered() && SettingsStore.getValue("FTUE.useCaseSelection") === null) {
 | 
						||
            this.setStateForNewView({ view: Views.USE_CASE_SELECTION });
 | 
						||
 | 
						||
            // Listen to changes in settings and hide the use case screen if appropriate - this is necessary because
 | 
						||
            // account settings can still be changing at this point in app init (due to the initial sync being cached,
 | 
						||
            // then subsequent syncs being received from the server)
 | 
						||
            //
 | 
						||
            // This seems unlikely for something that should happen directly after registration, but if a user does
 | 
						||
            // their initial login on another device/browser than they registered on, we want to avoid asking this
 | 
						||
            // question twice
 | 
						||
            //
 | 
						||
            // initPosthogAnalyticsToast pioneered this technique, we’re just reusing it here.
 | 
						||
            SettingsStore.watchSetting(
 | 
						||
                "FTUE.useCaseSelection",
 | 
						||
                null,
 | 
						||
                (originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
 | 
						||
                    if (newValue !== null && this.state.view === Views.USE_CASE_SELECTION) {
 | 
						||
                        this.onShowPostLoginScreen();
 | 
						||
                    }
 | 
						||
                },
 | 
						||
            );
 | 
						||
        } else {
 | 
						||
            return this.onShowPostLoginScreen();
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private async onShowPostLoginScreen(useCase?: UseCase): Promise<void> {
 | 
						||
        if (useCase) {
 | 
						||
            PosthogAnalytics.instance.setProperty("ftueUseCaseSelection", useCase);
 | 
						||
            SettingsStore.setValue("FTUE.useCaseSelection", null, SettingLevel.ACCOUNT, useCase);
 | 
						||
        }
 | 
						||
 | 
						||
        this.setStateForNewView({ view: Views.LOGGED_IN });
 | 
						||
        // If a specific screen is set to be shown after login, show that above
 | 
						||
        // all else, as it probably means the user clicked on something already.
 | 
						||
        if (this.screenAfterLogin && this.screenAfterLogin.screen) {
 | 
						||
            this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params);
 | 
						||
            this.screenAfterLogin = null;
 | 
						||
        } else if (MatrixClientPeg.currentUserIsJustRegistered()) {
 | 
						||
            MatrixClientPeg.setJustRegisteredUserId(null);
 | 
						||
 | 
						||
            const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
 | 
						||
            if (snakedConfig.get("welcome_user_id") && getCurrentLanguage().startsWith("en")) {
 | 
						||
                const welcomeUserRoom = await this.startWelcomeUserChat();
 | 
						||
                if (welcomeUserRoom === null) {
 | 
						||
                    // We didn't redirect to the welcome user room, so show
 | 
						||
                    // the homepage.
 | 
						||
                    dis.dispatch<ViewHomePagePayload>({ action: Action.ViewHomePage, justRegistered: true });
 | 
						||
                }
 | 
						||
            } else if (ThreepidInviteStore.instance.pickBestInvite()) {
 | 
						||
                // The user has a 3pid invite pending - show them that
 | 
						||
                const threepidInvite = ThreepidInviteStore.instance.pickBestInvite();
 | 
						||
 | 
						||
                // HACK: This is a pretty brutal way of threading the invite back through
 | 
						||
                // our systems, but it's the safest we have for now.
 | 
						||
                const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
 | 
						||
                this.showScreen(`room/${threepidInvite.roomId}`, params);
 | 
						||
            } else {
 | 
						||
                // The user has just logged in after registering,
 | 
						||
                // so show the homepage.
 | 
						||
                dis.dispatch<ViewHomePagePayload>({ action: Action.ViewHomePage, justRegistered: true });
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            this.showScreenAfterLogin();
 | 
						||
        }
 | 
						||
 | 
						||
        if (SdkConfig.get("mobile_guide_toast")) {
 | 
						||
            // The toast contains further logic to detect mobile platforms,
 | 
						||
            // check if it has been dismissed before, etc.
 | 
						||
            showMobileGuideToast();
 | 
						||
        }
 | 
						||
 | 
						||
        const userNotice = SdkConfig.get("user_notice");
 | 
						||
        if (userNotice) {
 | 
						||
            const key = "user_notice_" + userNotice.title;
 | 
						||
            if (!userNotice.show_once || !localStorage.getItem(key)) {
 | 
						||
                ToastStore.sharedInstance().addOrReplaceToast({
 | 
						||
                    key,
 | 
						||
                    title: userNotice.title,
 | 
						||
                    props: {
 | 
						||
                        description: <Linkify>{userNotice.description}</Linkify>,
 | 
						||
                        acceptLabel: _t("OK"),
 | 
						||
                        onAccept: () => {
 | 
						||
                            ToastStore.sharedInstance().dismissToast(key);
 | 
						||
                            localStorage.setItem(key, "1");
 | 
						||
                        },
 | 
						||
                    },
 | 
						||
                    component: GenericToast,
 | 
						||
                    className: "mx_AnalyticsToast",
 | 
						||
                    priority: 100,
 | 
						||
                });
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private initPosthogAnalyticsToast(): void {
 | 
						||
        // Show the analytics toast if necessary
 | 
						||
        if (SettingsStore.getValue("pseudonymousAnalyticsOptIn") === null) {
 | 
						||
            showAnalyticsToast();
 | 
						||
        }
 | 
						||
 | 
						||
        // Listen to changes in settings and show the toast if appropriate - this is necessary because account
 | 
						||
        // settings can still be changing at this point in app init (due to the initial sync being cached, then
 | 
						||
        // subsequent syncs being received from the server)
 | 
						||
        SettingsStore.watchSetting(
 | 
						||
            "pseudonymousAnalyticsOptIn",
 | 
						||
            null,
 | 
						||
            (originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => {
 | 
						||
                if (newValue === null) {
 | 
						||
                    showAnalyticsToast();
 | 
						||
                } else {
 | 
						||
                    // It's possible for the value to change if a cached sync loads at page load, but then network
 | 
						||
                    // sync contains a new value of the flag with it set to false (e.g. another device set it since last
 | 
						||
                    // loading the page); so hide the toast.
 | 
						||
                    // (this flipping usually happens before first render so the user won't notice it; anyway flicker
 | 
						||
                    // on/off is probably better than showing the toast again when the user already dismissed it)
 | 
						||
                    hideAnalyticsToast();
 | 
						||
                }
 | 
						||
            },
 | 
						||
        );
 | 
						||
    }
 | 
						||
 | 
						||
    private showScreenAfterLogin(): void {
 | 
						||
        // If screenAfterLogin is set, use that, then null it so that a second login will
 | 
						||
        // result in view_home_page, _user_settings or _room_directory
 | 
						||
        if (this.screenAfterLogin && this.screenAfterLogin.screen) {
 | 
						||
            this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params);
 | 
						||
            this.screenAfterLogin = null;
 | 
						||
        } else if (localStorage && localStorage.getItem("mx_last_room_id")) {
 | 
						||
            // Before defaulting to directory, show the last viewed room
 | 
						||
            this.viewLastRoom();
 | 
						||
        } else {
 | 
						||
            if (MatrixClientPeg.get().isGuest()) {
 | 
						||
                dis.dispatch({ action: "view_welcome_page" });
 | 
						||
            } else {
 | 
						||
                dis.dispatch({ action: Action.ViewHomePage });
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private viewLastRoom(): void {
 | 
						||
        dis.dispatch<ViewRoomPayload>({
 | 
						||
            action: Action.ViewRoom,
 | 
						||
            room_id: localStorage.getItem("mx_last_room_id"),
 | 
						||
            metricsTrigger: undefined, // other
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Called when the session is logged out
 | 
						||
     */
 | 
						||
    private onLoggedOut(): void {
 | 
						||
        this.viewLogin({
 | 
						||
            ready: false,
 | 
						||
            collapseLhs: false,
 | 
						||
            currentRoomId: null,
 | 
						||
        });
 | 
						||
        this.subTitleStatus = "";
 | 
						||
        this.setPageSubtitle();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Called when the session is softly logged out
 | 
						||
     */
 | 
						||
    private onSoftLogout(): void {
 | 
						||
        this.notifyNewScreen("soft_logout");
 | 
						||
        this.setStateForNewView({
 | 
						||
            view: Views.SOFT_LOGOUT,
 | 
						||
            ready: false,
 | 
						||
            collapseLhs: false,
 | 
						||
            currentRoomId: null,
 | 
						||
        });
 | 
						||
        this.subTitleStatus = "";
 | 
						||
        this.setPageSubtitle();
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Called just before the matrix client is started
 | 
						||
     * (useful for setting listeners)
 | 
						||
     */
 | 
						||
    private onWillStartClient(): void {
 | 
						||
        // reset the 'have completed first sync' flag,
 | 
						||
        // since we're about to start the client and therefore about
 | 
						||
        // to do the first sync
 | 
						||
        this.firstSyncComplete = false;
 | 
						||
        this.firstSyncPromise = defer();
 | 
						||
        const cli = MatrixClientPeg.get();
 | 
						||
 | 
						||
        // Allow the JS SDK to reap timeline events. This reduces the amount of
 | 
						||
        // memory consumed as the JS SDK stores multiple distinct copies of room
 | 
						||
        // state (each of which can be 10s of MBs) for each DISJOINT timeline. This is
 | 
						||
        // particularly noticeable when there are lots of 'limited' /sync responses
 | 
						||
        // such as when laptops unsleep.
 | 
						||
        // https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568
 | 
						||
        cli.setCanResetTimelineCallback((roomId) => {
 | 
						||
            logger.log("Request to reset timeline in room ", roomId, " viewing:", this.state.currentRoomId);
 | 
						||
            if (roomId !== this.state.currentRoomId) {
 | 
						||
                // It is safe to remove events from rooms we are not viewing.
 | 
						||
                return true;
 | 
						||
            }
 | 
						||
            // We are viewing the room which we want to reset. It is only safe to do
 | 
						||
            // this if we are not scrolled up in the view. To find out, delegate to
 | 
						||
            // the timeline panel. If the timeline panel doesn't exist, then we assume
 | 
						||
            // it is safe to reset the timeline.
 | 
						||
            if (!this.loggedInView.current) {
 | 
						||
                return true;
 | 
						||
            }
 | 
						||
            return this.loggedInView.current.canResetTimelineInRoom(roomId);
 | 
						||
        });
 | 
						||
 | 
						||
        cli.on(ClientEvent.Sync, (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => {
 | 
						||
            if (state === SyncState.Error || state === SyncState.Reconnecting) {
 | 
						||
                if (data.error instanceof InvalidStoreError) {
 | 
						||
                    Lifecycle.handleInvalidStoreError(data.error);
 | 
						||
                }
 | 
						||
                this.setState({ syncError: data.error });
 | 
						||
            } else if (this.state.syncError) {
 | 
						||
                this.setState({ syncError: null });
 | 
						||
            }
 | 
						||
 | 
						||
            if (state === SyncState.Syncing && prevState === SyncState.Syncing) {
 | 
						||
                return;
 | 
						||
            }
 | 
						||
            logger.info("MatrixClient sync state => %s", state);
 | 
						||
            if (state !== SyncState.Prepared) {
 | 
						||
                return;
 | 
						||
            }
 | 
						||
 | 
						||
            this.firstSyncComplete = true;
 | 
						||
            this.firstSyncPromise.resolve();
 | 
						||
 | 
						||
            if (Notifier.shouldShowPrompt() && !MatrixClientPeg.userRegisteredWithinLastHours(24)) {
 | 
						||
                showNotificationsToast(false);
 | 
						||
            }
 | 
						||
 | 
						||
            dis.fire(Action.FocusSendMessageComposer);
 | 
						||
            this.setState({
 | 
						||
                ready: true,
 | 
						||
            });
 | 
						||
        });
 | 
						||
 | 
						||
        cli.on(HttpApiEvent.SessionLoggedOut, function (errObj) {
 | 
						||
            if (Lifecycle.isLoggingOut()) return;
 | 
						||
 | 
						||
            // A modal might have been open when we were logged out by the server
 | 
						||
            Modal.closeCurrentModal("Session.logged_out");
 | 
						||
 | 
						||
            if (errObj.httpStatus === 401 && errObj.data && errObj.data["soft_logout"]) {
 | 
						||
                logger.warn("Soft logout issued by server - avoiding data deletion");
 | 
						||
                Lifecycle.softLogout();
 | 
						||
                return;
 | 
						||
            }
 | 
						||
 | 
						||
            Modal.createDialog(ErrorDialog, {
 | 
						||
                title: _t("Signed Out"),
 | 
						||
                description: _t("For security, this session has been signed out. Please sign in again."),
 | 
						||
            });
 | 
						||
 | 
						||
            dis.dispatch({
 | 
						||
                action: "logout",
 | 
						||
            });
 | 
						||
        });
 | 
						||
        cli.on(HttpApiEvent.NoConsent, function (message, consentUri) {
 | 
						||
            Modal.createDialog(
 | 
						||
                QuestionDialog,
 | 
						||
                {
 | 
						||
                    title: _t("Terms and Conditions"),
 | 
						||
                    description: (
 | 
						||
                        <div>
 | 
						||
                            <p>
 | 
						||
                                {" "}
 | 
						||
                                {_t(
 | 
						||
                                    "To continue using the %(homeserverDomain)s homeserver " +
 | 
						||
                                        "you must review and agree to our terms and conditions.",
 | 
						||
                                    { homeserverDomain: cli.getDomain() },
 | 
						||
                                )}
 | 
						||
                            </p>
 | 
						||
                        </div>
 | 
						||
                    ),
 | 
						||
                    button: _t("Review terms and conditions"),
 | 
						||
                    cancelButton: _t("Dismiss"),
 | 
						||
                    onFinished: (confirmed) => {
 | 
						||
                        if (confirmed) {
 | 
						||
                            const wnd = window.open(consentUri, "_blank");
 | 
						||
                            wnd.opener = null;
 | 
						||
                        }
 | 
						||
                    },
 | 
						||
                },
 | 
						||
                null,
 | 
						||
                true,
 | 
						||
            );
 | 
						||
        });
 | 
						||
 | 
						||
        const dft = DecryptionFailureTracker.instance;
 | 
						||
 | 
						||
        // Shelved for later date when we have time to think about persisting history of
 | 
						||
        // tracked events across sessions.
 | 
						||
        // dft.loadTrackedEventHashMap();
 | 
						||
 | 
						||
        dft.start();
 | 
						||
 | 
						||
        // When logging out, stop tracking failures and destroy state
 | 
						||
        cli.on(HttpApiEvent.SessionLoggedOut, () => dft.stop());
 | 
						||
        cli.on(MatrixEventEvent.Decrypted, (e, err) => dft.eventDecrypted(e, err as DecryptionError));
 | 
						||
 | 
						||
        cli.on(ClientEvent.Room, (room) => {
 | 
						||
            if (MatrixClientPeg.get().isCryptoEnabled()) {
 | 
						||
                const blacklistEnabled = SettingsStore.getValueAt(
 | 
						||
                    SettingLevel.ROOM_DEVICE,
 | 
						||
                    "blacklistUnverifiedDevices",
 | 
						||
                    room.roomId,
 | 
						||
                    /*explicit=*/ true,
 | 
						||
                );
 | 
						||
                room.setBlacklistUnverifiedDevices(blacklistEnabled);
 | 
						||
            }
 | 
						||
        });
 | 
						||
        cli.on(CryptoEvent.Warning, (type) => {
 | 
						||
            switch (type) {
 | 
						||
                case "CRYPTO_WARNING_OLD_VERSION_DETECTED":
 | 
						||
                    Modal.createDialog(ErrorDialog, {
 | 
						||
                        title: _t("Old cryptography data detected"),
 | 
						||
                        description: _t(
 | 
						||
                            "Data from an older version of %(brand)s has been detected. " +
 | 
						||
                                "This will have caused end-to-end cryptography to malfunction " +
 | 
						||
                                "in the older version. End-to-end encrypted messages exchanged " +
 | 
						||
                                "recently whilst using the older version may not be decryptable " +
 | 
						||
                                "in this version. This may also cause messages exchanged with this " +
 | 
						||
                                "version to fail. If you experience problems, log out and back in " +
 | 
						||
                                "again. To retain message history, export and re-import your keys.",
 | 
						||
                            { brand: SdkConfig.get().brand },
 | 
						||
                        ),
 | 
						||
                    });
 | 
						||
                    break;
 | 
						||
            }
 | 
						||
        });
 | 
						||
        cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise<void> => {
 | 
						||
            let haveNewVersion;
 | 
						||
            let newVersionInfo;
 | 
						||
            // if key backup is still enabled, there must be a new backup in place
 | 
						||
            if (MatrixClientPeg.get().getKeyBackupEnabled()) {
 | 
						||
                haveNewVersion = true;
 | 
						||
            } else {
 | 
						||
                // otherwise check the server to see if there's a new one
 | 
						||
                try {
 | 
						||
                    newVersionInfo = await MatrixClientPeg.get().getKeyBackupVersion();
 | 
						||
                    if (newVersionInfo !== null) haveNewVersion = true;
 | 
						||
                } catch (e) {
 | 
						||
                    logger.error("Saw key backup error but failed to check backup version!", e);
 | 
						||
                    return;
 | 
						||
                }
 | 
						||
            }
 | 
						||
 | 
						||
            if (haveNewVersion) {
 | 
						||
                Modal.createDialogAsync(
 | 
						||
                    import(
 | 
						||
                        "../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
 | 
						||
                    ) as unknown as Promise<ComponentType<{}>>,
 | 
						||
                    { newVersionInfo },
 | 
						||
                );
 | 
						||
            } else {
 | 
						||
                Modal.createDialogAsync(
 | 
						||
                    import(
 | 
						||
                        "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog"
 | 
						||
                    ) as unknown as Promise<ComponentType<{}>>,
 | 
						||
                );
 | 
						||
            }
 | 
						||
        });
 | 
						||
 | 
						||
        cli.on(CryptoEvent.KeySignatureUploadFailure, (failures, source, continuation) => {
 | 
						||
            Modal.createDialog(KeySignatureUploadFailedDialog, { failures, source, continuation });
 | 
						||
        });
 | 
						||
 | 
						||
        cli.on(CryptoEvent.VerificationRequest, (request) => {
 | 
						||
            if (request.verifier) {
 | 
						||
                Modal.createDialog(
 | 
						||
                    IncomingSasDialog,
 | 
						||
                    {
 | 
						||
                        verifier: request.verifier,
 | 
						||
                    },
 | 
						||
                    null,
 | 
						||
                    /* priority = */ false,
 | 
						||
                    /* static = */ true,
 | 
						||
                );
 | 
						||
            } else if (request.pending) {
 | 
						||
                ToastStore.sharedInstance().addOrReplaceToast({
 | 
						||
                    key: "verifreq_" + request.channel.transactionId,
 | 
						||
                    title: _t("Verification requested"),
 | 
						||
                    icon: "verification",
 | 
						||
                    props: { request },
 | 
						||
                    component: VerificationRequestToast,
 | 
						||
                    priority: 90,
 | 
						||
                });
 | 
						||
            }
 | 
						||
        });
 | 
						||
 | 
						||
        this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
 | 
						||
    }
 | 
						||
 | 
						||
    /**
 | 
						||
     * Called shortly after the matrix client has started. Useful for
 | 
						||
     * setting up anything that requires the client to be started.
 | 
						||
     * @private
 | 
						||
     */
 | 
						||
    private onClientStarted(): void {
 | 
						||
        const cli = MatrixClientPeg.get();
 | 
						||
 | 
						||
        if (cli.isCryptoEnabled()) {
 | 
						||
            const blacklistEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "blacklistUnverifiedDevices");
 | 
						||
            cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
 | 
						||
 | 
						||
            // With cross-signing enabled, we send to unknown devices
 | 
						||
            // without prompting. Any bad-device status the user should
 | 
						||
            // be aware of will be signalled through the room shield
 | 
						||
            // changing colour. More advanced behaviour will come once
 | 
						||
            // we implement more settings.
 | 
						||
            cli.setGlobalErrorOnUnknownDevices(false);
 | 
						||
        }
 | 
						||
 | 
						||
        // Cannot be done in OnLoggedIn as at that point the AccountSettingsHandler doesn't yet have a client
 | 
						||
        // Will be moved to a pre-login flow as well
 | 
						||
        if (PosthogAnalytics.instance.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
 | 
						||
            this.initPosthogAnalyticsToast();
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    public showScreen(screen: string, params?: { [key: string]: any }): void {
 | 
						||
        const cli = MatrixClientPeg.get();
 | 
						||
        const isLoggedOutOrGuest = !cli || cli.isGuest();
 | 
						||
        if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) {
 | 
						||
            // user is logged in and landing on an auth page which will uproot their session, redirect them home instead
 | 
						||
            dis.dispatch({ action: Action.ViewHomePage });
 | 
						||
            return;
 | 
						||
        }
 | 
						||
 | 
						||
        if (screen === "register") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "start_registration",
 | 
						||
                params: params,
 | 
						||
            });
 | 
						||
            PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER);
 | 
						||
        } else if (screen === "login") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "start_login",
 | 
						||
                params: params,
 | 
						||
            });
 | 
						||
            PerformanceMonitor.instance.start(PerformanceEntryNames.LOGIN);
 | 
						||
        } else if (screen === "forgot_password") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "start_password_recovery",
 | 
						||
                params: params,
 | 
						||
            });
 | 
						||
        } else if (screen === "soft_logout") {
 | 
						||
            if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
 | 
						||
                // Logged in - visit a room
 | 
						||
                this.viewLastRoom();
 | 
						||
            } else {
 | 
						||
                // Ultimately triggers soft_logout if needed
 | 
						||
                dis.dispatch({
 | 
						||
                    action: "start_login",
 | 
						||
                    params: params,
 | 
						||
                });
 | 
						||
            }
 | 
						||
        } else if (screen === "new") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "view_create_room",
 | 
						||
            });
 | 
						||
        } else if (screen === "dm") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "view_create_chat",
 | 
						||
            });
 | 
						||
        } else if (screen === "settings") {
 | 
						||
            dis.fire(Action.ViewUserSettings);
 | 
						||
        } else if (screen === "welcome") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: "view_welcome_page",
 | 
						||
            });
 | 
						||
        } else if (screen === "home") {
 | 
						||
            dis.dispatch({
 | 
						||
                action: Action.ViewHomePage,
 | 
						||
            });
 | 
						||
        } else if (screen === "start") {
 | 
						||
            this.showScreen("home");
 | 
						||
            dis.dispatch({
 | 
						||
                action: "require_registration",
 | 
						||
            });
 | 
						||
        } else if (screen === "directory") {
 | 
						||
            dis.fire(Action.ViewRoomDirectory);
 | 
						||
        } else if (screen === "start_sso" || screen === "start_cas") {
 | 
						||
            let cli = MatrixClientPeg.get();
 | 
						||
            if (!cli) {
 | 
						||
                const { hsUrl, isUrl } = this.props.serverConfig;
 | 
						||
                cli = createClient({
 | 
						||
                    baseUrl: hsUrl,
 | 
						||
                    idBaseUrl: isUrl,
 | 
						||
                });
 | 
						||
            }
 | 
						||
 | 
						||
            const type = screen === "start_sso" ? "sso" : "cas";
 | 
						||
            PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
 | 
						||
        } else if (screen.indexOf("room/") === 0) {
 | 
						||
            // Rooms can have the following formats:
 | 
						||
            // #room_alias:domain or !opaque_id:domain
 | 
						||
            const room = screen.substring(5);
 | 
						||
            const domainOffset = room.indexOf(":") + 1; // 0 in case room does not contain a :
 | 
						||
            let eventOffset = room.length;
 | 
						||
            // room aliases can contain slashes only look for slash after domain
 | 
						||
            if (room.substring(domainOffset).indexOf("/") > -1) {
 | 
						||
                eventOffset = domainOffset + room.substring(domainOffset).indexOf("/");
 | 
						||
            }
 | 
						||
            const roomString = room.substring(0, eventOffset);
 | 
						||
            let eventId = room.substring(eventOffset + 1); // empty string if no event id given
 | 
						||
 | 
						||
            // Previously we pulled the eventID from the segments in such a way
 | 
						||
            // where if there was no eventId then we'd get undefined. However, we
 | 
						||
            // now do a splice and join to handle v3 event IDs which results in
 | 
						||
            // an empty string. To maintain our potential contract with the rest
 | 
						||
            // of the app, we coerce the eventId to be undefined where applicable.
 | 
						||
            if (!eventId) eventId = undefined;
 | 
						||
 | 
						||
            // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
 | 
						||
 | 
						||
            let threepidInvite: IThreepidInvite;
 | 
						||
            // if we landed here from a 3PID invite, persist it
 | 
						||
            if (params.signurl && params.email) {
 | 
						||
                threepidInvite = ThreepidInviteStore.instance.storeInvite(
 | 
						||
                    roomString,
 | 
						||
                    params as IThreepidInviteWireFormat,
 | 
						||
                );
 | 
						||
            }
 | 
						||
            // otherwise check that this room doesn't already have a known invite
 | 
						||
            if (!threepidInvite) {
 | 
						||
                const invites = ThreepidInviteStore.instance.getInvites();
 | 
						||
                threepidInvite = invites.find((invite) => invite.roomId === roomString);
 | 
						||
            }
 | 
						||
 | 
						||
            // on our URLs there might be a ?via=matrix.org or similar to help
 | 
						||
            // joins to the room succeed. We'll pass these through as an array
 | 
						||
            // to other levels. If there's just one ?via= then params.via is a
 | 
						||
            // single string. If someone does something like ?via=one.com&via=two.com
 | 
						||
            // then params.via is an array of strings.
 | 
						||
            let via = [];
 | 
						||
            if (params.via) {
 | 
						||
                if (typeof params.via === "string") via = [params.via];
 | 
						||
                else via = params.via;
 | 
						||
            }
 | 
						||
 | 
						||
            const payload: ViewRoomPayload = {
 | 
						||
                action: Action.ViewRoom,
 | 
						||
                event_id: eventId,
 | 
						||
                via_servers: via,
 | 
						||
                // If an event ID is given in the URL hash, notify RoomViewStore to mark
 | 
						||
                // it as highlighted, which will propagate to RoomView and highlight the
 | 
						||
                // associated EventTile.
 | 
						||
                highlighted: Boolean(eventId),
 | 
						||
                threepid_invite: threepidInvite,
 | 
						||
                // TODO: Replace oob_data with the threepidInvite (which has the same info).
 | 
						||
                // This isn't done yet because it's threaded through so many more places.
 | 
						||
                // See https://github.com/vector-im/element-web/issues/15157
 | 
						||
                oob_data: {
 | 
						||
                    name: threepidInvite?.roomName,
 | 
						||
                    avatarUrl: threepidInvite?.roomAvatarUrl,
 | 
						||
                    inviterName: threepidInvite?.inviterName,
 | 
						||
                },
 | 
						||
                room_alias: undefined,
 | 
						||
                room_id: undefined,
 | 
						||
                metricsTrigger: undefined, // unknown or external trigger
 | 
						||
            };
 | 
						||
            if (roomString[0] === "#") {
 | 
						||
                payload.room_alias = roomString;
 | 
						||
            } else {
 | 
						||
                payload.room_id = roomString;
 | 
						||
            }
 | 
						||
 | 
						||
            dis.dispatch(payload);
 | 
						||
        } else if (screen.indexOf("user/") === 0) {
 | 
						||
            const userId = screen.substring(5);
 | 
						||
            dis.dispatch({
 | 
						||
                action: "view_user_info",
 | 
						||
                userId: userId,
 | 
						||
                subAction: params.action,
 | 
						||
            });
 | 
						||
        } else {
 | 
						||
            logger.info("Ignoring showScreen for '%s'", screen);
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private notifyNewScreen(screen: string, replaceLast = false): void {
 | 
						||
        if (this.props.onNewScreen) {
 | 
						||
            this.props.onNewScreen(screen, replaceLast);
 | 
						||
        }
 | 
						||
        this.setPageSubtitle();
 | 
						||
    }
 | 
						||
 | 
						||
    private onLogoutClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>): void {
 | 
						||
        dis.dispatch({
 | 
						||
            action: "logout",
 | 
						||
        });
 | 
						||
        event.stopPropagation();
 | 
						||
        event.preventDefault();
 | 
						||
    }
 | 
						||
 | 
						||
    private handleResize = (): void => {
 | 
						||
        const LHS_THRESHOLD = 1000;
 | 
						||
        const width = UIStore.instance.windowWidth;
 | 
						||
 | 
						||
        if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
 | 
						||
            dis.dispatch({ action: "show_left_panel" });
 | 
						||
        }
 | 
						||
 | 
						||
        if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
 | 
						||
            dis.dispatch({ action: "hide_left_panel" });
 | 
						||
        }
 | 
						||
 | 
						||
        this.prevWindowWidth = width;
 | 
						||
        this.state.resizeNotifier.notifyWindowResized();
 | 
						||
    };
 | 
						||
 | 
						||
    private dispatchTimelineResize(): void {
 | 
						||
        dis.dispatch({ action: "timeline_resize" });
 | 
						||
    }
 | 
						||
 | 
						||
    private onRegisterClick = (): void => {
 | 
						||
        this.showScreen("register");
 | 
						||
    };
 | 
						||
 | 
						||
    private onLoginClick = (): void => {
 | 
						||
        this.showScreen("login");
 | 
						||
    };
 | 
						||
 | 
						||
    private onForgotPasswordClick = (): void => {
 | 
						||
        this.showScreen("forgot_password");
 | 
						||
    };
 | 
						||
 | 
						||
    private onRegisterFlowComplete = (credentials: IMatrixClientCreds, password: string): Promise<void> => {
 | 
						||
        return this.onUserCompletedLoginFlow(credentials, password);
 | 
						||
    };
 | 
						||
 | 
						||
    // returns a promise which resolves to the new MatrixClient
 | 
						||
    private onRegistered(credentials: IMatrixClientCreds): Promise<MatrixClient> {
 | 
						||
        return Lifecycle.setLoggedIn(credentials);
 | 
						||
    }
 | 
						||
 | 
						||
    private onSendEvent(roomId: string, event: MatrixEvent): void {
 | 
						||
        const cli = MatrixClientPeg.get();
 | 
						||
        if (!cli) return;
 | 
						||
 | 
						||
        cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
 | 
						||
            dis.dispatch({ action: "message_sent" });
 | 
						||
        });
 | 
						||
    }
 | 
						||
 | 
						||
    private setPageSubtitle(subtitle = ""): void {
 | 
						||
        if (this.state.currentRoomId) {
 | 
						||
            const client = MatrixClientPeg.get();
 | 
						||
            const room = client && client.getRoom(this.state.currentRoomId);
 | 
						||
            if (room) {
 | 
						||
                subtitle = `${this.subTitleStatus} | ${room.name} ${subtitle}`;
 | 
						||
            }
 | 
						||
        } else {
 | 
						||
            subtitle = `${this.subTitleStatus} ${subtitle}`;
 | 
						||
        }
 | 
						||
 | 
						||
        const title = `${SdkConfig.get().brand} ${subtitle}`;
 | 
						||
 | 
						||
        if (document.title !== title) {
 | 
						||
            document.title = title;
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private onUpdateStatusIndicator = (notificationState: SummarizedNotificationState, state: SyncState): void => {
 | 
						||
        const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
 | 
						||
 | 
						||
        if (PlatformPeg.get()) {
 | 
						||
            PlatformPeg.get().setErrorStatus(state === SyncState.Error);
 | 
						||
            PlatformPeg.get().setNotificationCount(numUnreadRooms);
 | 
						||
        }
 | 
						||
 | 
						||
        this.subTitleStatus = "";
 | 
						||
        if (state === SyncState.Error) {
 | 
						||
            this.subTitleStatus += `[${_t("Offline")}] `;
 | 
						||
        }
 | 
						||
        if (numUnreadRooms > 0) {
 | 
						||
            this.subTitleStatus += `[${numUnreadRooms}]`;
 | 
						||
        }
 | 
						||
 | 
						||
        this.setPageSubtitle();
 | 
						||
    };
 | 
						||
 | 
						||
    private onServerConfigChange = (serverConfig: ValidatedServerConfig): void => {
 | 
						||
        this.setState({ serverConfig });
 | 
						||
    };
 | 
						||
 | 
						||
    private makeRegistrationUrl = (params: QueryDict): string => {
 | 
						||
        if (this.props.startingFragmentQueryParams.referrer) {
 | 
						||
            params.referrer = this.props.startingFragmentQueryParams.referrer;
 | 
						||
        }
 | 
						||
        return this.props.makeRegistrationUrl(params);
 | 
						||
    };
 | 
						||
 | 
						||
    /**
 | 
						||
     * After registration or login, we run various post-auth steps before entering the app
 | 
						||
     * proper, such setting up cross-signing or verifying the new session.
 | 
						||
     *
 | 
						||
     * Note: SSO users (and any others using token login) currently do not pass through
 | 
						||
     * this, as they instead jump straight into the app after `attemptTokenLogin`.
 | 
						||
     */
 | 
						||
    private onUserCompletedLoginFlow = async (credentials: IMatrixClientCreds, password: string): Promise<void> => {
 | 
						||
        this.accountPassword = password;
 | 
						||
        // self-destruct the password after 5mins
 | 
						||
        if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
 | 
						||
        this.accountPasswordTimer = window.setTimeout(() => {
 | 
						||
            this.accountPassword = null;
 | 
						||
            this.accountPasswordTimer = null;
 | 
						||
        }, 60 * 5 * 1000);
 | 
						||
 | 
						||
        // Create and start the client
 | 
						||
        await Lifecycle.setLoggedIn(credentials);
 | 
						||
        await this.postLoginSetup();
 | 
						||
 | 
						||
        PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
 | 
						||
        PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
 | 
						||
    };
 | 
						||
 | 
						||
    // complete security / e2e setup has finished
 | 
						||
    private onCompleteSecurityE2eSetupFinished = (): void => {
 | 
						||
        this.onLoggedIn();
 | 
						||
    };
 | 
						||
 | 
						||
    private getFragmentAfterLogin(): string {
 | 
						||
        let fragmentAfterLogin = "";
 | 
						||
        const initialScreenAfterLogin = this.props.initialScreenAfterLogin;
 | 
						||
        if (
 | 
						||
            initialScreenAfterLogin &&
 | 
						||
            // XXX: workaround for https://github.com/vector-im/element-web/issues/11643 causing a login-loop
 | 
						||
            !["welcome", "login", "register", "start_sso", "start_cas"].includes(initialScreenAfterLogin.screen)
 | 
						||
        ) {
 | 
						||
            fragmentAfterLogin = `/${initialScreenAfterLogin.screen}`;
 | 
						||
        }
 | 
						||
        return fragmentAfterLogin;
 | 
						||
    }
 | 
						||
 | 
						||
    public render(): JSX.Element {
 | 
						||
        const fragmentAfterLogin = this.getFragmentAfterLogin();
 | 
						||
        let view = null;
 | 
						||
 | 
						||
        if (this.state.view === Views.LOADING) {
 | 
						||
            view = (
 | 
						||
                <div className="mx_MatrixChat_splash">
 | 
						||
                    <Spinner />
 | 
						||
                </div>
 | 
						||
            );
 | 
						||
        } else if (this.state.view === Views.COMPLETE_SECURITY) {
 | 
						||
            view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
 | 
						||
        } else if (this.state.view === Views.E2E_SETUP) {
 | 
						||
            view = (
 | 
						||
                <E2eSetup
 | 
						||
                    onFinished={this.onCompleteSecurityE2eSetupFinished}
 | 
						||
                    accountPassword={this.accountPassword}
 | 
						||
                    tokenLogin={!!this.tokenLogin}
 | 
						||
                />
 | 
						||
            );
 | 
						||
        } 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 InvalidStoreError;
 | 
						||
 | 
						||
            // `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 && !isStoreError) {
 | 
						||
                /* 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.get()}
 | 
						||
                        onRegistered={this.onRegistered}
 | 
						||
                        currentRoomId={this.state.currentRoomId}
 | 
						||
                    />
 | 
						||
                );
 | 
						||
            } else {
 | 
						||
                // we think we are logged in, but are still waiting for the /sync to complete
 | 
						||
                let errorBox;
 | 
						||
                if (this.state.syncError && !isStoreError) {
 | 
						||
                    errorBox = (
 | 
						||
                        <div className="mx_MatrixChat_syncError">{messageForSyncError(this.state.syncError)}</div>
 | 
						||
                    );
 | 
						||
                }
 | 
						||
                view = (
 | 
						||
                    <div className="mx_MatrixChat_splash">
 | 
						||
                        {errorBox}
 | 
						||
                        <Spinner />
 | 
						||
                        <div className="mx_MatrixChat_splashButtons">
 | 
						||
                            <AccessibleButton kind="link_inline" onClick={this.onLogoutClick}>
 | 
						||
                                {_t("Logout")}
 | 
						||
                            </AccessibleButton>
 | 
						||
                        </div>
 | 
						||
                    </div>
 | 
						||
                );
 | 
						||
            }
 | 
						||
        } else if (this.state.view === Views.WELCOME) {
 | 
						||
            view = <Welcome />;
 | 
						||
        } 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}
 | 
						||
                    makeRegistrationUrl={this.makeRegistrationUrl}
 | 
						||
                    onLoggedIn={this.onRegisterFlowComplete}
 | 
						||
                    onLoginClick={this.onLoginClick}
 | 
						||
                    onServerConfigChange={this.onServerConfigChange}
 | 
						||
                    defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
 | 
						||
                    fragmentAfterLogin={fragmentAfterLogin}
 | 
						||
                    {...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.startingFragmentQueryParams.defaultUsername as string}
 | 
						||
                    {...this.getServerProperties()}
 | 
						||
                />
 | 
						||
            );
 | 
						||
        } else if (this.state.view === Views.SOFT_LOGOUT) {
 | 
						||
            view = (
 | 
						||
                <SoftLogout
 | 
						||
                    realQueryParams={this.props.realQueryParams}
 | 
						||
                    onTokenLoginCompleted={this.props.onTokenLoginCompleted}
 | 
						||
                    fragmentAfterLogin={fragmentAfterLogin}
 | 
						||
                />
 | 
						||
            );
 | 
						||
        } else if (this.state.view === Views.USE_CASE_SELECTION) {
 | 
						||
            view = <UseCaseSelection onFinished={(useCase): Promise<void> => this.onShowPostLoginScreen(useCase)} />;
 | 
						||
        } else {
 | 
						||
            logger.error(`Unknown view ${this.state.view}`);
 | 
						||
        }
 | 
						||
 | 
						||
        return (
 | 
						||
            <ErrorBoundary>
 | 
						||
                <SDKContext.Provider value={this.stores}>{view}</SDKContext.Provider>
 | 
						||
            </ErrorBoundary>
 | 
						||
        );
 | 
						||
    }
 | 
						||
}
 |