2015-06-23 16:41:25 +01:00
/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
2024-01-04 18:12:08 +00:00
Copyright 2015-2024 The Matrix.org Foundation C.I.C.
2015-06-23 16:41:25 +01:00
2025-01-06 11:18:54 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-09 14:57:16 +01:00
Please see LICENSE files in the repository root for full details.
2015-06-23 16:41:25 +01:00
*/
2016-09-01 11:08:40 +01:00
2025-03-27 10:43:58 +00:00
import React , { type JSX , createRef , lazy } from "react" ;
2022-02-22 12:18:08 +00:00
import {
ClientEvent ,
createClient ,
EventType ,
HttpApiEvent ,
2025-02-05 13:25:06 +00:00
type MatrixClient ,
2025-05-28 19:48:32 +01:00
MatrixEvent ,
MsgType ,
2025-02-05 13:25:06 +00:00
type RoomType ,
2023-08-10 09:01:14 +01:00
SyncState ,
2025-02-05 13:25:06 +00:00
type SyncStateData ,
type TimelineEvents ,
2022-02-22 12:18:08 +00:00
} from "matrix-js-sdk/src/matrix" ;
2025-05-08 11:03:43 +01:00
import { type QueryDict } from "matrix-js-sdk/src/utils" ;
2021-12-09 09:10:23 +00:00
import { logger } from "matrix-js-sdk/src/logger" ;
2022-02-01 18:24:17 +01:00
import { throttle } from "lodash" ;
2025-02-05 13:25:06 +00:00
import { CryptoEvent , type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api" ;
2024-10-14 21:41:58 +05:30
import { TooltipProvider } from "@vector-im/compound-web" ;
2019-12-07 13:07:52 +00:00
// what-input helps improve keyboard accessibility
import "what-input" ;
2025-05-28 19:48:32 +01:00
import sanitizeHtml from "sanitize-html" ;
2025-12-02 13:47:14 +00:00
import { I18nContext } from "@element-hq/web-shared-components" ;
2025-12-15 12:00:35 +00:00
import { LockSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons" ;
2019-09-27 10:05:08 +01:00
2022-02-09 14:25:58 +00:00
import PosthogTrackers from "../../PosthogTrackers" ;
2018-07-06 11:00:39 +01:00
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker" ;
2025-02-05 13:25:06 +00:00
import { type IMatrixClientCreds , MatrixClientPeg } from "../../MatrixClientPeg" ;
2017-05-19 13:53:11 +01:00
import PlatformPeg from "../../PlatformPeg" ;
2025-02-05 13:25:06 +00:00
import SdkConfig , { type ConfigOptions } from "../../SdkConfig" ;
2020-05-13 20:41:41 -06:00
import dis from "../../dispatcher/dispatcher" ;
2019-03-12 17:29:16 +01:00
import Notifier from "../../Notifier" ;
2017-05-19 13:53:11 +01:00
import Modal from "../../Modal" ;
2020-05-13 21:03:12 -06:00
import { showRoomInviteDialog , showStartChatInviteDialog } from "../../RoomInvite" ;
2017-05-19 13:53:11 +01:00
import * as Rooms from "../../Rooms" ;
import * as Lifecycle from "../../Lifecycle" ;
2017-05-26 11:50:32 +01:00
// LifecycleStore is not used but does listen to and dispatch actions
2019-09-27 09:00:54 +01:00
import "../../stores/LifecycleStore" ;
2022-01-13 10:55:25 -05:00
import "../../stores/AutoRageshakeStore" ;
2021-09-27 12:40:34 +02:00
import PageType from "../../PageTypes" ;
2025-02-05 13:25:06 +00:00
import createRoom , { type IOpts } from "../../createRoom" ;
2024-02-12 15:04:13 +00:00
import { _t , _td } from "../../languageHandler" ;
2020-07-28 16:37:09 -06:00
import SettingsStore from "../../settings/SettingsStore" ;
2021-06-16 10:24:51 +01:00
import { startAnyRegistrationFlow } from "../../Registration" ;
2022-07-14 15:03:34 +02:00
import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils" ;
2025-09-02 12:10:10 +01:00
import { calculateRoomVia , makeRoomPermalink } from "../../utils/permalinks/Permalinks" ;
2025-01-28 05:53:35 -05:00
import ThemeWatcher , { ThemeWatcherEvent } from "../../settings/watchers/ThemeWatcher" ;
2020-05-28 13:55:07 +01:00
import { FontWatcher } from "../../settings/watchers/FontWatcher" ;
2019-11-12 11:43:18 +00:00
import { storeRoomAliasInCache } from "../../RoomAliasCache" ;
2020-01-16 20:23:47 +00:00
import ToastStore from "../../stores/ToastStore" ;
2020-02-20 00:38:08 +00:00
import * as StorageManager from "../../utils/StorageManager" ;
2020-04-22 13:22:33 +01:00
import type LoggedInViewType from "./LoggedInView" ;
2021-12-17 11:02:06 +00:00
import LoggedInView from "./LoggedInView" ;
2020-05-13 21:03:12 -06:00
import { Action } from "../../dispatcher/actions" ;
2020-05-22 22:04:21 +01:00
import { hideToast as hideAnalyticsToast , showToast as showAnalyticsToast } from "../../toasts/AnalyticsToast" ;
2021-06-29 13:11:58 +01:00
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast" ;
2025-02-05 13:25:06 +00:00
import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload" ;
2020-07-13 17:01:50 +01:00
import ErrorDialog from "../views/dialogs/ErrorDialog" ;
2021-12-17 11:02:06 +00:00
import {
RoomNotificationStateStore ,
UPDATE_STATUS_INDICATOR ,
} from "../../stores/notifications/RoomNotificationStateStore" ;
2020-07-28 11:53:43 -06:00
import { SettingLevel } from "../../settings/SettingLevel" ;
2025-02-05 13:25:06 +00:00
import ThreepidInviteStore , {
type IThreepidInvite ,
type IThreepidInviteWireFormat ,
} from "../../stores/ThreepidInviteStore" ;
2021-06-29 13:11:58 +01:00
import { UIFeature } from "../../settings/UIFeature" ;
2020-12-23 19:02:01 +00:00
import DialPadModal from "../views/voip/DialPadModal" ;
2021-01-28 17:21:36 +00:00
import { showToast as showMobileGuideToast } from "../../toasts/MobileGuideToast" ;
2021-02-16 15:17:51 -07:00
import { shouldUseLoginForWelcome } from "../../utils/pages" ;
2021-03-18 23:28:59 -06:00
import RoomListStore from "../../stores/room-list/RoomListStore" ;
2021-06-29 13:11:58 +01:00
import { RoomUpdateCause } from "../../stores/room-list/models" ;
2024-04-12 17:15:17 +02:00
import { ModuleRunner } from "../../modules/ModuleRunner" ;
2021-07-03 11:57:05 +02:00
import Spinner from "../views/elements/Spinner" ;
import QuestionDialog from "../views/dialogs/QuestionDialog" ;
2022-03-24 16:42:53 -06:00
import UserSettingsDialog from "../views/dialogs/UserSettingsDialog" ;
2021-07-03 11:57:05 +02:00
import CreateRoomDialog from "../views/dialogs/CreateRoomDialog" ;
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" ;
2021-07-03 12:24:07 +02:00
import VerificationRequestToast from "../views/toasts/VerificationRequestToast" ;
2021-05-19 10:07:02 +01:00
import PerformanceMonitor , { PerformanceEntryNames } from "../../performance" ;
2021-05-25 09:50:09 +01:00
import UIStore , { UI_EVENTS } from "../../stores/UIStore" ;
2021-07-03 11:57:05 +02:00
import SoftLogout from "./auth/SoftLogout" ;
2021-07-14 14:58:18 +01:00
import { copyPlaintext } from "../../utils/strings" ;
2021-08-03 11:55:02 +01:00
import { PosthogAnalytics } from "../../PosthogAnalytics" ;
2021-08-16 13:06:19 +01:00
import { initSentry } from "../../sentry" ;
2022-08-30 15:13:39 -04:00
import LegacyCallHandler from "../../LegacyCallHandler" ;
2021-09-29 14:04:02 +01:00
import { showSpaceInvite } from "../../utils/space" ;
2025-02-05 13:25:06 +00:00
import { type ButtonEvent } from "../views/elements/AccessibleButton" ;
import { type ActionPayload } from "../../dispatcher/payloads" ;
import { type SummarizedNotificationState } from "../../stores/notifications/SummarizedNotificationState" ;
2022-02-09 14:25:58 +00:00
import Views from "../../Views" ;
2025-02-05 13:25:06 +00:00
import { type FocusNextType , type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload" ;
import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload" ;
import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload" ;
2025-05-15 15:34:05 +05:30
import { type AfterForgetRoomPayload } from "../../dispatcher/payloads/AfterForgetRoomPayload" ;
2025-02-05 13:25:06 +00:00
import { type DoAfterSyncPreparedPayload } from "../../dispatcher/payloads/DoAfterSyncPreparedPayload" ;
import { type ViewStartChatOrReusePayload } from "../../dispatcher/payloads/ViewStartChatOrReusePayload" ;
2022-03-24 15:25:44 -06:00
import { leaveRoomBehaviour } from "../../utils/leave-behaviour" ;
2022-08-30 15:13:39 -04:00
import { CallStore } from "../../stores/CallStore" ;
2025-02-05 13:25:06 +00:00
import { type IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators" ;
import { type ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload" ;
2022-07-23 14:13:49 +02:00
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases" ;
import RightPanelStore from "../../stores/right-panel/RightPanelStore" ;
import { TimelineRenderingType } from "../../contexts/RoomContext" ;
2025-02-05 13:25:06 +00:00
import { type ValidatedServerConfig } from "../../utils/ValidatedServerConfig" ;
2022-07-20 09:26:25 +02:00
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom" ;
2024-05-20 10:53:50 -04:00
import { SDKContext , SdkContextClass } from "../../contexts/SDKContext" ;
2022-10-11 11:10:55 +02:00
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings" ;
2022-11-09 10:50:01 +00:00
import GenericToast from "../views/toasts/GenericToast" ;
2023-08-21 10:39:20 +01:00
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog" ;
2023-01-30 10:54:05 +01:00
import { findDMForUser } from "../../utils/dm/findDMForUser" ;
2025-05-28 19:48:32 +01:00
import { getHtmlText , Linkify } from "../../HtmlUtils" ;
2024-01-15 15:25:48 +00:00
import { NotificationLevel } from "../../stores/notifications/NotificationLevel" ;
2025-02-05 13:25:06 +00:00
import { type UserTab } from "../views/dialogs/UserTab" ;
2023-06-27 21:42:31 +12:00
import { shouldSkipSetupEncryption } from "../../utils/crypto/shouldSkipSetupEncryption" ;
2023-08-21 10:39:20 +01:00
import { Filter } from "../views/dialogs/spotlight/Filter" ;
2023-08-24 09:28:43 +01:00
import { SessionLockStolenView } from "./auth/SessionLockStolenView" ;
import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView" ;
2024-01-04 18:12:08 +00:00
import { LoginSplashView } from "./auth/LoginSplashView" ;
2024-08-07 09:35:57 +01:00
import { cleanUpDraftsIfRequired } from "../../DraftCleaner" ;
2024-12-11 13:10:27 +00:00
import { InitialCryptoSetupStore } from "../../stores/InitialCryptoSetupStore" ;
2025-01-28 05:53:35 -05:00
import { setTheme } from "../../theme" ;
2025-05-28 19:48:32 +01:00
import { type OpenForwardDialogPayload } from "../../dispatcher/payloads/OpenForwardDialogPayload" ;
import { ShareFormat , type SharePayload } from "../../dispatcher/payloads/SharePayload" ;
import Markdown from "../../Markdown" ;
import { sanitizeHtmlParams } from "../../Linkify" ;
2025-08-05 12:10:30 +01:00
import { isOnlyAdmin } from "../../utils/membership" ;
2025-11-05 07:24:26 +00:00
import { ModuleApi } from "../../modules/Api.ts" ;
2023-04-28 16:56:26 +02:00
2022-02-09 14:25:58 +00:00
// legacy export
export { default as Views } from "../../Views" ;
2016-06-08 23:03:46 +01:00
2024-09-13 12:12:04 +01:00
const AUTH_SCREENS = [ "register" , "mobile_register" , "login" , "forgot_password" , "start_sso" , "start_cas" , "welcome" ];
2020-11-16 15:56:54 +00:00
2017-11-10 10:51:28 +00:00
// 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.
2025-02-19 16:11:43 +01:00
const ONBOARDING_FLOW_STARTERS = [ Action . ViewUserSettings , Action . CreateChat , Action . CreateRoom ];
2017-11-10 10:51:28 +00:00
2020-04-22 13:08:33 +01:00
interface IScreen {
2020-04-27 23:59:07 +01:00
screen : string ;
2021-07-16 13:11:43 +01:00
params? : QueryDict ;
2020-04-22 13:08:33 +01:00
}
2017-06-14 20:04:55 +01:00
2022-08-09 12:55:49 +01:00
interface IProps {
2023-10-13 10:43:39 +01:00
config : ConfigOptions ;
2020-09-09 16:52:46 +01:00
onNewScreen : ( screen : string , replaceLast : boolean ) => void ;
2020-04-22 13:08:33 +01:00
enableGuest? : boolean ;
// the queryParams extracted from the [real] query-string of the URI
2023-05-09 18:24:40 +01:00
realQueryParams : QueryDict ;
2020-04-22 13:08:33 +01:00
// the initial queryParams extracted from the hash-fragment of the URI
2021-07-16 13:11:43 +01:00
startingFragmentQueryParams? : QueryDict ;
2020-04-22 13:08:33 +01:00
// called when we have completed a token login
2023-04-20 09:49:10 +01:00
onTokenLoginCompleted : () => void ;
2020-04-22 13:08:33 +01:00
// 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.
2020-06-18 14:32:43 +01:00
defaultDeviceDisplayName? : string ;
2020-04-22 13:08:33 +01:00
}
2016-08-11 11:00:15 +01:00
2020-04-22 13:08:33 +01:00
interface IState {
2025-12-02 11:14:00 +00:00
/**
* The master view we are showing.
*
* This represents the state of a state machine: see the documentation on {@link Views} for a transition diagram.
*
* TODO: this doesn't work well, because updates to React state are not instantaneous, meaning that if several
* events or {@link Action}s happen in quick succession, we may end up following the wrong transition.
* We should probably move the view into a separate object (like a ViewModel) and have the React state subscribe
* to updates.
*/
2020-04-23 10:12:50 +01:00
view : Views ;
2025-12-02 11:14:00 +00:00
2025-11-05 07:24:26 +00:00
// What the LoggedInView would be showing if visible.
// A member of the enum for standard pages or a string for those provided by
// a module.
2020-08-29 01:11:08 +01:00
// eslint-disable-next-line camelcase
2025-11-05 07:24:26 +00:00
page_type? : PageType | string ;
2020-04-22 13:08:33 +01:00
// 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.
2023-03-10 14:55:06 +00:00
currentRoomId : string | null ;
2020-04-22 13:08:33 +01:00
// If we're trying to just view a user ID (i.e. /user URL), this is it
2023-03-10 14:55:06 +00:00
currentUserId : string | null ;
2020-04-22 13:08:33 +01:00
// this is persisted as mx_lhs_size, loaded in LoggedInView
collapseLhs : boolean ;
// Parameters used in the registration dance with the IS
2020-08-29 01:11:08 +01:00
// eslint-disable-next-line camelcase
2020-04-22 13:08:33 +01:00
register_client_secret? : string ;
2020-08-29 01:11:08 +01:00
// eslint-disable-next-line camelcase
2020-04-22 13:08:33 +01:00
register_session_id? : string ;
2020-08-29 01:11:08 +01:00
// eslint-disable-next-line camelcase
2020-04-22 13:08:33 +01:00
register_id_sid? : string ;
2024-09-13 12:12:04 +01:00
isMobileRegistration? : boolean ;
2020-04-22 13:08:33 +01:00
// 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 ;
2023-03-10 14:55:06 +00:00
syncError : Error | null ;
2020-04-22 13:08:33 +01:00
serverConfig? : ValidatedServerConfig ;
2025-11-21 14:58:03 +00:00
/** Has our MatrixClient started? */
2020-04-22 13:08:33 +01:00
ready : boolean ;
2025-11-21 14:58:03 +00:00
2021-07-01 23:23:03 +01:00
threepidInvite? : IThreepidInvite ;
2020-04-22 13:08:33 +01:00
roomOobData? : object ;
pendingInitialSync? : boolean ;
2020-11-02 17:24:47 +00:00
justRegistered? : boolean ;
2021-03-01 17:54:53 +00:00
roomJustCreatedOpts? : IOpts ;
2021-10-20 06:55:22 -06:00
forceTimeline? : boolean ; // see props
2020-04-22 13:08:33 +01:00
}
2016-08-11 11:00:15 +01:00
2020-04-22 13:08:33 +01:00
export default class MatrixChat extends React . PureComponent < IProps , IState > {
2022-12-16 12:29:59 +00:00
public static displayName = "MatrixChat" ;
2016-08-12 11:41:06 +01:00
2022-12-16 12:29:59 +00:00
public static defaultProps = {
2020-04-22 11:55:03 +01:00
realQueryParams : {},
startingFragmentQueryParams : {},
config : {},
2023-01-12 13:25:14 +00:00
onTokenLoginCompleted : () : void => {},
2020-04-22 11:55:03 +01:00
};
2017-03-08 10:25:54 +00:00
2021-12-17 11:02:06 +00:00
private firstSyncComplete = false ;
2025-05-08 11:03:43 +01:00
private firstSyncPromise : PromiseWithResolvers < void >;
2017-03-02 14:40:55 +00:00
2020-04-22 13:08:33 +01:00
private screenAfterLogin? : IScreen ;
2025-11-21 14:58:03 +00:00
/** True if we have successfully completed an OIDC or token login.
*
* XXX it's unclear if this is ever cleared, so what happens if the user logs out and then logs back in?
*/
2021-01-27 12:50:12 +00:00
private tokenLogin? : boolean ;
2025-11-21 14:58:03 +00:00
2024-04-10 14:13:08 +01:00
// What to focus on next component update, if anything
private focusNext : FocusNextType ;
2020-04-22 13:08:33 +01:00
private subTitleStatus : string ;
2021-05-28 10:03:46 +01:00
private prevWindowWidth : number ;
2015-10-12 10:13:01 +01:00
2024-11-01 17:39:08 +00:00
private readonly loggedInView = createRef < LoggedInViewType >();
private dispatcherRef? : string ;
private themeWatcher? : ThemeWatcher ;
private fontWatcher? : FontWatcher ;
2022-10-19 13:07:03 +01:00
private readonly stores : SdkContextClass ;
2025-03-25 12:07:07 +00:00
private loadSessionAbortController = new AbortController ();
2017-03-08 10:25:54 +00:00
2025-08-28 17:09:54 +01:00
private sessionLoadStarted = false ;
2022-12-16 12:29:59 +00:00
public constructor ( props : IProps ) {
2021-12-17 11:02:06 +00:00
super ( props );
2022-10-19 13:07:03 +01:00
this . stores = SdkContextClass . instance ;
this . stores . constructEagerStores ();
2016-11-24 12:33:31 +00:00
2020-04-22 11:55:03 +01:00
this . state = {
2020-04-23 10:12:50 +01:00
view : Views.LOADING ,
2017-08-25 12:10:13 +01:00
collapseLhs : false ,
2023-03-10 14:55:06 +00:00
currentRoomId : null ,
currentUserId : null ,
2016-08-16 03:31:02 +05:30
2018-02-08 22:51:07 +01:00
hideToSRUsers : false ,
2024-09-13 12:12:04 +01:00
isMobileRegistration : false ,
2018-09-07 12:18:25 +01:00
syncError : null , // If the current syncing status is ERROR, the error object, otherwise null.
2020-04-22 13:08:33 +01:00
ready : false ,
2015-06-11 18:25:29 +01:00
};
2016-05-06 14:19:56 +01:00
SdkConfig . put ( this . props . config );
2016-08-02 18:46:43 +01:00
2017-04-06 11:30:47 +01:00
// Used by _viewRoom before getting state from sync
this . firstSyncComplete = false ;
2025-05-08 11:03:43 +01:00
this . firstSyncPromise = Promise . withResolvers ();
2017-04-06 11:30:47 +01:00
2016-08-03 09:53:02 +01:00
if ( this . props . config . sync_timeline_limit ) {
MatrixClientPeg . opts . initialSyncLimit = this . props . config . sync_timeline_limit ;
}
2017-02-03 11:48:24 +00:00
2017-08-08 22:19:38 +01:00
// a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a
// rerender.
2020-04-22 13:08:33 +01:00
this . screenAfterLogin = this . props . initialScreenAfterLogin ;
2020-09-11 19:49:48 -06:00
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 );
2021-07-16 13:11:43 +01:00
ThreepidInviteStore . instance . storeInvite ( roomId , params as unknown as IThreepidInviteWireFormat );
2020-09-11 19:49:48 -06:00
}
}
2017-08-08 22:19:38 +01:00
2021-05-28 10:03:46 +01:00
this . prevWindowWidth = UIStore . instance . windowWidth || 1000 ;
2016-08-10 11:33:58 +01:00
2019-09-23 22:01:49 +01:00
// 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 = "" ;
2023-08-24 09:28:43 +01:00
}
/**
* Kick off a call to {@link initSession}, and handle any errors
*/
private startInitSession = () : void => {
2024-10-03 09:55:06 +01:00
const initProm = this . initSession ();
initProm . catch (( err ) => {
2023-08-14 13:52:08 +01:00
// TODO: show an error screen, rather than a spinner of doom
logger . error ( "Error initialising Matrix session" , err );
});
2023-08-24 09:28:43 +01:00
};
2017-06-14 10:53:48 +01:00
2023-08-14 13:52:08 +01:00
/**
* Do what we can to establish a Matrix session.
*
* * Special-case soft-logged-out sessions
* * If we have OIDC or token login parameters, follow them
* * If we have a guest access token in the query params, use that
* * If we have parameters in local storage, use them
* * Attempt to auto-register as a guest
* * If all else fails, present a login screen.
*/
private async initSession () : Promise < void > {
2023-08-24 09:28:43 +01:00
// The Rust Crypto SDK will break if two Element instances try to use the same datastore at once, so
// make sure we are the only Element instance in town (on this browser/domain).
2025-09-23 15:46:01 +01:00
const platform = PlatformPeg . get ();
if ( platform && ! ( await platform . getSessionLock (() => this . onSessionLockStolen ()))) {
2023-08-24 09:28:43 +01:00
// we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do.
return ;
}
2023-08-14 13:52:08 +01:00
// If the user was soft-logged-out, we want to make the SoftLogout component responsible for doing any
// token auth (rather than Lifecycle.attemptDelegatedAuthLogin), since SoftLogout knows about submitting the
// device ID and preserving the session.
//
// So, we start by special-casing soft-logged-out sessions.
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.
2025-03-25 12:07:07 +00:00
Lifecycle . loadSession ({ abortSignal : this.loadSessionAbortController.signal });
2023-08-14 13:52:08 +01:00
return ;
2019-07-08 12:43:16 -06:00
}
2018-05-15 13:15:40 +01:00
2023-08-14 13:52:08 +01:00
// Otherwise, the first thing to do is to try the token params in the query-string
const delegatedAuthSucceeded = await Lifecycle . attemptDelegatedAuthLogin (
this . props . realQueryParams ,
this . props . defaultDeviceDisplayName ,
this . getFragmentAfterLogin (),
);
// remove the loginToken or auth code from the URL regardless
if (
this . props . realQueryParams ? . loginToken ||
this . props . realQueryParams ? . code ||
this . props . realQueryParams ? . state
) {
this . props . onTokenLoginCompleted ();
}
if ( delegatedAuthSucceeded ) {
// token auth/OIDC worked! Time to fire up the client.
this . tokenLogin = true ;
// Create and start the client
// accesses the new credentials just set in storage during attemptDelegatedAuthLogin
// and sets logged in state
2024-09-11 16:13:04 +01:00
await Lifecycle . restoreSessionFromStorage ({ ignoreGuest : true });
2023-08-14 13:52:08 +01:00
await this . postLoginSetup ();
return ;
}
// 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 ;
}
2023-10-03 16:19:54 +01:00
// If the first screen is an auth screen, we don't want to wait for login.
if ( firstScreen !== null && AUTH_SCREENS . includes ( firstScreen )) {
2023-08-14 13:52:08 +01:00
this . showScreenAfterLogin ();
}
2020-04-22 11:55:03 +01:00
}
2015-06-12 14:59:33 +01:00
2023-08-24 09:28:43 +01:00
private async onSessionLockStolen () : Promise < void > {
// switch to the LockStolenView. We deliberately do this immediately, rather than going through the dispatcher,
// because there can be a substantial queue in the dispatcher, and some of the events in it might require an
// active MatrixClient.
await new Promise < void >(( resolve ) => {
this . setState ({ view : Views.LOCK_STOLEN }, resolve );
});
// now we can tell the Lifecycle routines to abort any active startup, and to stop the active client.
await Lifecycle . onSessionLockStolen ();
}
2025-11-21 14:58:03 +00:00
/**
2025-11-26 11:29:58 +00:00
* Perform actions that are specific to a user that has just logged in.
2025-11-21 14:58:03 +00:00
*
* Called when:
*
* - We successfully completed an OIDC or token login, via {@link initSession}.
* - The {@link Login} or {@link Register} components notify us that we successfully completed a non-OIDC login or
* registration.
*
2025-11-26 11:29:58 +00:00
* In both cases, {@link Action.OnLoggedIn} will already have been emitted, but the call to {@link onShowPostLoginScreen} will
2025-11-21 14:58:03 +00:00
* have been suppressed (by either {@link tokenLogin} being set, or the view being set to {@link Views.LOGIN} or
* {@link Views.REGISTER}).
*
* {@link onWillStartClient} and {@link onClientStarted} will already have been called (but not necessarily
* completed).
*
* This method either calls {@link onLiggedIn} directly, or switches to {@link Views.E2E_SETUP} or
* {@link Views.COMPLETE_SECURITY}, which will later call {@link onCompleteSecurityE2eSetupFinished}.
*/
2023-01-12 13:25:14 +00:00
private async postLoginSetup () : Promise < void > {
2023-06-15 15:11:49 +01:00
const cli = MatrixClientPeg . safeGet ();
2024-10-15 16:24:58 +02:00
const cryptoEnabled = Boolean ( cli . getCrypto ());
2021-01-28 11:00:04 +00:00
if ( ! cryptoEnabled ) {
2025-11-26 11:29:58 +00:00
this . onShowPostLoginScreen ();
2021-01-28 11:00:04 +00:00
}
2021-01-27 12:50:12 +00:00
2021-06-01 22:21:04 -06:00
const promisesList : Promise < any >[] = [ this . firstSyncPromise . promise ];
2022-12-09 14:14:06 +00:00
let crossSigningIsSetUp = false ;
2021-01-27 12:50:12 +00:00
if ( cryptoEnabled ) {
2022-12-09 14:14:06 +00:00
// 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 (
2023-01-12 13:25:14 +00:00
( async () : Promise < void > => {
2024-10-15 09:50:26 +02:00
crossSigningIsSetUp = Boolean ( await cli . getCrypto () ? . userHasCrossSigningKeys ());
2022-12-09 14:14:06 +00:00
})(),
);
2021-01-27 12:50:12 +00:00
}
// 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 ) {
2022-12-09 14:14:06 +00:00
// if the user has previously set up cross-signing, verify this device so we can fetch the
// private keys.
2024-04-12 17:15:17 +02:00
const cryptoExtension = ModuleRunner . instance . extensions . cryptoSetup ;
if ( cryptoExtension . SHOW_ENCRYPTION_SETUP_UI == false ) {
2025-11-26 11:29:58 +00:00
this . onShowPostLoginScreen ();
2021-04-08 09:20:14 -06:00
} else {
2021-06-29 13:11:58 +01:00
this . setStateForNewView ({ view : Views.COMPLETE_SECURITY });
2021-04-08 09:20:14 -06:00
}
2025-11-19 12:28:08 -05:00
} else if ( ! ( await shouldSkipSetupEncryption ( cli ))) {
2022-12-09 14:14:06 +00:00
// if cross-signing is not yet set up, do so now if possible.
2024-12-11 13:10:27 +00:00
InitialCryptoSetupStore . sharedInstance (). startInitialCryptoSetup (
cli ,
this . onCompleteSecurityE2eSetupFinished ,
);
2021-01-27 12:50:12 +00:00
this . setStateForNewView ({ view : Views.E2E_SETUP });
} else {
2025-11-26 11:29:58 +00:00
this . onShowPostLoginScreen ();
2021-01-27 12:50:12 +00:00
}
this . setState ({ pendingInitialSync : false });
}
2022-11-18 09:22:43 +00:00
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 {
2025-12-02 11:14:00 +00:00
if ( state && "view" in state ) {
logger . debug ( `MatrixChat: Queuing change of view from ${ Views [ this . state . view ] } to ${ Views [ state . view ] } ` );
}
2022-11-18 09:22:43 +00:00
if ( this . shouldTrackPageChange ( this . state , { ... this . state , ... state })) {
2020-04-22 11:55:03 +01:00
this . startPageChangeTimer ();
}
2022-11-18 09:22:43 +00:00
super . setState < K >( state , callback );
2020-04-22 11:55:03 +01:00
}
2022-02-01 18:24:17 +01:00
public componentDidMount () : void {
2024-11-01 17:39:08 +00:00
UIStore . instance . on ( UI_EVENTS . Resize , this . handleResize );
// For PersistentElement
2025-10-06 10:23:06 +01:00
this . stores . resizeNotifier . on ( "middlePanelResized" , this . dispatchTimelineResize );
2024-11-01 17:39:08 +00:00
RoomNotificationStateStore . instance . on ( UPDATE_STATUS_INDICATOR , this . onUpdateStatusIndicator );
this . dispatcherRef = dis . register ( this . onAction );
this . themeWatcher = new ThemeWatcher ();
this . fontWatcher = new FontWatcher ();
this . themeWatcher . start ();
2025-01-28 05:53:35 -05:00
this . themeWatcher . on ( ThemeWatcherEvent . Change , setTheme );
2024-11-01 17:39:08 +00:00
this . fontWatcher . start ();
initSentry ( SdkConfig . get ( "sentry" ));
2022-02-01 18:24:17 +01:00
window . addEventListener ( "resize" , this . onWindowResized );
2025-08-28 17:09:54 +01:00
// Once we start loading the MatrixClient, we can't stop, even if MatrixChat gets unmounted (as it does
// in React's Strict Mode). So, start loading the session now, but only if this MatrixChat was not previously
// mounted.
if ( ! this . sessionLoadStarted ) {
this . sessionLoadStarted = true ;
2025-09-23 15:46:01 +01:00
const platform = PlatformPeg . get ();
if ( platform && ! platform . checkSessionLockFree ()) {
2025-08-28 17:09:54 +01:00
// another instance holds the lock; confirm its theft before proceeding
setTimeout (() => this . setState ({ view : Views.CONFIRM_LOCK_THEFT }), 0 );
} else {
this . startInitSession ();
}
}
2022-02-01 18:24:17 +01:00
}
2023-02-13 11:39:16 +00:00
public componentDidUpdate ( prevProps : IProps , prevState : IState ) : void {
2020-04-22 11:55:03 +01:00
if ( this . shouldTrackPageChange ( prevState , this . state )) {
const durationMs = this . stopPageChangeTimer ();
2023-04-28 09:45:36 +01:00
if ( durationMs != null ) {
PosthogTrackers . instance . trackPageChange ( this . state . view , this . state . page_type , durationMs );
}
2020-04-22 11:55:03 +01:00
}
2024-04-10 14:13:08 +01:00
if ( this . focusNext === "composer" ) {
2021-07-08 17:36:31 +02:00
dis . fire ( Action . FocusSendMessageComposer );
2024-04-10 14:13:08 +01:00
this . focusNext = undefined ;
} else if ( this . focusNext === "threadsPanel" ) {
dis . fire ( Action . FocusThreadsPanel );
2020-04-22 11:55:03 +01:00
}
}
2023-01-12 13:25:14 +00:00
public componentWillUnmount () : void {
2020-04-22 11:55:03 +01:00
Lifecycle . stopMatrixClient ();
dis . unregister ( this . dispatcherRef );
2025-01-28 05:53:35 -05:00
this . themeWatcher ? . off ( ThemeWatcherEvent . Change , setTheme );
2024-11-01 17:39:08 +00:00
this . themeWatcher ? . stop ();
this . fontWatcher ? . stop ();
2021-05-25 09:50:09 +01:00
UIStore . destroy ();
2025-10-06 10:23:06 +01:00
this . stores . resizeNotifier . removeListener ( "middlePanelResized" , this . dispatchTimelineResize );
2022-02-01 18:24:17 +01:00
window . removeEventListener ( "resize" , this . onWindowResized );
2020-04-22 11:55:03 +01:00
}
2022-02-01 18:24:17 +01:00
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" ;
2023-10-02 16:13:00 +05:30
const waitText = _t ( "console_wait" );
2023-09-21 09:11:26 +01:00
const scamText = _t ( "console_scam_warning" );
const devText = _t ( "console_dev_note" );
2022-02-01 18:24:17 +01:00
2022-02-04 14:30:18 +01:00
global . mx_rage_logger . bypassRageshake (
"log" ,
2022-02-01 18:24:17 +01:00
`%c ${ waitText } \ n%c ${ scamText } \ n%c ${ devText } ` ,
`font-size: ${ largeFontSize } ; color:blue;` ,
`font-size: ${ normalFontSize } ; color:red;` ,
`font-size: ${ normalFontSize } ;` ,
);
}, 1000 );
2023-04-13 08:52:57 +01:00
private getFallbackHsUrl () : string | undefined {
2023-04-28 11:16:07 +01:00
if ( this . getServerProperties (). serverConfig ? . isDefault ) {
2023-04-13 08:52:57 +01:00
return this . props . config . fallback_hs_url ;
2020-04-22 11:55:03 +01:00
}
}
2023-01-12 13:25:14 +00:00
private getServerProperties () : { serverConfig : ValidatedServerConfig } {
2023-04-28 11:16:07 +01:00
const props = this . state . serverConfig || SdkConfig . get ( "validated_server_config" ) ! ;
2021-06-29 13:11:58 +01:00
return { serverConfig : props };
2020-04-22 11:55:03 +01:00
}
2022-10-13 09:22:32 +01:00
private loadSession () : Promise < boolean > {
2019-03-07 17:04:50 -07:00
// 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 ,
2019-05-02 23:09:07 -06:00
guestHsUrl : this.getServerProperties (). serverConfig . hsUrl ,
guestIsUrl : this.getServerProperties (). serverConfig . isUrl ,
2019-03-07 17:04:50 -07:00
defaultDeviceDisplayName : this.props.defaultDeviceDisplayName ,
2025-03-25 12:07:07 +00:00
abortSignal : this.loadSessionAbortController.signal ,
2019-03-07 17:04:50 -07:00
});
})
. then (( loadedSession ) => {
if ( ! loadedSession ) {
2020-09-11 19:53:52 -06:00
// fall back to showing the welcome screen... unless we have a 3pid invite pending
2024-07-15 11:02:23 +01:00
if (
ThreepidInviteStore . instance . pickBestInvite () &&
SettingsStore . getValue ( UIFeature . Registration )
) {
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "start_registration" });
2020-09-11 19:53:52 -06:00
} else {
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "view_welcome_page" });
2022-12-12 12:24:14 +01:00
}
2020-09-11 19:53:52 -06:00
}
2022-10-13 09:22:32 +01:00
return loadedSession ;
2019-03-07 17:04:50 -07:00
});
// 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.
2020-04-22 11:55:03 +01:00
}
2015-06-18 15:03:57 +01:00
2023-01-12 13:25:14 +00:00
private startPageChangeTimer () : void {
2021-05-19 10:07:02 +01:00
PerformanceMonitor . instance . start ( PerformanceEntryNames . PAGE_CHANGE );
2020-04-22 11:55:03 +01:00
}
2018-03-28 10:25:28 +01:00
2023-01-12 13:25:14 +00:00
private stopPageChangeTimer () : number | null {
2021-05-19 10:07:02 +01:00
const perfMonitor = PerformanceMonitor . instance ;
2018-06-04 09:38:21 +01:00
2021-05-19 10:07:02 +01:00
perfMonitor . stop ( PerformanceEntryNames . PAGE_CHANGE );
2018-05-03 14:08:10 +01:00
2021-05-19 10:07:02 +01:00
const entries = perfMonitor . getEntries ({
name : PerformanceEntryNames.PAGE_CHANGE ,
});
const measurement = entries . pop ();
2018-05-03 14:08:10 +01:00
2021-05-19 10:07:02 +01:00
return measurement ? measurement.duration : null ;
2020-04-22 11:55:03 +01:00
}
2018-03-28 10:25:28 +01:00
2021-12-17 11:02:06 +00:00
private shouldTrackPageChange ( prevState : IState , state : IState ) : boolean {
2018-03-27 11:17:49 +01:00
return (
prevState . currentRoomId !== state . currentRoomId ||
prevState . view !== state . view ||
prevState . page_type !== state . page_type
);
2020-04-22 11:55:03 +01:00
}
2018-03-27 11:17:49 +01:00
2025-11-21 17:04:52 +00:00
private isLoggedInViewPageDisplayed () : boolean {
return this . loggedInView . current !== null && this . state . page_type !== undefined ;
}
2021-12-17 11:02:06 +00:00
private setStateForNewView ( state : Partial < IState >) : void {
2017-06-14 20:04:55 +01:00
if ( state . view === undefined ) {
throw new Error ( "setStateForNewView with no view!" );
}
2023-02-13 11:39:16 +00:00
this . setState ({
currentUserId : undefined ,
2020-11-02 17:24:47 +00:00
justRegistered : false ,
2023-02-13 11:39:16 +00:00
... state ,
} as IState );
2020-04-22 11:55:03 +01:00
}
2016-09-30 11:56:32 +01:00
2021-12-17 11:02:06 +00:00
private onAction = ( payload : ActionPayload ) : void => {
2023-08-24 09:28:43 +01:00
// once the session lock has been stolen, don't try to do anything.
if ( this . state . view === Views . LOCK_STOLEN ) {
2025-12-02 11:14:00 +00:00
logger . warn ( `MatrixChat: ignoring action ${ payload . action } as session lock has been stolen` );
2023-08-24 09:28:43 +01:00
return ;
}
2025-12-02 11:14:00 +00:00
// Exclude some rather spammy actions from being logged.
if ( payload . action != Action . UserActivity ) {
logger . debug ( `MatrixChat: handling action ${ payload . action } ` );
}
2017-11-10 10:51:28 +00:00
// Start the onboarding process for certain actions
2025-02-19 16:11:43 +01:00
if (
MatrixClientPeg . get () ? . isGuest () &&
ONBOARDING_FLOW_STARTERS . includes ( payload . action as unknown as Action )
) {
2017-11-10 10:51:28 +00:00
// 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 ({
2022-02-22 11:04:27 +01:00
action : Action.DoAfterSyncPrepared ,
2017-11-10 10:51:28 +00:00
deferred_action : payload ,
});
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "require_registration" });
2017-11-10 10:51:28 +00:00
return ;
}
2015-06-12 17:34:17 +01:00
switch ( payload . action ) {
2019-08-16 13:10:41 -06:00
case "MatrixActions.accountData" :
// XXX: This is a collection of several hacks to solve a minor problem. We want to
2021-07-13 15:26:38 +01:00
// update our local state when the identity server changes, but don't want to put that in
2019-08-16 13:10:41 -06:00
// 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 ) {
2023-06-15 15:11:49 +01:00
MatrixClientPeg . safeGet (). setIdentityServerUrl ( undefined );
2019-08-16 13:10:41 -06:00
localStorage . removeItem ( "mx_is_access_token" );
localStorage . removeItem ( "mx_is_url" );
} else {
2023-06-15 15:11:49 +01:00
MatrixClientPeg . safeGet (). setIdentityServerUrl ( fullUrl );
2019-08-16 13:10:41 -06:00
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
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "id_server_changed" });
2019-08-16 13:10:41 -06:00
}
break ;
2015-06-12 17:34:17 +01:00
case "logout" :
2022-08-30 15:13:39 -04:00
LegacyCallHandler . instance . hangupAllCalls ();
2024-12-02 10:53:27 +00:00
Promise . all ([...[... CallStore . instance . connectedCalls ]. map (( call ) => call . disconnect ())]). finally (() =>
Lifecycle . logout ( this . stores . oidcClientStore ),
);
2015-06-12 17:34:17 +01:00
break ;
2018-09-05 17:07:39 +01:00
case "require_registration" :
2021-12-06 09:25:05 +00:00
startAnyRegistrationFlow ( payload as any );
2018-09-05 17:07:39 +01:00
break ;
2024-09-13 12:12:04 +01:00
case "start_mobile_registration" :
this . startRegistration ( payload . params || {}, true );
break ;
2015-07-13 19:14:02 +01:00
case "start_registration" :
2019-07-04 16:51:55 -06:00
if ( Lifecycle . isSoftLogout ()) {
2020-04-22 13:08:33 +01:00
this . onSoftLogout ();
2019-07-04 16:51:55 -06:00
break ;
}
2018-09-05 17:07:39 +01:00
// This starts the full registration flow
2019-06-10 10:24:06 +01:00
if ( payload . screenAfterLogin ) {
2020-04-22 13:08:33 +01:00
this . screenAfterLogin = payload . screenAfterLogin ;
2019-06-10 10:24:06 +01:00
}
2020-04-22 13:08:33 +01:00
this . startRegistration ( payload . params || {});
2015-07-13 19:14:02 +01:00
break ;
case "start_login" :
2019-07-04 16:51:55 -06:00
if ( Lifecycle . isSoftLogout ()) {
2020-04-22 13:08:33 +01:00
this . onSoftLogout ();
2019-07-04 16:51:55 -06:00
break ;
}
2019-06-10 10:24:06 +01:00
if ( payload . screenAfterLogin ) {
2020-04-22 13:08:33 +01:00
this . screenAfterLogin = payload . screenAfterLogin ;
2019-06-10 10:24:06 +01:00
}
2021-03-12 15:01:05 -07:00
this . viewLogin ();
2015-11-20 11:57:04 +00:00
break ;
2016-01-12 17:20:16 +00:00
case "start_password_recovery" :
2017-06-09 17:18:45 +01:00
this . setStateForNewView ({
2020-04-23 10:12:50 +01:00
view : Views.FORGOT_PASSWORD ,
2016-01-12 17:20:16 +00:00
});
this . notifyNewScreen ( "forgot_password" );
2015-12-13 13:49:28 +00:00
break ;
2017-06-01 14:16:25 +01:00
case "start_chat" :
2023-06-15 15:11:49 +01:00
createRoom ( MatrixClientPeg . safeGet (), {
2017-06-01 14:16:25 +01:00
dmUserId : payload.user_id ,
2015-12-13 13:49:28 +00:00
});
2015-07-13 19:14:02 +01:00
break ;
2015-12-13 13:49:28 +00:00
case "leave_room" :
2020-04-22 13:08:33 +01:00
this . leaveRoom ( payload . room_id );
2015-07-13 19:14:02 +01:00
break ;
2020-07-13 16:35:03 +01:00
case "forget_room" :
this . forgetRoom ( payload . room_id );
break ;
2021-07-14 14:58:18 +01:00
case "copy_room" :
this . copyRoom ( payload . room_id );
break ;
2019-04-17 10:57:45 +02:00
case "view_user_info" :
2020-04-22 13:08:33 +01:00
this . viewUser ( payload . userId , payload . subAction );
2019-04-17 10:57:45 +02:00
break ;
2022-05-26 10:19:00 +01:00
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 ;
}
2021-11-25 17:49:43 -03:00
case Action . ViewRoom : {
2016-06-14 12:56:37 +01:00
// 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.
2022-02-10 14:29:55 +00:00
const promise = this . viewRoom ( payload as ViewRoomPayload );
2020-02-28 23:12:10 +00:00
if ( payload . deferred_action ) {
promise . then (() => {
dis . dispatch ( payload . deferred_action );
});
}
2015-06-12 18:01:38 +01:00
break ;
2020-02-28 23:12:10 +00:00
}
2022-10-11 11:10:55 +02:00
case Action . ViewUserDeviceSettings : {
2023-05-26 13:58:28 +12:00
viewUserDeviceSettings ();
2022-10-11 11:10:55 +02:00
break ;
}
2020-05-13 21:07:19 -06:00
case Action . ViewUserSettings : {
2020-06-08 08:20:15 -06:00
const tabPayload = payload as OpenToTabPayload ;
2022-06-14 17:51:51 +01:00
Modal . createDialog (
UserSettingsDialog ,
2024-06-06 09:57:28 +01:00
{ ... payload . props , initialTabId : tabPayload.initialTabId as UserTab , sdkContext : this.stores },
2023-03-10 14:55:06 +00:00
/*className=*/ undefined ,
2020-08-29 01:11:08 +01:00
/*isPriority=*/ false ,
/*isStatic=*/ true ,
);
2019-02-04 13:25:26 -07:00
2019-02-11 13:31:27 +00:00
// View the welcome or home page if we need something to look at
2020-04-22 13:08:33 +01:00
this . viewSomethingBehindModal ();
2015-09-18 18:39:16 +01:00
break ;
2019-01-21 17:49:48 -07:00
}
2025-02-19 16:11:43 +01:00
case Action.CreateRoom :
2022-04-01 10:36:10 -04:00
this.createRoom ( payload . public , payload . defaultName , payload . type );
2022-02-18 09:43:19 +01:00
// View the welcome or home page if we need something to look at
this . viewSomethingBehindModal ();
2015-09-18 18:39:16 +01:00
break ;
2020-06-08 20:33:21 -06:00
case Action . ViewRoomDirectory : {
2022-11-22 15:05:09 +00:00
Modal . createDialog (
RovingSpotlightDialog ,
{
2021-11-30 18:08:46 +00:00
initialText : payload.initialText ,
2022-11-22 15:05:09 +00:00
initialFilter : Filter.PublicRooms ,
},
"mx_SpotlightDialog_wrapper" ,
false ,
true ,
);
2019-01-29 23:25:07 -07:00
2019-02-11 13:31:27 +00:00
// View the welcome or home page if we need something to look at
2020-04-22 13:08:33 +01:00
this . viewSomethingBehindModal ();
2020-04-22 11:55:03 +01:00
break ;
2019-01-29 15:40:19 +01:00
}
2019-02-07 16:25:09 +00:00
case "view_welcome_page" :
2020-04-22 13:08:33 +01:00
this . viewWelcome ();
2019-02-07 16:25:09 +00:00
break ;
2022-02-22 11:04:27 +01:00
case Action.ViewHomePage :
2020-11-02 17:24:47 +00:00
this.viewHome ( payload . justRegistered );
2016-11-13 14:10:46 +00:00
break ;
2025-05-28 19:48:32 +01:00
case Action.Share :
this.viewShare ( payload . format , payload . msg );
break ;
2022-02-22 11:04:27 +01:00
case Action.ViewStartChatOrReuse :
2020-04-22 13:08:33 +01:00
this.chatCreateOrReuse ( payload . user_id );
2017-06-01 14:16:25 +01:00
break ;
2025-02-19 16:11:43 +01:00
case Action.CreateChat :
2020-11-11 13:36:17 +00:00
showStartChatInviteDialog ( payload . initialText || "" );
2022-02-18 09:43:19 +01:00
// View the welcome or home page if we need something to look at
this . viewSomethingBehindModal ();
2016-09-01 16:45:24 +01:00
break ;
2021-09-29 14:04:02 +01:00
case "view_invite" : {
2023-06-15 15:11:49 +01:00
const room = MatrixClientPeg . safeGet (). getRoom ( payload . roomId );
2021-09-29 14:04:02 +01:00
if ( room ? . isSpaceRoom ()) {
showSpaceInvite ( room );
} else {
showRoomInviteDialog ( payload . roomId );
}
2016-09-13 14:28:03 +01:00
break ;
2021-09-29 14:04:02 +01:00
}
2015-10-11 13:49:44 +01:00
case "hide_left_panel" :
this . setState (
{
2017-08-25 12:10:13 +01:00
collapseLhs : true ,
2020-07-16 06:05:53 +01:00
},
() => {
2025-10-06 10:23:06 +01:00
this . stores . resizeNotifier . notifyLeftHandleResized ();
2015-10-11 13:49:44 +01:00
},
);
break ;
case "show_left_panel" :
this . setState (
{
2017-08-25 12:10:13 +01:00
collapseLhs : false ,
2020-07-16 06:05:53 +01:00
},
() => {
2025-10-06 10:23:06 +01:00
this . stores . resizeNotifier . notifyLeftHandleResized ();
2015-10-11 13:49:44 +01:00
},
);
break ;
2020-12-23 19:02:01 +00:00
case Action.OpenDialPad :
2022-06-14 17:51:51 +01:00
Modal.createDialog ( DialPadModal , {}, "mx_Dialog_dialPadWrapper" );
2020-12-23 19:02:01 +00:00
break ;
2022-05-26 09:56:53 +01:00
case Action.OnLoggedIn :
2025-11-26 11:29:58 +00:00
this.onLoggedIn ();
2019-07-03 16:46:37 -06:00
break ;
2025-11-20 18:18:04 +00:00
case Action.ClientNotViable :
2020-04-22 13:08:33 +01:00
this.onSoftLogout ();
2016-08-02 14:04:20 +01:00
break ;
2022-05-26 09:56:53 +01:00
case Action.OnLoggedOut :
2020-04-22 13:08:33 +01:00
this.onLoggedOut ();
2016-08-02 14:04:20 +01:00
break ;
2025-11-20 18:18:04 +00:00
case Action.WillStartClient :
2021-06-29 13:11:58 +01:00
this.setState ({ ready : false }, () => {
2017-06-23 10:48:21 +01:00
// 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.
2020-04-22 13:08:33 +01:00
this . onWillStartClient ();
2017-06-23 10:48:21 +01:00
});
2016-08-10 00:03:29 +01:00
break ;
2025-11-20 18:18:04 +00:00
case Action . ClientStarted :
2024-10-03 09:55:06 +01:00
// No need to make this handler async to wait for the result of this
this . onClientStarted (). catch (( e ) => {
logger . error ( "Exception in onClientStarted" , e );
});
2017-11-04 20:13:23 -07:00
break ;
2017-06-16 16:12:52 +01:00
case "send_event" :
this . onSendEvent ( payload . room_id , payload . event );
break ;
2018-02-08 22:51:07 +01:00
case "aria_hide_main_app" :
this . setState ({
hideToSRUsers : true ,
});
break ;
case "aria_unhide_main_app" :
this . setState ({
hideToSRUsers : false ,
});
break ;
2021-12-06 09:39:33 +11:00
case Action.PseudonymousAnalyticsAccept :
2020-05-22 22:04:21 +01:00
hideAnalyticsToast ();
2021-12-06 09:39:33 +11:00
SettingsStore . setValue ( "pseudonymousAnalyticsOptIn" , null , SettingLevel . ACCOUNT , true );
break ;
case Action.PseudonymousAnalyticsReject :
hideAnalyticsToast ();
SettingsStore . setValue ( "pseudonymousAnalyticsOptIn" , null , SettingLevel . ACCOUNT , false );
2018-05-15 13:15:40 +01:00
break ;
2022-07-23 14:13:49 +02:00
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 ;
}
2023-08-21 10:39:20 +01:00
case Action.OpenSpotlight :
Modal.createDialog (
RovingSpotlightDialog ,
{
initialText : payload.initialText ,
initialFilter : payload.initialFilter ,
},
"mx_SpotlightDialog_wrapper" ,
false ,
true ,
);
break ;
2015-06-12 17:34:17 +01:00
}
2020-04-22 11:55:03 +01:00
};
2015-06-12 17:34:17 +01:00
2023-01-12 13:25:14 +00:00
private setPage ( pageType : PageType ) : void {
2015-12-10 21:53:14 +00:00
this . setState ({
2015-12-14 11:07:59 +00:00
page_type : pageType ,
2015-12-10 21:53:14 +00:00
});
2020-04-22 11:55:03 +01:00
}
2015-12-10 21:53:14 +00:00
2024-09-13 12:12:04 +01:00
private async startRegistration ( params : { [ key : string ] : string }, isMobileRegistration? : boolean ) : Promise < void > {
2024-09-20 12:24:39 +01:00
// If registration is disabled or mobile registration is requested but not enabled in settings redirect to the welcome screen
if (
! SettingsStore . getValue ( UIFeature . Registration ) ||
( isMobileRegistration && ! SettingsStore . getValue ( "Registration.mobileRegistrationHelper" ))
) {
2024-07-15 11:02:23 +01:00
this . showScreen ( "welcome" );
return ;
}
2020-04-22 13:08:33 +01:00
const newState : Partial < IState > = {
2020-04-23 10:12:50 +01:00
view : Views.REGISTER ,
2018-02-07 15:51:03 +00:00
};
2024-09-20 12:24:39 +01:00
if ( isMobileRegistration && params . hs_url ) {
2024-09-13 16:59:11 +01:00
try {
const config = await AutoDiscoveryUtils . validateServerConfigWithStaticUrls ( params . hs_url );
newState . serverConfig = config ;
2024-10-16 16:43:07 +01:00
} catch {
2024-09-13 16:59:11 +01:00
logger . warn ( "Failed to load hs_url param:" , params . hs_url );
}
} else if ( params . client_secret && params . session_id && params . hs_url && params . is_url && params . sid ) {
// Only honour params if they are all present, otherwise we reset
// HS and IS URLs when switching to registration.
2019-06-12 18:11:58 +01:00
newState . serverConfig = await AutoDiscoveryUtils . validateServerConfigWithStaticUrls (
params . hs_url ,
params . is_url ,
);
2021-10-19 09:43:34 +01:00
// If the hs url matches then take the hs name we know locally as it is likely prettier
2022-03-18 10:12:36 -06:00
const defaultConfig = SdkConfig . get ( "validated_server_config" );
2021-10-19 09:43:34 +01:00
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 ;
}
2018-02-07 15:51:03 +00:00
newState . register_client_secret = params . client_secret ;
newState . register_session_id = params . session_id ;
newState . register_id_sid = params . sid ;
}
2024-09-20 12:24:39 +01:00
newState . isMobileRegistration = isMobileRegistration ;
2024-09-13 12:12:04 +01:00
2018-02-07 15:51:03 +00:00
this . setStateForNewView ( newState );
2024-09-20 12:24:39 +01:00
this . notifyNewScreen ( isMobileRegistration ? "mobile_register" : "register" );
2020-04-22 11:55:03 +01:00
}
2017-05-19 13:53:11 +01:00
2016-02-01 16:31:12 +00:00
// switch view to the given room
2023-01-12 13:25:14 +00:00
private async viewRoom ( roomInfo : ViewRoomPayload ) : Promise < void > {
2024-04-10 14:13:08 +01:00
this . focusNext = roomInfo . focusNext ?? "composer" ;
2019-07-23 09:13:51 +02:00
2017-06-07 22:41:02 +01:00
if ( roomInfo . room_alias ) {
2021-12-06 09:25:05 +00:00
logger . log ( `Switching to room alias ${ roomInfo . room_alias } at event ${ roomInfo . event_id } ` );
2017-06-07 22:41:02 +01:00
} else {
2021-12-06 09:25:05 +00:00
logger . log ( `Switching to room id ${ roomInfo . room_id } at event ${ roomInfo . event_id } ` );
2016-02-01 16:31:12 +00:00
}
2017-04-06 11:30:47 +01:00
// Wait for the first sync to complete so that if a room does have an alias,
// it would have been retrieved.
2017-04-06 11:44:15 +01:00
if ( ! this . firstSyncComplete ) {
2021-12-06 09:25:05 +00:00
await this . firstSyncPromise . promise ;
2017-04-06 11:30:47 +01:00
}
2023-04-28 09:45:36 +01:00
let presentedId = roomInfo . room_alias || roomInfo . room_id ! ;
2023-06-15 15:11:49 +01:00
const room = MatrixClientPeg . safeGet (). getRoom ( roomInfo . room_id );
2021-12-06 09:25:05 +00:00
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.
2025-09-02 12:10:10 +01:00
storeRoomAliasInCache ( theAlias , room . roomId , calculateRoomVia ( room ));
2015-12-10 16:26:36 +00:00
}
2016-01-08 03:22:38 +00:00
2021-12-06 09:25:05 +00:00
// 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.
2022-05-26 10:19:00 +01:00
localStorage ? . setItem ( "mx_last_room_id" , room . roomId );
2021-12-06 09:25:05 +00:00
}
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
2022-07-20 09:26:25 +02:00
let replaceLast = presentedId [ 0 ] === "#" && roomInfo . room_id === this . state . currentRoomId ;
if ( isLocalRoom ( this . state . currentRoomId )) {
// Replace local room history items
replaceLast = true ;
}
2021-12-06 09:25:05 +00:00
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 ,
2023-03-10 14:55:06 +00:00
currentRoomId : roomInfo.room_id ?? null ,
2021-12-06 09:25:05 +00:00
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 );
2017-04-06 11:30:47 +01:00
},
);
2020-04-22 11:55:03 +01:00
}
2015-12-10 16:26:36 +00:00
2023-01-12 13:25:14 +00:00
private viewSomethingBehindModal () : void {
2020-04-23 10:12:50 +01:00
if ( this . state . view !== Views . LOGGED_IN ) {
2020-04-22 13:08:33 +01:00
this . viewWelcome ();
2019-02-11 13:31:27 +00:00
return ;
}
2025-11-21 17:04:52 +00:00
if ( ! this . state . currentRoomId && ! this . state . currentUserId && ! this . isLoggedInViewPageDisplayed ()) {
2020-04-22 13:08:33 +01:00
this . viewHome ();
2019-02-11 13:31:27 +00:00
}
2020-04-22 11:55:03 +01:00
}
2019-02-11 13:31:27 +00:00
2023-01-12 13:25:14 +00:00
private viewWelcome () : void {
2021-03-12 15:01:05 -07:00
if ( shouldUseLoginForWelcome ( SdkConfig . get ())) {
return this . viewLogin ();
}
2019-02-07 16:25:09 +00:00
this . setStateForNewView ({
2020-04-23 10:12:50 +01:00
view : Views.WELCOME ,
2019-02-07 16:25:09 +00:00
});
this . notifyNewScreen ( "welcome" );
2020-04-22 11:55:03 +01:00
}
2019-02-07 16:25:09 +00:00
2023-01-12 13:25:14 +00:00
private viewLogin ( otherState? : any ) : void {
2021-03-12 15:01:05 -07:00
this . setStateForNewView ({
view : Views.LOGIN ,
... otherState ,
});
this . notifyNewScreen ( "login" );
}
2023-01-12 13:25:14 +00:00
private viewHome ( justRegistered = false ) : void {
2019-01-03 14:22:16 -07:00
// The home page requires the "logged in" view, so we'll set that.
2018-12-20 17:26:13 -07:00
this . setStateForNewView ({
2020-04-23 10:12:50 +01:00
view : Views.LOGGED_IN ,
2020-11-02 17:24:47 +00:00
justRegistered ,
2021-08-31 17:14:50 +01:00
currentRoomId : null ,
2018-12-20 17:26:13 -07:00
});
2021-09-27 12:40:34 +02:00
this . setPage ( PageType . HomePage );
2018-05-29 13:16:39 +01:00
this . notifyNewScreen ( "home" );
2020-04-22 11:55:03 +01:00
}
2018-05-29 13:16:39 +01:00
2023-01-12 13:25:14 +00:00
private viewUser ( userId : string , subAction : string ) : void {
2019-04-17 10:57:45 +02:00
// Wait for the first sync so that `getRoom` gives us a room object if it's
// in the sync response
2025-04-09 19:03:09 +00:00
this . firstSyncPromise . promise . then (() => {
2019-04-17 18:26:07 +02:00
if ( subAction === "chat" ) {
2020-04-22 13:08:33 +01:00
this . chatCreateOrReuse ( userId );
2019-04-17 10:57:45 +02:00
return ;
}
this . notifyNewScreen ( "user/" + userId );
2021-06-29 13:11:58 +01:00
this . setState ({ currentUserId : userId });
2021-09-27 12:40:34 +02:00
this . setPage ( PageType . UserView );
2019-04-17 10:57:45 +02:00
});
2020-04-22 11:55:03 +01:00
}
2019-04-17 10:57:45 +02:00
2025-05-28 19:48:32 +01:00
private viewShare ( format : ShareFormat , msg : string ) : void {
// Wait for the first sync so we can present possible rooms to share into
this . firstSyncPromise . promise . then (() => {
this . notifyNewScreen ( "share" );
let rawEvent ;
switch ( format ) {
case ShareFormat . Html : {
rawEvent = {
type : "m.room.message" ,
content : {
msgtype : MsgType.Text ,
body : getHtmlText ( msg ),
format : "org.matrix.custom.html" ,
formatted_body : sanitizeHtml ( msg , sanitizeHtmlParams ),
},
origin_server_ts : Date.now (),
};
break ;
}
case ShareFormat . Markdown : {
const html = new Markdown ( msg ). toHTML ({ externalLinks : true });
rawEvent = {
type : "m.room.message" ,
content : {
msgtype : MsgType.Text ,
body : msg ,
format : "org.matrix.custom.html" ,
formatted_body : html ,
},
origin_server_ts : Date.now (),
};
break ;
}
default :
rawEvent = {
type : "m.room.message" ,
content : {
msgtype : MsgType.Text ,
body : msg ,
},
origin_server_ts : Date.now (),
};
}
const event = new MatrixEvent ( rawEvent );
dis . dispatch < OpenForwardDialogPayload >({
action : Action.OpenForwardDialog ,
event : event ,
permalinkCreator : null ,
});
});
}
2023-01-12 13:25:14 +00:00
private async createRoom ( defaultPublic = false , defaultName? : string , type ?: RoomType ) : Promise < void > {
2022-06-14 17:51:51 +01:00
const modal = Modal . createDialog ( CreateRoomDialog , {
2022-04-01 10:36:10 -04:00
type ,
2021-05-19 19:20:58 +01:00
defaultPublic ,
defaultName ,
});
2019-08-09 16:47:22 +01:00
2020-01-23 13:54:28 +00:00
const [ shouldCreate , opts ] = await modal . finished ;
2019-08-09 16:47:22 +01:00
if ( shouldCreate ) {
2023-06-15 15:11:49 +01:00
createRoom ( MatrixClientPeg . safeGet (), opts ! );
2019-08-09 16:47:22 +01:00
}
2020-04-22 11:55:03 +01:00
}
2017-06-05 17:45:01 +01:00
2023-01-12 13:25:14 +00:00
private chatCreateOrReuse ( userId : string ) : void {
2017-06-02 11:42:47 +01:00
// Use a deferred action to reshow the dialog once the user has registered
2023-06-15 15:11:49 +01:00
if ( MatrixClientPeg . safeGet (). isGuest ()) {
2024-02-12 15:04:13 +00:00
dis . dispatch < DoAfterSyncPreparedPayload < ViewStartChatOrReusePayload >>({
action : Action.DoAfterSyncPrepared ,
deferred_action : {
action : Action.ViewStartChatOrReuse ,
user_id : userId ,
2020-02-28 10:47:23 +00:00
},
2017-06-01 14:16:25 +01:00
});
return ;
}
2019-08-07 12:13:59 -06:00
// TODO: Immutable DMs replaces this
2023-06-15 15:11:49 +01:00
const client = MatrixClientPeg . safeGet ();
2023-01-30 10:54:05 +01:00
const dmRoom = findDMForUser ( client , userId );
2019-08-07 12:13:59 -06:00
2023-01-30 10:54:05 +01:00
if ( dmRoom ) {
2022-02-10 14:29:55 +00:00
dis . dispatch < ViewRoomPayload >({
2021-11-25 17:49:43 -03:00
action : Action.ViewRoom ,
2023-01-30 10:54:05 +01:00
room_id : dmRoom.roomId ,
2022-02-17 18:03:27 +00:00
metricsTrigger : "MessageUser" ,
2019-08-07 12:13:59 -06:00
});
} else {
dis . dispatch ({
action : "start_chat" ,
user_id : userId ,
});
}
2020-04-22 11:55:03 +01:00
}
2017-06-01 14:16:25 +01:00
2023-01-12 13:25:14 +00:00
private leaveRoomWarnings ( roomId : string ) : JSX . Element [] {
2023-06-15 15:11:49 +01:00
const roomToLeave = MatrixClientPeg . safeGet (). getRoom ( roomId );
2022-03-22 17:07:37 -06:00
const isSpace = roomToLeave ? . isSpaceRoom ();
2018-01-19 22:27:48 +00:00
// Show a warning if there are additional complications.
2023-01-12 13:25:14 +00:00
const warnings : JSX.Element [] = [];
2020-11-10 15:04:01 -06:00
2023-03-10 14:55:06 +00:00
const memberCount = roomToLeave ? . currentState . getJoinedMemberCount ();
2020-11-10 15:04:01 -06:00
if ( memberCount === 1 ) {
2020-11-18 13:54:49 -06:00
warnings . push (
2024-03-06 23:34:14 +00:00
< strong className = "warning" key = "only_member_warning" >
2021-07-19 22:43:11 +01:00
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
2023-10-02 16:13:00 +05:30
{ _t ( "leave_room_dialog|last_person_warning" )}
2024-03-06 23:34:14 +00:00
</ strong >,
2020-11-18 13:54:49 -06:00
);
2020-11-10 15:04:01 -06:00
return warnings ;
}
2023-03-10 14:55:06 +00:00
const joinRules = roomToLeave ? . currentState . getStateEvents ( "m.room.join_rules" , "" );
2018-01-19 22:27:48 +00:00
if ( joinRules ) {
const rule = joinRules . getContent (). join_rule ;
if ( rule !== "public" ) {
warnings . push (
2024-03-06 23:34:14 +00:00
< strong className = "warning" key = "non_public_warning" >
2021-07-19 22:43:11 +01:00
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
2021-03-02 09:55:55 +00:00
{ isSpace
2023-10-02 16:13:00 +05:30
? _t ( "leave_room_dialog|space_rejoin_warning" )
: _t ( "leave_room_dialog|room_rejoin_warning" )}
2024-03-06 23:34:14 +00:00
</ strong >,
2018-01-19 22:27:48 +00:00
);
}
}
2022-10-19 12:51:31 +02:00
const client = MatrixClientPeg . get ();
2024-03-21 11:39:47 +00:00
if ( client && roomToLeave ) {
2025-08-05 12:10:30 +01:00
// If the user is the only user with highest power level
if ( isOnlyAdmin ( roomToLeave )) {
const userLevelValues = roomToLeave . getJoinedMembers (). map (( m ) => m . powerLevel );
2024-03-21 11:39:47 +00:00
const maxUserLevel = Math . max (...( userLevelValues as number []));
2025-08-05 12:10:30 +01:00
const warning =
maxUserLevel >= 100
? _t ( "leave_room_dialog|room_leave_admin_warning" )
: _t ( "leave_room_dialog|room_leave_mod_warning" );
warnings . push (
< strong className = "warning" key = "last_admin_warning" >
{ " " /* Whitespace, otherwise the sentences get smashed together */ }
{ warning }
</ strong >,
);
2022-10-19 12:51:31 +02:00
}
}
2018-01-19 22:27:48 +00:00
return warnings ;
2020-04-22 11:55:03 +01:00
}
2018-01-19 22:27:48 +00:00
2023-01-12 13:25:14 +00:00
private leaveRoom ( roomId : string ) : void {
2023-06-15 15:11:49 +01:00
const cli = MatrixClientPeg . safeGet ();
2023-05-23 16:24:12 +01:00
const roomToLeave = cli . getRoom ( roomId );
2020-04-22 13:08:33 +01:00
const warnings = this . leaveRoomWarnings ( roomId );
2018-01-19 22:27:48 +00:00
2022-03-22 17:07:37 -06:00
const isSpace = roomToLeave ? . isSpaceRoom ();
2025-04-30 16:56:21 +01:00
const { finished } = Modal . createDialog ( QuestionDialog , {
2023-10-03 19:17:26 +01:00
title : isSpace ? _t ( "space|leave_dialog_action" ) : _t ( "action|leave_room" ),
2017-06-02 10:47:08 +01:00
description : (
< span >
2021-03-02 09:55:55 +00:00
{ isSpace
2023-10-02 16:13:00 +05:30
? _t ( "leave_room_dialog|leave_space_question" , {
2023-09-04 19:41:39 +01:00
spaceName : roomToLeave?.name ?? _t ( "common|unnamed_space" ),
2021-06-29 13:42:19 +01:00
})
2023-10-02 16:13:00 +05:30
: _t ( "leave_room_dialog|leave_room_question" , {
2023-09-04 19:41:39 +01:00
roomName : roomToLeave?.name ?? _t ( "common|unnamed_room" ),
2023-03-10 14:55:06 +00:00
})}
2020-04-22 11:55:03 +01:00
{ warnings }
2017-06-02 10:47:08 +01:00
</ span >
),
2023-08-22 20:55:15 +01:00
button : _t ( "action|leave" ),
2024-03-21 11:54:02 +00:00
danger : warnings.length > 0 ,
2025-04-30 16:56:21 +01:00
});
2017-05-19 13:53:11 +01:00
2025-04-30 16:56:21 +01:00
finished . then ( async ([ shouldLeave ]) => {
if ( shouldLeave ) {
await leaveRoomBehaviour ( cli , roomId );
dis . dispatch < AfterLeaveRoomPayload >({
action : Action.AfterLeaveRoom ,
room_id : roomId ,
});
}
2017-05-19 13:53:11 +01:00
});
2020-04-22 11:55:03 +01:00
}
2016-08-10 00:03:29 +01:00
2023-01-12 13:25:14 +00:00
private forgetRoom ( roomId : string ) : void {
2023-06-15 15:11:49 +01:00
const room = MatrixClientPeg . safeGet (). getRoom ( roomId );
MatrixClientPeg . safeGet ()
2020-07-13 16:35:03 +01:00
. forget ( roomId )
. then (() => {
2020-12-03 11:15:55 +00:00
// Switch to home page if we're currently viewing the forgotten room
2020-07-13 16:35:03 +01:00
if ( this . state . currentRoomId === roomId ) {
2022-02-22 11:04:27 +01:00
dis . dispatch ({ action : Action.ViewHomePage });
2020-07-13 16:35:03 +01:00
}
2021-03-18 23:28:59 -06:00
2025-05-15 15:34:05 +05:30
if ( room ) {
// Legacy room list store needs to be told to manually remove this room
RoomListStore . instance . manualRoomUpdate ( room , RoomUpdateCause . RoomRemoved );
// New room list store will remove the room on the following dispatch
dis . dispatch < AfterForgetRoomPayload >({ action : Action.AfterForgetRoom , room });
}
2020-07-13 17:01:50 +01:00
})
. catch (( err ) => {
2023-10-03 19:17:26 +01:00
const errCode = err . errcode || _td ( "error|unknown_error_code" );
2022-06-14 17:51:51 +01:00
Modal . createDialog ( ErrorDialog , {
2023-10-02 16:13:00 +05:30
title : _t ( "error_dialog|forget_room_failed" , { errCode }),
2024-10-16 17:38:22 +01:00
description : err?.message ?? _t ( "invite|failed_generic" ),
2022-12-12 12:24:14 +01:00
});
2020-07-13 16:35:03 +01:00
});
}
2023-01-12 13:25:14 +00:00
private async copyRoom ( roomId : string ) : Promise < void > {
2023-06-15 15:11:49 +01:00
const roomLink = makeRoomPermalink ( MatrixClientPeg . safeGet (), roomId );
2021-07-14 14:58:18 +01:00
const success = await copyPlaintext ( roomLink );
if ( ! success ) {
2022-06-14 17:51:51 +01:00
Modal . createDialog ( ErrorDialog , {
2023-10-02 16:13:00 +05:30
title : _t ( "error_dialog|copy_room_link_failed|title" ),
description : _t ( "error_dialog|copy_room_link_failed|description" ),
2021-07-14 14:58:18 +01:00
});
}
}
2024-10-03 09:55:06 +01:00
/**
* Returns true if the user must go through the device verification process before they
* can use the app.
* @returns true if the user must verify
*/
private async shouldForceVerification () : Promise < boolean > {
if ( ! SdkConfig . get ( "force_verification" )) return false ;
const mustVerifyFlag = localStorage . getItem ( "must_verify_device" );
if ( ! mustVerifyFlag ) return false ;
const client = MatrixClientPeg . safeGet ();
if ( client . isGuest ()) return false ;
const crypto = client . getCrypto ();
const crossSigningReady = await crypto ? . isCrossSigningReady ();
return ! crossSigningReady ;
}
2016-08-02 19:05:05 +01:00
/**
2025-11-26 11:29:58 +00:00
* Show the first screen after the application is successfully loaded in a logged-in state.
2025-11-21 14:58:03 +00:00
*
* Called:
*
* - on {@link Action.OnLoggedIn}, but only when we don't expect a separate call to {@link postLoginSetup}.
* - from {@link postLoginSetup}, when we don't have crypto setup tasks to perform after the login.
* - by {@link onCompleteSecurityE2eSetupFinished}
*
* In other words, whenever we think we have completed the login and E2E setup tasks.
*/
2025-12-02 11:14:00 +00:00
private onShowPostLoginScreen () : void {
logger . debug ( "onShowPostLoginScreen: Transitioning to logged in view." );
2020-04-23 10:12:50 +01:00
this . setStateForNewView ({ view : Views.LOGGED_IN });
2020-01-18 23:23:46 +00:00
// 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.
2023-03-10 14:55:06 +00:00
if ( this . screenAfterLogin ? . screen ) {
2025-12-02 11:14:00 +00:00
logger . debug ( `onShowPostLoginScreen: showing screen ${ this . screenAfterLogin . screen } ` );
2020-04-22 13:08:33 +01:00
this . showScreen ( this . screenAfterLogin . screen , this . screenAfterLogin . params );
2023-03-10 14:55:06 +00:00
this . screenAfterLogin = undefined ;
2020-01-18 23:23:46 +00:00
} else if ( MatrixClientPeg . currentUserIsJustRegistered ()) {
2019-06-14 15:31:19 +01:00
MatrixClientPeg . setJustRegisteredUserId ( null );
2017-05-26 11:46:33 +01:00
2024-02-12 15:04:13 +00:00
if ( ThreepidInviteStore . instance . pickBestInvite ()) {
2020-09-11 20:20:33 -06:00
// The user has a 3pid invite pending - show them that
const threepidInvite = ThreepidInviteStore . instance . pickBestInvite ();
2025-12-02 11:14:00 +00:00
logger . debug ( `onShowPostLoginScreen: showing room ${ threepidInvite . roomId } after registration` );
2020-09-11 20:20:33 -06:00
// 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 );
2021-06-29 13:11:58 +01:00
this . showScreen ( `room/ ${ threepidInvite . roomId } ` , params );
2019-06-14 16:04:09 +01:00
} else {
// The user has just logged in after registering,
// so show the homepage.
2025-12-02 11:14:00 +00:00
logger . debug ( "onShowPostLoginScreen: Showing home page after registration" );
2022-02-22 11:04:27 +01:00
dis . dispatch < ViewHomePagePayload >({ action : Action.ViewHomePage , justRegistered : true });
2017-05-12 17:39:38 +01:00
}
2025-12-02 11:14:00 +00:00
} else {
logger . debug ( "onShowPostLoginScreen: showScreenAfterLogin" );
2020-04-22 13:08:33 +01:00
this . showScreenAfterLogin ();
2017-03-15 12:02:08 +00:00
}
2020-02-20 00:38:08 +00:00
2022-03-18 10:12:36 -06:00
if ( SdkConfig . get ( "mobile_guide_toast" )) {
2021-01-28 17:21:36 +00:00
// The toast contains further logic to detect mobile platforms,
// check if it has been dismissed before, etc.
showMobileGuideToast ();
}
2022-11-09 10:50:01 +00:00
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 >,
2024-07-30 13:57:15 +01:00
primaryLabel : _t ( "action|ok" ),
onPrimaryClick : () => {
2022-11-09 10:50:01 +00:00
ToastStore . sharedInstance (). dismissToast ( key );
localStorage . setItem ( key , "1" );
},
},
component : GenericToast ,
className : "mx_AnalyticsToast" ,
priority : 100 ,
});
}
}
2020-04-22 11:55:03 +01:00
}
2017-03-15 12:02:08 +00:00
2023-01-12 13:25:14 +00:00
private initPosthogAnalyticsToast () : void {
2021-12-06 09:39:33 +11:00
// Show the analytics toast if necessary
if ( SettingsStore . getValue ( "pseudonymousAnalyticsOptIn" ) === null ) {
2022-06-14 17:51:51 +01:00
showAnalyticsToast ();
2021-12-06 09:39:33 +11:00
}
// 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 ) {
2022-06-14 17:51:51 +01:00
showAnalyticsToast ();
2021-12-06 09:39:33 +11:00
} 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 ();
}
},
);
}
2023-01-12 13:25:14 +00:00
private showScreenAfterLogin () : void {
2017-03-08 10:25:54 +00:00
// 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
2020-04-22 13:08:33 +01:00
if ( this . screenAfterLogin && this . screenAfterLogin . screen ) {
2025-12-02 11:14:00 +00:00
logger . debug ( `showScreenAfterLogin: showing screen ${ this . screenAfterLogin . screen } ` );
2020-04-22 13:08:33 +01:00
this . showScreen ( this . screenAfterLogin . screen , this . screenAfterLogin . params );
2023-03-10 14:55:06 +00:00
this . screenAfterLogin = undefined ;
2017-04-06 11:30:47 +01:00
} else if ( localStorage && localStorage . getItem ( "mx_last_room_id" )) {
// Before defaulting to directory, show the last viewed room
2025-12-02 11:14:00 +00:00
logger . debug ( "showScreenAfterLogin: showing last room" );
2020-04-22 13:08:33 +01:00
this . viewLastRoom ();
2017-03-08 15:11:38 +00:00
} else {
2023-06-15 15:11:49 +01:00
if ( MatrixClientPeg . safeGet (). isGuest ()) {
2025-12-02 11:14:00 +00:00
logger . debug ( "showScreenAfterLogin: showing guest welcome page" );
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "view_welcome_page" });
2019-02-14 12:35:09 +00:00
} else {
2025-12-02 11:14:00 +00:00
logger . debug ( "showScreenAfterLogin: showing home page" );
2022-02-22 11:04:27 +01:00
dis . dispatch ({ action : Action.ViewHomePage });
2019-02-08 12:12:43 +00:00
}
2017-02-28 15:05:49 +00:00
}
2020-04-22 11:55:03 +01:00
}
2015-06-12 14:59:33 +01:00
2023-01-12 13:25:14 +00:00
private viewLastRoom () : void {
2022-02-10 14:29:55 +00:00
dis . dispatch < ViewRoomPayload >({
2021-11-25 17:49:43 -03:00
action : Action.ViewRoom ,
2023-03-10 14:55:06 +00:00
room_id : localStorage.getItem ( "mx_last_room_id" ) ?? undefined ,
2022-02-17 18:03:27 +00:00
metricsTrigger : undefined , // other
2019-07-08 12:43:16 -06:00
});
2020-04-22 11:55:03 +01:00
}
2019-07-08 12:43:16 -06:00
2025-11-26 11:29:58 +00:00
/**
* Handle an {@link Action.OnLoggedIn} action (i.e, we now have a client with working credentials).
*/
private onLoggedIn () : void {
this . stores . client = MatrixClientPeg . safeGet ();
StorageManager . tryPersistStorage ();
2025-12-02 11:14:00 +00:00
// If we're loading the app for the first time, we can now transition to a splash screen while we wait for the
// client to start. The exceptions are:
//
// - If there is a token login in flight: in that case we wait for the login to complete (which hits
// `postLoginSetup`).
//
// - Lifecycle emits an `Action.OnLoggedIn` event during startup even if the localstorage flag indicating a
// previous soft logout is set. In that situation we actually want to wait for the `Action.ClientNotViable`
// event, which will transition us into Views.SOFT_LOGOUT. We therefore have to check for !isSoftLogout().
// There will be a subsequent `Action.OnLoggedIn` event once the reauthentication completes.
//
// XXX: fix this properly by having Lifecycle not emit OnLoggedIn when it knows it is about to emit a
// ClientNotViable.
//
// If we're already in the SOFT_LOGOUT view, that means that reauthentication has succeeded, and we can
// transition to the splash screen.
2025-11-26 11:29:58 +00:00
if (
2025-12-02 11:14:00 +00:00
( this . state . view === Views . LOADING && ! Lifecycle . isSoftLogout () && ! this . tokenLogin ) ||
this . state . view === Views . SOFT_LOGOUT
2025-11-26 11:29:58 +00:00
) {
2025-12-02 11:14:00 +00:00
this . setStateForNewView ({ view : Views.PENDING_CLIENT_START });
2025-11-26 11:29:58 +00:00
}
}
2016-08-02 19:05:05 +01:00
/**
* Called when the session is logged out
*/
2023-01-12 13:25:14 +00:00
private onLoggedOut () : void {
2021-03-12 15:01:05 -07:00
this . viewLogin ({
2016-08-02 14:04:20 +01:00
ready : false ,
2017-08-25 12:10:13 +01:00
collapseLhs : false ,
2016-11-24 14:58:37 +00:00
currentRoomId : null ,
2016-08-02 14:04:20 +01:00
});
2019-09-23 22:01:49 +01:00
this . subTitleStatus = "" ;
2020-04-22 13:08:33 +01:00
this . setPageSubtitle ();
2023-03-27 10:07:43 +02:00
this . stores . onLoggedOut ();
2020-04-22 11:55:03 +01:00
}
2016-08-02 14:04:20 +01:00
2019-07-03 16:46:37 -06:00
/**
* Called when the session is softly logged out
*/
2023-01-12 13:25:14 +00:00
private onSoftLogout () : void {
2019-07-03 16:46:37 -06:00
this . notifyNewScreen ( "soft_logout" );
this . setStateForNewView ({
2020-04-23 10:12:50 +01:00
view : Views.SOFT_LOGOUT ,
2019-07-03 16:46:37 -06:00
ready : false ,
collapseLhs : false ,
currentRoomId : null ,
});
2019-09-23 22:01:49 +01:00
this . subTitleStatus = "" ;
2020-04-22 13:08:33 +01:00
this . setPageSubtitle ();
2020-04-22 11:55:03 +01:00
}
2019-07-03 16:46:37 -06:00
2016-08-02 19:05:05 +01:00
/**
* Called just before the matrix client is started
2016-08-03 09:53:02 +01:00
* (useful for setting listeners)
2016-08-02 19:05:05 +01:00
*/
2023-01-12 13:25:14 +00:00
private onWillStartClient () : void {
2025-04-09 19:03:09 +00:00
// Reset the 'have completed first sync' flag,
// since we're about to start the client and therefore about to do the first sync
// We resolve the existing promise with the new one to update any existing listeners
if ( ! this . firstSyncComplete ) {
2025-05-08 11:03:43 +01:00
const firstSyncPromise = Promise . withResolvers < void >();
2025-04-09 19:03:09 +00:00
this . firstSyncPromise . resolve ( firstSyncPromise . promise );
this . firstSyncPromise = firstSyncPromise ;
} else {
2025-05-08 11:03:43 +01:00
this . firstSyncPromise = Promise . withResolvers ();
2025-04-09 19:03:09 +00:00
}
2017-06-21 11:27:18 +01:00
this . firstSyncComplete = false ;
2023-06-15 15:11:49 +01:00
const cli = MatrixClientPeg . safeGet ();
2016-08-02 14:04:20 +01:00
2017-03-22 15:06:52 +00:00
// 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.
2020-08-03 16:02:26 +01:00
// https://github.com/vector-im/element-web/issues/3307#issuecomment-282895568
2020-04-22 13:08:33 +01:00
cli . setCanResetTimelineCallback (( roomId ) => {
2021-09-21 17:48:09 +02:00
logger . log ( "Request to reset timeline in room " , roomId , " viewing:" , this . state . currentRoomId );
2020-04-22 13:08:33 +01:00
if ( roomId !== this . state . currentRoomId ) {
2017-03-22 15:06:52 +00:00
// 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.
2020-05-07 10:02:40 +01:00
if ( ! this . loggedInView . current ) {
2017-03-22 15:06:52 +00:00
return true ;
}
2020-04-22 13:08:33 +01:00
return this . loggedInView . current . canResetTimelineInRoom ( roomId );
2017-03-22 15:06:52 +00:00
});
2023-08-10 09:01:14 +01:00
cli . on ( ClientEvent . Sync , ( state : SyncState , prevState : SyncState | null , data? : SyncStateData ) => {
2021-12-17 11:02:06 +00:00
if ( state === SyncState . Error || state === SyncState . Reconnecting ) {
2023-03-10 14:55:06 +00:00
this . setState ({ syncError : data?.error ?? null });
2020-04-22 13:08:33 +01:00
} else if ( this . state . syncError ) {
2021-06-29 13:11:58 +01:00
this . setState ({ syncError : null });
2018-09-07 12:18:25 +01:00
}
2021-12-17 11:02:06 +00:00
if ( state === SyncState . Syncing && prevState === SyncState . Syncing ) {
2024-08-07 09:35:57 +01:00
// We know we have performabed a live update and known rooms should be in a good state.
// Now is a good time to clean up drafts.
cleanUpDraftsIfRequired ();
2015-12-15 14:23:58 +00:00
return ;
}
2023-08-07 09:41:39 +01:00
logger . debug ( `MatrixClient sync state => ${ state } ` );
2021-12-17 11:02:06 +00:00
if ( state !== SyncState . Prepared ) {
return ;
}
2017-04-06 11:30:47 +01:00
2026-02-16 12:23:32 +01:00
// Re-apply theme now that account data (including custom_themes) is loaded, otherwise we might end up with the wrong theme applied if the user has custom themes enabled
setTheme ();
2020-04-22 13:08:33 +01:00
this . firstSyncComplete = true ;
this . firstSyncPromise . resolve ();
2015-09-25 17:22:42 +01:00
2020-11-02 17:25:48 +00:00
if ( Notifier . shouldShowPrompt () && ! MatrixClientPeg . userRegisteredWithinLastHours ( 24 )) {
showNotificationsToast ( false );
2020-05-28 19:03:42 +01:00
}
2021-07-08 17:36:31 +02:00
dis . fire ( Action . FocusSendMessageComposer );
2015-06-12 14:59:33 +01:00
});
2020-09-16 13:44:24 +01:00
2025-03-25 12:07:07 +00:00
cli . on ( HttpApiEvent . SessionLoggedOut , ( errObj ) => {
this . loadSessionAbortController . abort ( errObj );
this . loadSessionAbortController = new AbortController ();
2018-07-15 22:33:00 +01:00
if ( Lifecycle . isLoggingOut ()) return ;
2019-07-03 16:46:37 -06:00
2021-02-05 09:24:12 +00:00
// A modal might have been open when we were logged out by the server
2024-07-05 14:39:13 +01:00
Modal . forceCloseAllModals ();
2021-02-05 09:24:12 +00:00
2025-03-25 12:07:07 +00:00
if ( errObj . httpStatus === 401 && errObj . data ? .[ "soft_logout" ]) {
2021-10-15 16:31:29 +02:00
logger . warn ( "Soft logout issued by server - avoiding data deletion" );
2019-07-03 16:46:37 -06:00
Lifecycle . softLogout ();
return ;
}
2025-03-25 12:07:07 +00:00
dis . dispatch (
{
action : "logout" ,
},
true ,
);
// The above dispatch closes all modals, so open the modal after calling it synchronously
2022-06-14 17:51:51 +01:00
Modal . createDialog ( ErrorDialog , {
2023-09-21 18:16:04 +01:00
title : _t ( "auth|session_logged_out_title" ),
description : _t ( "auth|session_logged_out_description" ),
2016-03-15 10:48:16 +00:00
});
});
2022-02-22 12:18:08 +00:00
cli . on ( HttpApiEvent . NoConsent , function ( message , consentUri ) {
2025-04-30 16:56:21 +01:00
const { finished } = Modal . createDialog (
2022-06-14 17:51:51 +01:00
QuestionDialog ,
{
2023-09-21 18:16:04 +01:00
title : _t ( "terms|tac_title" ),
2018-05-22 15:47:31 +01:00
description : (
< div >
2023-09-21 18:16:04 +01:00
< p > { _t ( "terms|tac_description" , { homeserverDomain : cli.getDomain () })}</ p >
2018-05-22 15:47:31 +01:00
</ div >
),
2023-09-21 18:16:04 +01:00
button : _t ( "terms|tac_button" ),
2023-08-23 11:57:22 +01:00
cancelButton : _t ( "action|dismiss" ),
2018-05-22 15:47:31 +01:00
},
2023-03-10 14:55:06 +00:00
undefined ,
2018-05-22 15:47:31 +01:00
true ,
);
2025-04-30 16:56:21 +01:00
finished . then (([ confirmed ]) => {
if ( confirmed ) {
const wnd = window . open ( consentUri , "_blank" ) ! ;
wnd . opener = null ;
}
});
2018-05-22 15:47:31 +01:00
});
2024-05-20 10:53:50 -04:00
DecryptionFailureTracker . instance
. start ( cli )
. catch (( e ) => logger . error ( "Unable to start DecryptionFailureTracker" , e ));
2018-06-15 13:33:07 +01:00
2022-02-22 12:18:08 +00:00
cli . on ( ClientEvent . Room , ( room ) => {
2024-10-15 16:24:58 +02:00
if ( cli . getCrypto ()) {
2017-11-05 14:56:41 -07:00
const blacklistEnabled = SettingsStore . getValueAt (
SettingLevel . ROOM_DEVICE ,
"blacklistUnverifiedDevices" ,
room . roomId ,
2017-11-08 17:41:32 -07:00
/*explicit=*/ true ,
2017-11-05 14:56:41 -07:00
);
room . setBlacklistUnverifiedDevices ( blacklistEnabled );
}
2017-11-04 20:13:23 -07:00
});
2023-01-12 13:25:14 +00:00
cli . on ( CryptoEvent . KeyBackupFailed , async ( errcode ) : Promise < void > => {
2023-06-15 15:11:49 +01:00
let haveNewVersion : boolean | undefined ;
2024-07-04 16:50:07 +01:00
let newVersionInfo : KeyBackupInfo | null = null ;
2024-10-18 11:45:45 +02:00
const keyBackupEnabled = Boolean (
cli . getCrypto () && ( await cli . getCrypto () ? . getActiveSessionBackupVersion ()) !== null ,
);
2019-01-11 13:15:09 +00:00
// if key backup is still enabled, there must be a new backup in place
2024-10-18 11:45:45 +02:00
if ( keyBackupEnabled ) {
2019-01-11 13:15:09 +00:00
haveNewVersion = true ;
} else {
// otherwise check the server to see if there's a new one
try {
2024-11-25 10:30:42 +01:00
newVersionInfo = ( await cli . getCrypto () ? . getKeyBackupInfo ()) ?? null ;
2019-01-11 13:15:09 +00:00
if ( newVersionInfo !== null ) haveNewVersion = true ;
} catch ( e ) {
2021-10-15 16:30:53 +02:00
logger . error ( "Saw key backup error but failed to check backup version!" , e );
2019-01-10 15:13:44 -06:00
return ;
2019-01-11 13:15:09 +00:00
}
}
if ( haveNewVersion ) {
2024-11-12 21:19:11 +00:00
Modal . createDialog (
lazy (() => import ( "../../async-components/views/dialogs/security/NewRecoveryMethodDialog" )),
2019-01-11 13:15:09 +00:00
);
} else {
2024-11-12 21:19:11 +00:00
Modal . createDialog (
lazy (() => import ( "../../async-components/views/dialogs/security/RecoveryMethodRemovedDialog" )),
2019-01-11 13:15:09 +00:00
);
2019-01-10 15:13:44 -06:00
}
2018-12-13 15:55:48 +00:00
});
2018-10-15 14:41:00 -06:00
2023-06-28 11:11:18 +01:00
cli . on ( CryptoEvent . VerificationRequestReceived , ( request ) => {
2020-03-06 17:22:56 +01:00
if ( request . verifier ) {
2022-06-14 17:51:51 +01:00
Modal . createDialog (
IncomingSasDialog ,
{
2020-03-06 17:22:56 +01:00
verifier : request.verifier ,
2020-03-06 17:20:08 +01:00
},
2023-03-10 14:55:06 +00:00
undefined ,
2020-03-06 17:20:08 +01:00
/* priority = */ false ,
/* static = */ true ,
);
} else if ( request . pending ) {
2020-03-06 16:50:39 +01:00
ToastStore . sharedInstance (). addOrReplaceToast ({
2023-06-07 15:43:44 +01:00
key : "verifreq_" + request . transactionId ,
2023-09-21 18:16:04 +01:00
title : _t ( "encryption|verification_requested_toast_title" ),
2025-12-15 12:00:35 +00:00
icon : < LockSolidIcon color = "var(--cpd-color-text-primary)" />,
2021-06-29 13:11:58 +01:00
props : { request },
2021-07-03 12:24:07 +02:00
component : VerificationRequestToast ,
2020-05-22 12:57:48 +01:00
priority : 90 ,
2020-03-06 16:50:39 +01:00
});
}
});
2020-04-22 11:55:03 +01:00
}
2017-11-04 20:13:23 -07:00
/**
* Called shortly after the matrix client has started. Useful for
* setting up anything that requires the client to be started.
* @private
*/
2024-10-03 09:55:06 +01:00
private async onClientStarted () : Promise < void > {
2023-06-15 15:11:49 +01:00
const cli = MatrixClientPeg . safeGet ();
2017-11-04 20:13:23 -07:00
2024-10-03 09:55:06 +01:00
const shouldForceVerification = await this . shouldForceVerification ();
2024-10-21 13:53:39 +02:00
const crypto = cli . getCrypto ();
if ( crypto ) {
2026-02-10 15:26:57 +00:00
crypto . globalBlacklistUnverifiedDevices = SettingsStore . getValueAt (
SettingLevel . DEVICE ,
"blacklistUnverifiedDevices" ,
);
SettingsStore . watchSetting (
"blacklistUnverifiedDevices" ,
null ,
( _settingName , _roomId , atLevel , blacklistEnabled : boolean ) => {
if ( atLevel != SettingLevel . DEVICE ) {
return ;
}
crypto . globalBlacklistUnverifiedDevices = blacklistEnabled ;
},
);
2017-11-05 14:56:41 -07:00
}
2022-07-29 15:03:25 +01:00
// 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 ();
}
2024-10-03 09:55:06 +01:00
this . setState ({
ready : true ,
});
2025-12-02 11:14:00 +00:00
// If the view is PENDING_CLIENT_START, that means we recovered the session from localstorage, or from
// soft-logout: we can now transition to the logged-in view.
//
// If the view is something else, that probably means it's a login or registration view; we handle that in
// `postLoginSetup`.
if ( this . state . view === Views . PENDING_CLIENT_START ) {
if ( shouldForceVerification ) {
this . setStateForNewView ({ view : Views.COMPLETE_SECURITY });
} else {
this . onShowPostLoginScreen ();
}
}
2020-04-22 11:55:03 +01:00
}
2015-07-15 19:25:36 +01:00
2023-01-12 13:25:14 +00:00
public showScreen ( screen : string , params ?: { [ key : string ] : any }) : void {
2025-12-02 11:14:00 +00:00
logger . debug ( `showScreen ${ screen } ` );
2020-11-16 15:56:54 +00:00
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
2025-12-02 11:14:00 +00:00
logger . info (
`showScreen: suppressing change to AuthScreen ${ screen } for logged-in user, and going to home screen instead` ,
);
2022-02-22 11:04:27 +01:00
dis . dispatch ({ action : Action.ViewHomePage });
2020-11-16 15:56:54 +00:00
return ;
}
2020-04-27 23:59:07 +01:00
if ( screen === "register" ) {
2015-07-15 20:33:12 +01:00
dis . dispatch ({
action : "start_registration" ,
2017-05-19 13:53:11 +01:00
params : params ,
2015-07-15 20:33:12 +01:00
});
2021-05-19 10:07:02 +01:00
PerformanceMonitor . instance . start ( PerformanceEntryNames . REGISTER );
2024-09-13 12:12:04 +01:00
} else if ( screen === "mobile_register" ) {
dis . dispatch ({
action : "start_mobile_registration" ,
params : params ,
});
2020-04-27 23:59:07 +01:00
} else if ( screen === "login" ) {
2015-07-15 20:33:12 +01:00
dis . dispatch ({
action : "start_login" ,
2017-05-19 13:53:11 +01:00
params : params ,
2015-07-15 20:33:12 +01:00
});
2021-05-19 10:07:02 +01:00
PerformanceMonitor . instance . start ( PerformanceEntryNames . LOGIN );
2020-04-27 23:59:07 +01:00
} else if ( screen === "forgot_password" ) {
2016-01-12 17:20:16 +00:00
dis . dispatch ({
action : "start_password_recovery" ,
2017-05-19 13:53:11 +01:00
params : params ,
2016-01-12 17:20:16 +00:00
});
2019-07-08 12:43:16 -06:00
} else if ( screen === "soft_logout" ) {
2023-06-15 15:11:49 +01:00
if ( !! cli ? . getUserId () && ! Lifecycle . isSoftLogout ()) {
2019-07-08 12:43:16 -06:00
// Logged in - visit a room
2020-04-27 23:59:07 +01:00
this . viewLastRoom ();
2019-07-08 12:43:16 -06:00
} else {
// Ultimately triggers soft_logout if needed
dis . dispatch ({
action : "start_login" ,
params : params ,
});
}
2020-04-27 23:59:07 +01:00
} else if ( screen === "new" ) {
2015-11-11 02:01:48 +01:00
dis . dispatch ({
2025-02-19 16:11:43 +01:00
action : Action.CreateRoom ,
2015-11-11 02:01:48 +01:00
});
2022-02-18 09:43:19 +01:00
} else if ( screen === "dm" ) {
dis . dispatch ({
2025-02-19 16:11:43 +01:00
action : Action.CreateChat ,
2022-02-18 09:43:19 +01:00
});
2025-05-28 19:48:32 +01:00
} else if ( screen === "share" ) {
if ( params && params [ "msg" ] !== undefined ) {
dis . dispatch < SharePayload >({
action : Action.Share ,
msg : params [ "msg" ],
format : params [ "format" ],
});
}
// if we weren't already coming at this from an existing screen
// and we're logged in, then explicitly default to home.
// if we're not logged in, then the login flow will do the right thing.
2025-11-21 17:04:52 +00:00
if ( ! this . state . currentRoomId && ! this . state . currentUserId && ! this . isLoggedInViewPageDisplayed ()) {
2025-05-28 19:48:32 +01:00
this . viewHome ();
}
2020-04-27 23:59:07 +01:00
} else if ( screen === "settings" ) {
2020-05-13 21:07:19 -06:00
dis . fire ( Action . ViewUserSettings );
2020-04-27 23:59:07 +01:00
} else if ( screen === "welcome" ) {
2019-02-07 16:25:09 +00:00
dis . dispatch ({
action : "view_welcome_page" ,
});
2020-04-27 23:59:07 +01:00
} else if ( screen === "home" ) {
2016-11-13 14:10:46 +00:00
dis . dispatch ({
2022-02-22 11:04:27 +01:00
action : Action.ViewHomePage ,
2016-11-13 14:10:46 +00:00
});
2020-04-27 23:59:07 +01:00
} else if ( screen === "directory" ) {
2020-06-08 20:33:21 -06:00
dis . fire ( Action . ViewRoomDirectory );
2020-06-25 22:00:22 +01:00
} else if ( screen === "start_sso" || screen === "start_cas" ) {
let cli = MatrixClientPeg . get ();
if ( ! cli ) {
2023-04-28 11:16:07 +01:00
const { hsUrl , isUrl } = this . getServerProperties (). serverConfig ;
2021-03-18 20:50:34 -06:00
cli = createClient ({
2020-06-25 22:00:22 +01:00
baseUrl : hsUrl ,
idBaseUrl : isUrl ,
});
}
const type = screen === "start_sso" ? "sso" : "cas" ;
2023-03-10 14:55:06 +00:00
PlatformPeg . get () ? . startSingleSignOn ( cli , type , this . getFragmentAfterLogin ());
2020-04-27 23:59:07 +01:00
} else if ( screen . indexOf ( "room/" ) === 0 ) {
2019-12-24 15:06:50 +01:00
// 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 );
2023-03-10 14:55:06 +00:00
let eventId : string | undefined = room . substring ( eventOffset + 1 ); // empty string if no event id given
2019-03-25 23:02:25 -06:00
// 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 ;
2020-08-03 16:02:26 +01:00
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
2016-03-01 18:23:57 +00:00
2023-03-10 14:55:06 +00:00
let threepidInvite : IThreepidInvite | undefined ;
2021-02-05 17:18:13 +00:00
// if we landed here from a 3PID invite, persist it
2023-03-10 14:55:06 +00:00
if ( params ? . signurl && params ? . email ) {
2020-09-11 19:49:48 -06:00
threepidInvite = ThreepidInviteStore . instance . storeInvite (
roomString ,
params as IThreepidInviteWireFormat ,
);
}
2021-02-05 17:18:13 +00:00
// 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 );
}
2016-03-01 18:23:57 +00:00
2018-10-19 13:30:38 -06:00
// 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.
2023-03-10 14:55:06 +00:00
let via : string [] = [];
if ( params ? . via ) {
2018-10-19 13:30:38 -06:00
if ( typeof params . via === "string" ) via = [ params . via ];
else via = params . via ;
}
2022-02-10 14:29:55 +00:00
const payload : ViewRoomPayload = {
2021-11-25 17:49:43 -03:00
action : Action.ViewRoom ,
2016-06-10 15:12:42 +01:00
event_id : eventId ,
2018-10-19 13:30:38 -06:00
via_servers : via ,
2017-06-08 17:57:37 +01:00
// 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.
2017-06-08 14:17:49 +01:00
highlighted : Boolean ( eventId ),
2020-09-11 19:49:48 -06:00
threepid_invite : threepidInvite ,
2020-09-11 19:55:15 -06:00
// 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
2020-09-11 19:49:48 -06:00
oob_data : {
name : threepidInvite?.roomName ,
avatarUrl : threepidInvite?.roomAvatarUrl ,
inviterName : threepidInvite?.inviterName ,
},
2020-04-27 23:59:07 +01:00
room_alias : undefined ,
room_id : undefined ,
2022-02-17 18:03:27 +00:00
metricsTrigger : undefined , // unknown or external trigger
2016-06-10 15:12:42 +01:00
};
2020-04-27 23:59:07 +01:00
if ( roomString [ 0 ] === "#" ) {
2016-06-10 15:12:42 +01:00
payload . room_alias = roomString ;
2015-09-18 18:39:16 +01:00
} else {
2016-06-10 15:12:42 +01:00
payload . room_id = roomString ;
2016-06-09 18:49:06 +01:00
}
2019-02-11 13:49:18 +00:00
dis . dispatch ( payload );
2020-04-27 23:59:07 +01:00
} else if ( screen . indexOf ( "user/" ) === 0 ) {
2017-05-19 13:53:11 +01:00
const userId = screen . substring ( 5 );
2019-04-17 18:26:07 +02:00
dis . dispatch ({
action : "view_user_info" ,
userId : userId ,
2023-03-10 14:55:06 +00:00
subAction : params?.action ,
2019-04-17 18:26:07 +02:00
});
2025-11-05 07:24:26 +00:00
} else if ( ModuleApi . instance . navigation . locationRenderers . get ( screen )) {
this . setState ({ page_type : screen });
2015-11-20 14:26:49 +00:00
}
2020-04-22 11:55:03 +01:00
}
2015-07-15 19:25:36 +01:00
2023-01-12 13:25:14 +00:00
private notifyNewScreen ( screen : string , replaceLast = false ) : void {
2015-07-15 19:25:36 +01:00
if ( this . props . onNewScreen ) {
2020-09-09 16:52:46 +01:00
this . props . onNewScreen ( screen , replaceLast );
2015-07-15 19:25:36 +01:00
}
2020-04-22 13:08:33 +01:00
this . setPageSubtitle ();
2020-04-22 11:55:03 +01:00
}
2021-12-17 11:02:06 +00:00
2023-05-12 09:49:37 +01:00
private onLogoutClick ( event : ButtonEvent ) : void {
2015-11-30 18:11:04 +00:00
dis . dispatch ({
2017-05-19 13:53:11 +01:00
action : "logout" ,
2015-11-30 18:11:04 +00:00
});
event . stopPropagation ();
event . preventDefault ();
2020-04-22 11:55:03 +01:00
}
2015-11-30 18:11:04 +00:00
2023-01-12 13:25:14 +00:00
private handleResize = () : void => {
2021-05-25 09:50:09 +01:00
const LHS_THRESHOLD = 1000 ;
2021-05-25 10:25:36 +01:00
const width = UIStore . instance . windowWidth ;
2015-11-30 18:11:04 +00:00
2021-05-28 10:03:46 +01:00
if ( this . prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD ) {
2015-11-30 18:11:04 +00:00
dis . dispatch ({ action : "show_left_panel" });
}
2021-05-28 10:03:46 +01:00
if ( this . prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD ) {
dis . dispatch ({ action : "hide_left_panel" });
}
this . prevWindowWidth = width ;
2025-10-06 10:23:06 +01:00
this . stores . resizeNotifier . notifyWindowResized ();
2020-04-22 11:55:03 +01:00
};
2015-11-30 18:11:04 +00:00
2023-01-12 13:25:14 +00:00
private dispatchTimelineResize () : void {
2019-03-29 14:32:26 +01:00
dis . dispatch ({ action : "timeline_resize" });
2020-04-22 11:55:03 +01:00
}
2019-03-12 16:37:20 +01:00
2023-01-12 13:25:14 +00:00
private onRegisterClick = () : void => {
2015-11-30 18:11:04 +00:00
this . showScreen ( "register" );
2020-04-22 11:55:03 +01:00
};
2015-11-30 18:11:04 +00:00
2023-01-12 13:25:14 +00:00
private onLoginClick = () : void => {
2015-11-30 18:11:04 +00:00
this . showScreen ( "login" );
2020-04-22 11:55:03 +01:00
};
2015-11-30 18:11:04 +00:00
2023-01-12 13:25:14 +00:00
private onForgotPasswordClick = () : void => {
2016-01-12 17:20:16 +00:00
this . showScreen ( "forgot_password" );
2020-04-22 11:55:03 +01:00
};
2016-01-12 17:20:16 +00:00
2024-12-19 11:55:05 +00:00
private onRegisterFlowComplete = ( credentials : IMatrixClientCreds ) : Promise < void > => {
return this . onUserCompletedLoginFlow ( credentials );
2020-04-22 11:55:03 +01:00
};
2020-01-24 19:11:57 +00:00
2017-06-19 10:22:18 +01:00
// returns a promise which resolves to the new MatrixClient
2021-12-17 11:02:06 +00:00
private onRegistered ( credentials : IMatrixClientCreds ) : Promise < MatrixClient > {
2017-06-19 10:22:18 +01:00
return Lifecycle . setLoggedIn ( credentials );
2020-04-22 11:55:03 +01:00
}
2017-02-13 14:36:03 +00:00
2021-12-17 11:02:06 +00:00
private onSendEvent ( roomId : string , event : MatrixEvent ) : void {
2017-06-16 16:12:52 +01:00
const cli = MatrixClientPeg . get ();
2021-09-14 17:49:18 +02:00
if ( ! cli ) return ;
2015-12-21 12:55:13 +00:00
2024-03-25 12:48:48 +00:00
cli . sendEvent ( roomId , event . getType () as keyof TimelineEvents , event . getContent ()). then (() => {
2021-06-29 13:11:58 +01:00
dis . dispatch ({ action : "message_sent" });
2017-06-16 16:12:52 +01:00
});
2020-04-22 11:55:03 +01:00
}
2017-06-16 16:12:52 +01:00
2023-01-12 13:25:14 +00:00
private setPageSubtitle ( subtitle = "" ) : void {
2019-09-23 22:01:49 +01:00
if ( this . state . currentRoomId ) {
2019-09-24 12:53:33 +02:00
const client = MatrixClientPeg . get ();
2023-05-10 08:41:55 +01:00
const room = client ? . getRoom ( this . state . currentRoomId );
2019-09-23 22:01:49 +01:00
if ( room ) {
2019-11-12 15:12:54 -07:00
subtitle = ` ${ this . subTitleStatus } | ${ room . name } ${ subtitle } ` ;
2019-09-23 22:01:49 +01:00
}
2019-11-12 15:12:54 -07:00
} else {
subtitle = ` ${ this . subTitleStatus } ${ subtitle } ` ;
2019-09-23 22:01:49 +01:00
}
2020-09-27 21:20:41 +01:00
2020-09-27 21:06:02 +01:00
const title = ` ${ SdkConfig . get (). brand } ${ subtitle } ` ;
if ( document . title !== title ) {
document . title = title ;
}
2020-04-22 11:55:03 +01:00
}
2017-06-20 15:31:12 +01:00
2021-12-17 11:02:06 +00:00
private onUpdateStatusIndicator = ( notificationState : SummarizedNotificationState , state : SyncState ) : void => {
2020-07-21 20:24:16 -06:00
const numUnreadRooms = notificationState . numUnreadStates ; // we know that states === rooms here
2016-03-16 06:32:49 +05:30
2016-11-02 15:10:21 +00:00
if ( PlatformPeg . get ()) {
2023-03-10 14:55:06 +00:00
PlatformPeg . get () ! . setErrorStatus ( state === SyncState . Error );
PlatformPeg . get () ! . setNotificationCount ( numUnreadRooms );
2016-01-27 15:19:25 +00:00
}
2016-11-02 15:10:21 +00:00
2019-09-23 22:01:49 +01:00
this . subTitleStatus = "" ;
2021-12-17 11:02:06 +00:00
if ( state === SyncState . Error ) {
2023-08-22 18:47:33 +01:00
this . subTitleStatus += `[ ${ _t ( "common|offline" ) } ] ` ;
2017-06-07 11:40:46 +01:00
}
2020-07-21 20:24:16 -06:00
if ( numUnreadRooms > 0 ) {
this . subTitleStatus += `[ ${ numUnreadRooms } ]` ;
2024-01-15 15:25:48 +00:00
} else if ( notificationState . level >= NotificationLevel . Activity ) {
2023-02-05 21:31:16 -05:00
this . subTitleStatus += `*` ;
2017-06-07 11:40:46 +01:00
}
2020-04-22 13:08:33 +01:00
this . setPageSubtitle ();
2021-12-17 11:02:06 +00:00
};
2015-12-21 12:55:13 +00:00
2023-01-12 13:25:14 +00:00
private onServerConfigChange = ( serverConfig : ValidatedServerConfig ) : void => {
2021-06-29 13:11:58 +01:00
this . setState ({ serverConfig });
2020-04-22 11:55:03 +01:00
};
2018-12-04 23:34:57 -07:00
2020-09-15 15:25:50 +01:00
/**
* 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`.
*/
2024-12-19 11:55:05 +00:00
private onUserCompletedLoginFlow = async ( credentials : IMatrixClientCreds ) : Promise < void > => {
2020-06-05 17:52:09 +01:00
// Create and start the client
await Lifecycle . setLoggedIn ( credentials );
2021-01-27 12:50:12 +00:00
await this . postLoginSetup ();
2021-06-15 10:24:07 +01:00
2021-05-19 10:07:02 +01:00
PerformanceMonitor . instance . stop ( PerformanceEntryNames . LOGIN );
PerformanceMonitor . instance . stop ( PerformanceEntryNames . REGISTER );
2020-04-22 11:55:03 +01:00
};
2020-01-15 21:13:56 +00:00
2025-11-21 14:58:03 +00:00
/** Called when {@link Views.E2E_SETUP} or {@link Views.COMPLETE_SECURITY} have completed. */
2025-03-20 15:10:08 +00:00
private onCompleteSecurityE2eSetupFinished = async () : Promise < void > => {
const forceVerify = await this . shouldForceVerification ();
if ( forceVerify ) {
const isVerified = await MatrixClientPeg . safeGet (). getCrypto () ? . isCrossSigningReady ();
if ( ! isVerified ) {
// We must verify but we haven't yet verified - don't continue logging in
return ;
}
}
2025-12-02 11:14:00 +00:00
this . onShowPostLoginScreen ();
2020-04-22 11:55:03 +01:00
};
2020-01-15 21:13:56 +00:00
2021-12-17 11:02:06 +00:00
private getFragmentAfterLogin () : string {
2020-05-13 06:24:04 +01:00
let fragmentAfterLogin = "" ;
2020-06-29 15:40:20 +01:00
const initialScreenAfterLogin = this . props . initialScreenAfterLogin ;
if (
initialScreenAfterLogin &&
2020-08-03 16:02:26 +01:00
// XXX: workaround for https://github.com/vector-im/element-web/issues/11643 causing a login-loop
2020-06-29 15:40:20 +01:00
! [ "welcome" , "login" , "register" , "start_sso" , "start_cas" ]. includes ( initialScreenAfterLogin . screen )
2020-06-02 16:26:07 +01:00
) {
2020-06-29 15:40:20 +01:00
fragmentAfterLogin = `/ ${ initialScreenAfterLogin . screen } ` ;
2020-05-13 06:24:04 +01:00
}
2020-06-25 22:00:22 +01:00
return fragmentAfterLogin ;
}
2020-05-13 06:24:04 +01:00
2023-02-13 17:01:43 +00:00
public render () : React . ReactNode {
2020-06-25 22:00:22 +01:00
const fragmentAfterLogin = this . getFragmentAfterLogin ();
2023-03-07 10:45:55 +00:00
let view : JSX.Element ;
2019-10-02 16:26:23 +01:00
2020-04-23 10:12:50 +01:00
if ( this . state . view === Views . LOADING ) {
2019-10-02 16:26:23 +01:00
view = (
2016-08-10 11:33:58 +01:00
< div className = "mx_MatrixChat_splash" >
< Spinner />
</ div >
2016-08-10 00:03:29 +01:00
);
2023-08-24 09:28:43 +01:00
} else if ( this . state . view === Views . CONFIRM_LOCK_THEFT ) {
view = (
< ConfirmSessionLockTheftView
onConfirm = {() => {
this . setState ({ view : Views.LOADING });
this . startInitSession ();
}}
/>
);
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . COMPLETE_SECURITY ) {
2020-01-24 19:11:57 +00:00
view = < CompleteSecurity onFinished = { this . onCompleteSecurityE2eSetupFinished } />;
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . E2E_SETUP ) {
2025-11-20 18:17:51 +00:00
view = < E2eSetup onCancelled = { this . onCompleteSecurityE2eSetupFinished } />;
2025-12-02 11:14:00 +00:00
} else if ( this . state . view === Views . PENDING_CLIENT_START ) {
// we think we are logged in, but are still waiting for the /sync to complete
view = (
< LoginSplashView
matrixClient = { MatrixClientPeg . safeGet ()}
onLogoutClick = { this . onLogoutClick }
syncError = { this . state . syncError }
/>
);
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . LOGGED_IN ) {
2017-06-15 17:36:57 +01:00
// `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.
2024-03-22 14:26:08 +00:00
if ( this . state . ready && this . state . page_type ) {
2017-06-15 17:36:57 +01:00
/* 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.
*/
2019-10-02 16:26:23 +01:00
view = (
2020-04-23 10:12:50 +01:00
< LoggedInView
{ ...this.props }
{ ...this.state }
ref = { this . loggedInView }
2023-06-15 15:11:49 +01:00
matrixClient = { MatrixClientPeg . safeGet ()}
2017-06-15 17:36:57 +01:00
onRegistered = { this . onRegistered }
currentRoomId = { this . state . currentRoomId }
/>
);
} else {
// we think we are logged in, but are still waiting for the /sync to complete
2019-10-02 16:26:23 +01:00
view = (
2024-01-04 18:12:08 +00:00
< LoginSplashView
2024-01-17 07:14:49 +00:00
matrixClient = { MatrixClientPeg . safeGet ()}
2024-01-04 18:12:08 +00:00
onLogoutClick = { this . onLogoutClick }
2024-03-22 14:26:08 +00:00
syncError = { this . state . syncError }
2024-01-04 18:12:08 +00:00
/>
2017-06-15 17:36:57 +01:00
);
}
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . WELCOME ) {
2020-06-25 22:02:39 +01:00
view = < Welcome />;
2020-09-17 11:55:10 +01:00
} else if ( this . state . view === Views . REGISTER && SettingsStore . getValue ( UIFeature . Registration )) {
2020-09-11 20:20:33 -06:00
const email = ThreepidInviteStore . instance . pickBestInvite () ? . toEmail ;
2019-10-02 16:26:23 +01:00
view = (
2015-11-30 18:11:04 +00:00
< Registration
clientSecret = { this . state . register_client_secret }
sessionId = { this . state . register_session_id }
idSid = { this . state . register_id_sid }
2020-09-11 20:20:33 -06:00
email = { email }
2016-06-02 11:50:00 +01:00
brand = { this . props . config . brand }
2020-01-24 19:11:57 +00:00
onLoggedIn = { this . onRegisterFlowComplete }
2016-03-06 14:33:36 -05:00
onLoginClick = { this . onLoginClick }
2018-02-07 15:51:03 +00:00
onServerConfigChange = { this . onServerConfigChange }
2020-01-30 11:27:54 +00:00
defaultDeviceDisplayName = { this . props . defaultDeviceDisplayName }
2020-11-24 12:09:11 +00:00
fragmentAfterLogin = { fragmentAfterLogin }
2024-09-13 12:12:04 +01:00
mobileRegister = { this . state . isMobileRegistration }
2019-06-12 18:11:58 +01:00
{ ...this.getServerProperties () }
2019-05-02 23:09:07 -06:00
/>
2015-11-30 18:11:04 +00:00
);
2020-09-17 11:55:10 +01:00
} else if ( this . state . view === Views . FORGOT_PASSWORD && SettingsStore . getValue ( UIFeature . PasswordReset )) {
2019-10-02 16:26:23 +01:00
view = (
2016-01-12 17:20:16 +00:00
< ForgotPassword
2016-03-06 14:33:36 -05:00
onComplete = { this . onLoginClick }
2019-05-02 23:09:07 -06:00
onLoginClick = { this . onLoginClick }
{ ...this.getServerProperties () }
/>
2016-01-12 17:20:16 +00:00
);
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . LOGIN ) {
2020-09-17 11:55:10 +01:00
const showPasswordReset = SettingsStore . getValue ( UIFeature . PasswordReset );
2019-10-02 16:26:23 +01:00
view = (
2015-11-30 18:11:04 +00:00
< Login
2020-04-15 12:21:49 +01:00
isSyncing = { this . state . pendingInitialSync }
2020-01-15 21:13:56 +00:00
onLoggedIn = { this . onUserCompletedLoginFlow }
2015-11-30 18:11:04 +00:00
onRegisterClick = { this . onRegisterClick }
2016-05-27 14:57:43 +01:00
fallbackHsUrl = { this . getFallbackHsUrl ()}
2016-08-12 11:41:06 +01:00
defaultDeviceDisplayName = { this . props . defaultDeviceDisplayName }
2020-09-17 11:55:10 +01:00
onForgotPasswordClick = { showPasswordReset ? this . onForgotPasswordClick : undefined }
2018-02-07 15:51:03 +00:00
onServerConfigChange = { this . onServerConfigChange }
2020-05-13 06:24:04 +01:00
fragmentAfterLogin = { fragmentAfterLogin }
2023-04-20 09:49:10 +01:00
defaultUsername = { this . props . startingFragmentQueryParams ? . defaultUsername as string | undefined }
2019-05-02 23:09:07 -06:00
{ ...this.getServerProperties () }
2016-09-01 11:08:40 +01:00
/>
2015-11-30 18:11:04 +00:00
);
2020-04-23 10:12:50 +01:00
} else if ( this . state . view === Views . SOFT_LOGOUT ) {
2019-10-02 16:26:23 +01:00
view = (
2019-07-08 12:43:16 -06:00
< SoftLogout
realQueryParams = { this . props . realQueryParams }
onTokenLoginCompleted = { this . props . onTokenLoginCompleted }
2020-05-13 06:24:04 +01:00
fragmentAfterLogin = { fragmentAfterLogin }
2019-07-08 12:43:16 -06:00
/>
2019-07-03 16:46:37 -06:00
);
2023-08-24 09:28:43 +01:00
} else if ( this . state . view === Views . LOCK_STOLEN ) {
view = < SessionLockStolenView />;
2019-10-02 16:26:23 +01:00
} else {
2021-10-15 16:30:53 +02:00
logger . error ( `Unknown view ${ this . state . view } ` );
2023-03-07 10:45:55 +00:00
return null ;
2019-07-03 16:46:37 -06:00
}
2019-10-02 16:26:23 +01:00
return (
< ErrorBoundary >
2025-12-02 13:47:14 +00:00
< I18nContext.Provider value = { ModuleApi . instance . i18n }>
< SDKContext.Provider value = { this . stores }>
< TooltipProvider >{ view }</ TooltipProvider >
</ SDKContext.Provider >
</ I18nContext.Provider >
2019-10-02 16:26:23 +01:00
</ ErrorBoundary >
);
2020-04-22 11:55:03 +01:00
}
}