UNPKG

matrix-react-sdk

Version:
1,085 lines (1,052 loc) 307 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "Views", { enumerable: true, get: function () { return _Views.default; } }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _matrix = require("matrix-js-sdk/src/matrix"); var _utils = require("matrix-js-sdk/src/utils"); var _logger = require("matrix-js-sdk/src/logger"); var _lodash = require("lodash"); var _crypto = require("matrix-js-sdk/src/crypto"); var _compoundWeb = require("@vector-im/compound-web"); require("what-input"); var _PosthogTrackers = _interopRequireDefault(require("../../PosthogTrackers")); var _DecryptionFailureTracker = require("../../DecryptionFailureTracker"); var _MatrixClientPeg = require("../../MatrixClientPeg"); var _PlatformPeg = _interopRequireDefault(require("../../PlatformPeg")); var _SdkConfig = _interopRequireDefault(require("../../SdkConfig")); var _dispatcher = _interopRequireDefault(require("../../dispatcher/dispatcher")); var _Notifier = _interopRequireDefault(require("../../Notifier")); var _Modal = _interopRequireDefault(require("../../Modal")); var _RoomInvite = require("../../RoomInvite"); var Rooms = _interopRequireWildcard(require("../../Rooms")); var Lifecycle = _interopRequireWildcard(require("../../Lifecycle")); require("../../stores/LifecycleStore"); require("../../stores/AutoRageshakeStore"); var _PageTypes = _interopRequireDefault(require("../../PageTypes")); var _createRoom = _interopRequireDefault(require("../../createRoom")); var _languageHandler = require("../../languageHandler"); var _SettingsStore = _interopRequireDefault(require("../../settings/SettingsStore")); var _ThemeController = _interopRequireDefault(require("../../settings/controllers/ThemeController")); var _Registration = require("../../Registration"); var _ResizeNotifier = _interopRequireDefault(require("../../utils/ResizeNotifier")); var _AutoDiscoveryUtils = _interopRequireDefault(require("../../utils/AutoDiscoveryUtils")); var _ThemeWatcher = _interopRequireDefault(require("../../settings/watchers/ThemeWatcher")); var _FontWatcher = require("../../settings/watchers/FontWatcher"); var _RoomAliasCache = require("../../RoomAliasCache"); var _ToastStore = _interopRequireDefault(require("../../stores/ToastStore")); var StorageManager = _interopRequireWildcard(require("../../utils/StorageManager")); var _LoggedInView = _interopRequireDefault(require("./LoggedInView")); var _actions = require("../../dispatcher/actions"); var _AnalyticsToast = require("../../toasts/AnalyticsToast"); var _DesktopNotificationsToast = require("../../toasts/DesktopNotificationsToast"); var _ErrorDialog = _interopRequireDefault(require("../views/dialogs/ErrorDialog")); var _RoomNotificationStateStore = require("../../stores/notifications/RoomNotificationStateStore"); var _SettingLevel = require("../../settings/SettingLevel"); var _ThreepidInviteStore = _interopRequireDefault(require("../../stores/ThreepidInviteStore")); var _UIFeature = require("../../settings/UIFeature"); var _DialPadModal = _interopRequireDefault(require("../views/voip/DialPadModal")); var _MobileGuideToast = require("../../toasts/MobileGuideToast"); var _pages = require("../../utils/pages"); var _RoomListStore = _interopRequireDefault(require("../../stores/room-list/RoomListStore")); var _models = require("../../stores/room-list/models"); var _ModuleRunner = require("../../modules/ModuleRunner"); var _Spinner = _interopRequireDefault(require("../views/elements/Spinner")); var _QuestionDialog = _interopRequireDefault(require("../views/dialogs/QuestionDialog")); var _UserSettingsDialog = _interopRequireDefault(require("../views/dialogs/UserSettingsDialog")); var _CreateRoomDialog = _interopRequireDefault(require("../views/dialogs/CreateRoomDialog")); var _IncomingSasDialog = _interopRequireDefault(require("../views/dialogs/IncomingSasDialog")); var _CompleteSecurity = _interopRequireDefault(require("./auth/CompleteSecurity")); var _Welcome = _interopRequireDefault(require("../views/auth/Welcome")); var _ForgotPassword = _interopRequireDefault(require("./auth/ForgotPassword")); var _E2eSetup = _interopRequireDefault(require("./auth/E2eSetup")); var _Registration2 = _interopRequireDefault(require("./auth/Registration")); var _Login = _interopRequireDefault(require("./auth/Login")); var _ErrorBoundary = _interopRequireDefault(require("../views/elements/ErrorBoundary")); var _VerificationRequestToast = _interopRequireDefault(require("../views/toasts/VerificationRequestToast")); var _performance = _interopRequireWildcard(require("../../performance")); var _UIStore = _interopRequireWildcard(require("../../stores/UIStore")); var _SoftLogout = _interopRequireDefault(require("./auth/SoftLogout")); var _Permalinks = require("../../utils/permalinks/Permalinks"); var _strings = require("../../utils/strings"); var _PosthogAnalytics = require("../../PosthogAnalytics"); var _sentry = require("../../sentry"); var _LegacyCallHandler = _interopRequireDefault(require("../../LegacyCallHandler")); var _space = require("../../utils/space"); var _Views = _interopRequireDefault(require("../../Views")); var _leaveBehaviour = require("../../utils/leave-behaviour"); var _CallStore = require("../../stores/CallStore"); var _RightPanelStorePhases = require("../../stores/right-panel/RightPanelStorePhases"); var _RightPanelStore = _interopRequireDefault(require("../../stores/right-panel/RightPanelStore")); var _RoomContext = require("../../contexts/RoomContext"); var _UseCaseSelection = require("../views/elements/UseCaseSelection"); var _isLocalRoom = require("../../utils/localRoom/isLocalRoom"); var _SDKContext = require("../../contexts/SDKContext"); var _viewUserDeviceSettings = require("../../actions/handlers/viewUserDeviceSettings"); var _voiceBroadcast = require("../../voice-broadcast"); var _GenericToast = _interopRequireDefault(require("../views/toasts/GenericToast")); var _SpotlightDialog = _interopRequireDefault(require("../views/dialogs/spotlight/SpotlightDialog")); var _findDMForUser = require("../../utils/dm/findDMForUser"); var _HtmlUtils = require("../../HtmlUtils"); var _NotificationLevel = require("../../stores/notifications/NotificationLevel"); var _shouldSkipSetupEncryption = require("../../utils/crypto/shouldSkipSetupEncryption"); var _Filter = require("../views/dialogs/spotlight/Filter"); var _SessionLock = require("../../utils/SessionLock"); var _SessionLockStolenView = require("./auth/SessionLockStolenView"); var _ConfirmSessionLockTheftView = require("./auth/ConfirmSessionLockTheftView"); var _LoginSplashView = require("./auth/LoginSplashView"); var _DraftCleaner = require("../../DraftCleaner"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /* Copyright 2024 New Vector Ltd. Copyright 2015-2024 The Matrix.org Foundation C.I.C. SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ // what-input helps improve keyboard accessibility // LifecycleStore is not used but does listen to and dispatch actions // legacy export const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", "start_sso", "start_cas", "welcome"]; // Actions that are redirected through the onboarding process prior to being // re-dispatched. NOTE: some actions are non-trivial and would require // re-factoring to be included in this list in future. const ONBOARDING_FLOW_STARTERS = [_actions.Action.ViewUserSettings, "view_create_chat", "view_create_room"]; class MatrixChat extends _react.default.PureComponent { constructor(props) { super(props); (0, _defineProperty2.default)(this, "firstSyncComplete", false); (0, _defineProperty2.default)(this, "firstSyncPromise", void 0); (0, _defineProperty2.default)(this, "screenAfterLogin", void 0); (0, _defineProperty2.default)(this, "tokenLogin", void 0); // What to focus on next component update, if anything (0, _defineProperty2.default)(this, "focusNext", void 0); (0, _defineProperty2.default)(this, "subTitleStatus", void 0); (0, _defineProperty2.default)(this, "prevWindowWidth", void 0); (0, _defineProperty2.default)(this, "voiceBroadcastResumer", void 0); (0, _defineProperty2.default)(this, "loggedInView", void 0); (0, _defineProperty2.default)(this, "dispatcherRef", void 0); (0, _defineProperty2.default)(this, "themeWatcher", void 0); (0, _defineProperty2.default)(this, "fontWatcher", void 0); (0, _defineProperty2.default)(this, "stores", void 0); /** * Kick off a call to {@link initSession}, and handle any errors */ (0, _defineProperty2.default)(this, "startInitSession", () => { const initProm = this.initSession(); if (this.props.initPromiseCallback) { this.props.initPromiseCallback(initProm); } initProm.catch(err => { // TODO: show an error screen, rather than a spinner of doom _logger.logger.error("Error initialising Matrix session", err); }); }); (0, _defineProperty2.default)(this, "onWindowResized", () => { // XXX: This is a very unreliable way to detect whether or not the the devtools are open this.warnInConsole(); }); (0, _defineProperty2.default)(this, "warnInConsole", (0, _lodash.throttle)(() => { const largeFontSize = "50px"; const normalFontSize = "15px"; const waitText = (0, _languageHandler._t)("console_wait"); const scamText = (0, _languageHandler._t)("console_scam_warning"); const devText = (0, _languageHandler._t)("console_dev_note"); global.mx_rage_logger.bypassRageshake("log", `%c${waitText}\n%c${scamText}\n%c${devText}`, `font-size:${largeFontSize}; color:blue;`, `font-size:${normalFontSize}; color:red;`, `font-size:${normalFontSize};`); }, 1000)); (0, _defineProperty2.default)(this, "onAction", payload => { // once the session lock has been stolen, don't try to do anything. if (this.state.view === _Views.default.LOCK_STOLEN) { return; } // Start the onboarding process for certain actions if (_MatrixClientPeg.MatrixClientPeg.get()?.isGuest() && ONBOARDING_FLOW_STARTERS.includes(payload.action)) { // This will cause `payload` to be dispatched later, once a // sync has reached the "prepared" state. Setting a matrix ID // will cause a full login and sync and finally the deferred // action will be dispatched. _dispatcher.default.dispatch({ action: _actions.Action.DoAfterSyncPrepared, deferred_action: payload }); _dispatcher.default.dispatch({ action: "require_registration" }); return; } switch (payload.action) { case "MatrixActions.accountData": // XXX: This is a collection of several hacks to solve a minor problem. We want to // update our local state when the identity server changes, but don't want to put that in // the js-sdk as we'd be then dictating how all consumers need to behave. However, // this component is already bloated and we probably don't want this tiny logic in // here, but there's no better place in the react-sdk for it. Additionally, we're // abusing the MatrixActionCreator stuff to avoid errors on dispatches. if (payload.event_type === "m.identity_server") { const fullUrl = payload.event_content ? payload.event_content["base_url"] : null; if (!fullUrl) { _MatrixClientPeg.MatrixClientPeg.safeGet().setIdentityServerUrl(undefined); localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_url"); } else { _MatrixClientPeg.MatrixClientPeg.safeGet().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); // clear token localStorage.setItem("mx_is_url", fullUrl); // XXX: Do we still need this? } // redispatch the change with a more specific action _dispatcher.default.dispatch({ action: "id_server_changed" }); } break; case "logout": _LegacyCallHandler.default.instance.hangupAllCalls(); Promise.all([...[..._CallStore.CallStore.instance.connectedCalls].map(call => call.disconnect()), (0, _voiceBroadcast.cleanUpBroadcasts)(this.stores)]).finally(() => Lifecycle.logout(this.stores.oidcClientStore)); break; case "require_registration": (0, _Registration.startAnyRegistrationFlow)(payload); break; case "start_mobile_registration": this.startRegistration(payload.params || {}, true); break; case "start_registration": if (Lifecycle.isSoftLogout()) { this.onSoftLogout(); break; } // This starts the full registration flow if (payload.screenAfterLogin) { this.screenAfterLogin = payload.screenAfterLogin; } this.startRegistration(payload.params || {}); break; case "start_login": if (Lifecycle.isSoftLogout()) { this.onSoftLogout(); break; } if (payload.screenAfterLogin) { this.screenAfterLogin = payload.screenAfterLogin; } this.viewLogin(); break; case "start_password_recovery": this.setStateForNewView({ view: _Views.default.FORGOT_PASSWORD }); this.notifyNewScreen("forgot_password"); break; case "start_chat": (0, _createRoom.default)(_MatrixClientPeg.MatrixClientPeg.safeGet(), { dmUserId: payload.user_id }); break; case "leave_room": this.leaveRoom(payload.room_id); break; case "forget_room": this.forgetRoom(payload.room_id); break; case "copy_room": this.copyRoom(payload.room_id); break; case "reject_invite": _Modal.default.createDialog(_QuestionDialog.default, { title: (0, _languageHandler._t)("reject_invitation_dialog|title"), description: (0, _languageHandler._t)("reject_invitation_dialog|confirmation"), onFinished: confirm => { if (confirm) { // FIXME: controller shouldn't be loading a view :( const modal = _Modal.default.createDialog(_Spinner.default, undefined, "mx_Dialog_spinner"); _MatrixClientPeg.MatrixClientPeg.safeGet().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { _dispatcher.default.dispatch({ action: _actions.Action.ViewHomePage }); } }, err => { modal.close(); _Modal.default.createDialog(_ErrorDialog.default, { title: (0, _languageHandler._t)("reject_invitation_dialog|failed"), description: err.toString() }); }); } } }); break; case "view_user_info": this.viewUser(payload.userId, payload.subAction); break; case "MatrixActions.RoomState.events": { const event = payload.event; if (event.getType() === _matrix.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: _actions.Action.ViewRoom, room_id: this.state.currentRoomId, metricsTrigger: undefined // room doesn't change }); } break; } case _actions.Action.ViewRoom: { // Takes either a room ID or room alias: if switching to a room the client is already // known to be in (eg. user clicks on a room in the recents panel), supply the ID // If the user is clicking on a room in the context of the alias being presented // to them, supply the room alias. If both are supplied, the room ID will be ignored. const promise = this.viewRoom(payload); if (payload.deferred_action) { promise.then(() => { _dispatcher.default.dispatch(payload.deferred_action); }); } break; } case _actions.Action.ViewUserDeviceSettings: { (0, _viewUserDeviceSettings.viewUserDeviceSettings)(); break; } case _actions.Action.ViewUserSettings: { const tabPayload = payload; _Modal.default.createDialog(_UserSettingsDialog.default, _objectSpread(_objectSpread({}, payload.props), {}, { initialTabId: tabPayload.initialTabId, sdkContext: this.stores }), /*className=*/undefined, /*isPriority=*/false, /*isStatic=*/true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; } case "view_create_room": this.createRoom(payload.public, payload.defaultName, payload.type); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; case _actions.Action.ViewRoomDirectory: { _Modal.default.createDialog(_SpotlightDialog.default, { initialText: payload.initialText, initialFilter: _Filter.Filter.PublicRooms }, "mx_SpotlightDialog_wrapper", false, true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; } case "view_welcome_page": this.viewWelcome(); break; case _actions.Action.ViewHomePage: this.viewHome(payload.justRegistered); break; case _actions.Action.ViewStartChatOrReuse: this.chatCreateOrReuse(payload.user_id); break; case "view_create_chat": (0, _RoomInvite.showStartChatInviteDialog)(payload.initialText || ""); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; case "view_invite": { const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(payload.roomId); if (room?.isSpaceRoom()) { (0, _space.showSpaceInvite)(room); } else { (0, _RoomInvite.showRoomInviteDialog)(payload.roomId); } break; } case "view_last_screen": // This function does what we want, despite the name. The idea is that it shows // the last room we were looking at or some reasonable default/guess. We don't // have to worry about email invites or similar being re-triggered because the // function will have cleared that state and not execute that path. this.showScreenAfterLogin(); break; case "hide_left_panel": this.setState({ collapseLhs: true }, () => { this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case "show_left_panel": this.setState({ collapseLhs: false }, () => { this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case _actions.Action.OpenDialPad: _Modal.default.createDialog(_DialPadModal.default, {}, "mx_Dialog_dialPadWrapper"); break; case _actions.Action.OnLoggedIn: this.stores.client = _MatrixClientPeg.MatrixClientPeg.safeGet(); if ( // Skip this handling for token login as that always calls onLoggedIn itself !this.tokenLogin && !Lifecycle.isSoftLogout() && this.state.view !== _Views.default.LOGIN && this.state.view !== _Views.default.REGISTER && this.state.view !== _Views.default.COMPLETE_SECURITY && this.state.view !== _Views.default.E2E_SETUP && this.state.view !== _Views.default.USE_CASE_SELECTION) { this.onLoggedIn(); } break; case "on_client_not_viable": this.onSoftLogout(); break; case _actions.Action.OnLoggedOut: this.onLoggedOut(); break; case "will_start_client": this.setState({ ready: false }, () => { // if the client is about to start, we are, by definition, not ready. // Set ready to false now, then it'll be set to true when the sync // listener we set below fires. this.onWillStartClient(); }); break; case "client_started": // No need to make this handler async to wait for the result of this this.onClientStarted().catch(e => { _logger.logger.error("Exception in onClientStarted", e); }); break; case "send_event": this.onSendEvent(payload.room_id, payload.event); break; case "aria_hide_main_app": this.setState({ hideToSRUsers: true }); break; case "aria_unhide_main_app": this.setState({ hideToSRUsers: false }); break; case _actions.Action.PseudonymousAnalyticsAccept: (0, _AnalyticsToast.hideToast)(); _SettingsStore.default.setValue("pseudonymousAnalyticsOptIn", null, _SettingLevel.SettingLevel.ACCOUNT, true); break; case _actions.Action.PseudonymousAnalyticsReject: (0, _AnalyticsToast.hideToast)(); _SettingsStore.default.setValue("pseudonymousAnalyticsOptIn", null, _SettingLevel.SettingLevel.ACCOUNT, false); break; case _actions.Action.ShowThread: { const { rootEvent, initialEvent, highlighted, scrollIntoView, push } = payload; const threadViewCard = { phase: _RightPanelStorePhases.RightPanelPhases.ThreadView, state: { threadHeadEvent: rootEvent, initialEvent: initialEvent, isInitialEventHighlighted: highlighted, initialEventScrollIntoView: scrollIntoView } }; if (push ?? false) { _RightPanelStore.default.instance.pushCard(threadViewCard); } else { _RightPanelStore.default.instance.setCards([{ phase: _RightPanelStorePhases.RightPanelPhases.ThreadPanel }, threadViewCard]); } // Focus the composer _dispatcher.default.dispatch({ action: _actions.Action.FocusSendMessageComposer, context: _RoomContext.TimelineRenderingType.Thread }); break; } case _actions.Action.OpenSpotlight: _Modal.default.createDialog(_SpotlightDialog.default, { initialText: payload.initialText, initialFilter: payload.initialFilter }, "mx_SpotlightDialog_wrapper", false, true); break; } }); (0, _defineProperty2.default)(this, "handleResize", () => { const LHS_THRESHOLD = 1000; const width = _UIStore.default.instance.windowWidth; if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) { _dispatcher.default.dispatch({ action: "show_left_panel" }); } if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) { _dispatcher.default.dispatch({ action: "hide_left_panel" }); } this.prevWindowWidth = width; this.state.resizeNotifier.notifyWindowResized(); }); (0, _defineProperty2.default)(this, "onRegisterClick", () => { this.showScreen("register"); }); (0, _defineProperty2.default)(this, "onLoginClick", () => { this.showScreen("login"); }); (0, _defineProperty2.default)(this, "onForgotPasswordClick", () => { this.showScreen("forgot_password"); }); (0, _defineProperty2.default)(this, "onRegisterFlowComplete", (credentials, password) => { return this.onUserCompletedLoginFlow(credentials, password); }); (0, _defineProperty2.default)(this, "onUpdateStatusIndicator", (notificationState, state) => { const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here if (_PlatformPeg.default.get()) { _PlatformPeg.default.get().setErrorStatus(state === _matrix.SyncState.Error); _PlatformPeg.default.get().setNotificationCount(numUnreadRooms); } this.subTitleStatus = ""; if (state === _matrix.SyncState.Error) { this.subTitleStatus += `[${(0, _languageHandler._t)("common|offline")}] `; } if (numUnreadRooms > 0) { this.subTitleStatus += `[${numUnreadRooms}]`; } else if (notificationState.level >= _NotificationLevel.NotificationLevel.Activity) { this.subTitleStatus += `*`; } this.setPageSubtitle(); }); (0, _defineProperty2.default)(this, "onServerConfigChange", serverConfig => { this.setState({ serverConfig }); }); /** * 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`. */ (0, _defineProperty2.default)(this, "onUserCompletedLoginFlow", async (credentials, password) => { this.stores.accountPasswordStore.setPassword(password); // Create and start the client await Lifecycle.setLoggedIn(credentials); await this.postLoginSetup(); _performance.default.instance.stop(_performance.PerformanceEntryNames.LOGIN); _performance.default.instance.stop(_performance.PerformanceEntryNames.REGISTER); }); // complete security / e2e setup has finished (0, _defineProperty2.default)(this, "onCompleteSecurityE2eSetupFinished", () => { if (_MatrixClientPeg.MatrixClientPeg.currentUserIsJustRegistered() && _SettingsStore.default.getValue("FTUE.useCaseSelection") === null) { this.setStateForNewView({ view: _Views.default.USE_CASE_SELECTION }); // Listen to changes in settings and hide the use case screen if appropriate - this is necessary because // account settings can still be changing at this point in app init (due to the initial sync being cached, // then subsequent syncs being received from the server) // // This seems unlikely for something that should happen directly after registration, but if a user does // their initial login on another device/browser than they registered on, we want to avoid asking this // question twice // // initPosthogAnalyticsToast pioneered this technique, we’re just reusing it here. _SettingsStore.default.watchSetting("FTUE.useCaseSelection", null, (originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue) => { if (newValue !== null && this.state.view === _Views.default.USE_CASE_SELECTION) { this.onShowPostLoginScreen(); } }); } else { // This is async but we makign this function async to wait for it isn't useful this.onShowPostLoginScreen().catch(e => { _logger.logger.error("Exception showing post-login screen", e); }); } }); this.stores = _SDKContext.SdkContextClass.instance; this.stores.constructEagerStores(); this.state = { view: _Views.default.LOADING, collapseLhs: false, currentRoomId: null, currentUserId: null, hideToSRUsers: false, isMobileRegistration: false, syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. resizeNotifier: new _ResizeNotifier.default(), ready: false }; this.loggedInView = /*#__PURE__*/(0, _react.createRef)(); _SdkConfig.default.put(this.props.config); // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; this.firstSyncPromise = (0, _utils.defer)(); if (this.props.config.sync_timeline_limit) { _MatrixClientPeg.MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } // a thing to call showScreen with once login completes. this is kept // outside this.state because updating it should never trigger a // rerender. this.screenAfterLogin = this.props.initialScreenAfterLogin; if (this.screenAfterLogin) { const params = this.screenAfterLogin.params || {}; if (this.screenAfterLogin.screen.startsWith("room/") && params["signurl"] && params["email"]) { // probably a threepid invite - try to store it const roomId = this.screenAfterLogin.screen.substring("room/".length); _ThreepidInviteStore.default.instance.storeInvite(roomId, params); } } this.prevWindowWidth = _UIStore.default.instance.windowWidth || 1000; _UIStore.default.instance.on(_UIStore.UI_EVENTS.Resize, this.handleResize); // For PersistentElement this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); _RoomNotificationStateStore.RoomNotificationStateStore.instance.on(_RoomNotificationStateStore.UPDATE_STATUS_INDICATOR, this.onUpdateStatusIndicator); this.dispatcherRef = _dispatcher.default.register(this.onAction); this.themeWatcher = new _ThemeWatcher.default(); this.fontWatcher = new _FontWatcher.FontWatcher(); this.themeWatcher.start(); this.fontWatcher.start(); // 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 = ""; (0, _sentry.initSentry)(_SdkConfig.default.get("sentry")); if (!(0, _SessionLock.checkSessionLockFree)()) { // another instance holds the lock; confirm its theft before proceeding setTimeout(() => this.setState({ view: _Views.default.CONFIRM_LOCK_THEFT }), 0); } else { this.startInitSession(); } } /** * 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. */ async initSession() { // 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). if (!(await (0, _SessionLock.getSessionLock)(() => this.onSessionLockStolen()))) { // we failed to get the lock. onSessionLockStolen should already have been called, so nothing left to do. return; } // 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. Lifecycle.loadSession(); return; } // 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 await Lifecycle.restoreSessionFromStorage({ ignoreGuest: true }); 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; } // If the first screen is an auth screen, we don't want to wait for login. if (firstScreen !== null && AUTH_SCREENS.includes(firstScreen)) { this.showScreenAfterLogin(); } } async onSessionLockStolen() { // 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(resolve => { this.setState({ view: _Views.default.LOCK_STOLEN }, resolve); }); // now we can tell the Lifecycle routines to abort any active startup, and to stop the active client. await Lifecycle.onSessionLockStolen(); } async postLoginSetup() { const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const cryptoEnabled = cli.isCryptoEnabled(); if (!cryptoEnabled) { this.onLoggedIn(); } const promisesList = [this.firstSyncPromise.promise]; let crossSigningIsSetUp = false; if (cryptoEnabled) { // check if the user has previously published public cross-signing keys, // as a proxy to figure out if it's worth prompting the user to verify // from another device. promisesList.push((async () => { crossSigningIsSetUp = Boolean(await cli.getCrypto()?.userHasCrossSigningKeys()); })()); } // Now update the state to say we're waiting for the first sync to complete rather // than for the login to finish. this.setState({ pendingInitialSync: true }); await Promise.all(promisesList); if (!cryptoEnabled) { this.setState({ pendingInitialSync: false }); return; } if (crossSigningIsSetUp) { // if the user has previously set up cross-signing, verify this device so we can fetch the // private keys. const cryptoExtension = _ModuleRunner.ModuleRunner.instance.extensions.cryptoSetup; if (cryptoExtension.SHOW_ENCRYPTION_SETUP_UI == false) { this.onLoggedIn(); } else { this.setStateForNewView({ view: _Views.default.COMPLETE_SECURITY }); } } else if ((await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) && !(0, _shouldSkipSetupEncryption.shouldSkipSetupEncryption)(cli)) { // if cross-signing is not yet set up, do so now if possible. this.setStateForNewView({ view: _Views.default.E2E_SETUP }); } else { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); } setState(state, callback) { if (this.shouldTrackPageChange(this.state, _objectSpread(_objectSpread({}, this.state), state))) { this.startPageChangeTimer(); } super.setState(state, callback); } componentDidMount() { window.addEventListener("resize", this.onWindowResized); } componentDidUpdate(prevProps, prevState) { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); if (durationMs != null) { _PosthogTrackers.default.instance.trackPageChange(this.state.view, this.state.page_type, durationMs); } } if (this.focusNext === "composer") { _dispatcher.default.fire(_actions.Action.FocusSendMessageComposer); this.focusNext = undefined; } else if (this.focusNext === "threadsPanel") { _dispatcher.default.fire(_actions.Action.FocusThreadsPanel); } } componentWillUnmount() { Lifecycle.stopMatrixClient(); _dispatcher.default.unregister(this.dispatcherRef); this.themeWatcher.stop(); this.fontWatcher.stop(); _UIStore.default.destroy(); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); window.removeEventListener("resize", this.onWindowResized); this.stores.accountPasswordStore.clearPassword(); this.voiceBroadcastResumer?.destroy(); } getFallbackHsUrl() { if (this.getServerProperties().serverConfig?.isDefault) { return this.props.config.fallback_hs_url; } } getServerProperties() { const props = this.state.serverConfig || _SdkConfig.default.get("validated_server_config"); return { serverConfig: props }; } loadSession() { // the extra Promise.resolve() ensures that synchronous exceptions hit the same codepath as // asynchronous ones. return Promise.resolve().then(() => { return Lifecycle.loadSession({ fragmentQueryParams: this.props.startingFragmentQueryParams, enableGuest: this.props.enableGuest, guestHsUrl: this.getServerProperties().serverConfig.hsUrl, guestIsUrl: this.getServerProperties().serverConfig.isUrl, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName }); }).then(loadedSession => { if (!loadedSession) { // fall back to showing the welcome screen... unless we have a 3pid invite pending if (_ThreepidInviteStore.default.instance.pickBestInvite() && _SettingsStore.default.getValue(_UIFeature.UIFeature.Registration)) { _dispatcher.default.dispatch({ action: "start_registration" }); } else { _dispatcher.default.dispatch({ action: "view_welcome_page" }); } } return loadedSession; }); // Note we don't catch errors from this: we catch everything within // loadSession as there's logic there to ask the user if they want // to try logging out. } startPageChangeTimer() { _performance.default.instance.start(_performance.PerformanceEntryNames.PAGE_CHANGE); } stopPageChangeTimer() { const perfMonitor = _performance.default.instance; perfMonitor.stop(_performance.PerformanceEntryNames.PAGE_CHANGE); const entries = perfMonitor.getEntries({ name: _performance.PerformanceEntryNames.PAGE_CHANGE }); const measurement = entries.pop(); return measurement ? measurement.duration : null; } shouldTrackPageChange(prevState, state) { return prevState.currentRoomId !== state.currentRoomId || prevState.view !== state.view || prevState.page_type !== state.page_type; } setStateForNewView(state) { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); } this.setState(_objectSpread({ currentUserId: undefined, justRegistered: false }, state)); } setPage(pageType) { this.setState({ page_type: pageType }); } async startRegistration(params, isMobileRegistration) { // If registration is disabled or mobile registration is requested but not enabled in settings redirect to the welcome screen if (!_SettingsStore.default.getValue(_UIFeature.UIFeature.Registration) || isMobileRegistration && !_SettingsStore.default.getValue("Registration.mobileRegistrationHelper")) { this.showScreen("welcome"); return; } const newState = { view: _Views.default.REGISTER }; if (isMobileRegistration && params.hs_url) { try { const config = await _AutoDiscoveryUtils.default.validateServerConfigWithStaticUrls(params.hs_url); newState.serverConfig = config; } catch (err) { _logger.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. newState.serverConfig = await _AutoDiscoveryUtils.default.validateServerConfigWithStaticUrls(params.hs_url, params.is_url); // If the hs url matches then take the hs name we know locally as it is likely prettier const defaultConfig = _SdkConfig.default.get("validated_server_config"); if (defaultConfig && defaultConfig.hsUrl === newState.serverConfig.hsUrl) { newState.serverConfig.hsName = defaultConfig.hsName; newState.serverConfig.hsNameIsDifferent = defaultConfig.hsNameIsDifferent; newState.serverConfig.isDefault = defaultConfig.isDefault; newState.serverConfig.isNameResolvable = defaultConfig.isNameResolvable; } newState.register_client_secret = params.client_secret; newState.register_session_id = params.session_id; newState.register_id_sid = params.sid; } newState.isMobileRegistration = isMobileRegistration; this.setStateForNewView(newState); _ThemeController.default.isLogin = true; this.themeWatcher.recheck(); this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); } // switch view to the given room async viewRoom(roomInfo) { this.focusNext = roomInfo.focusNext ?? "composer"; if (roomInfo.room_alias) { _logger.logger.log(`Switching to room alias ${roomInfo.room_alias} at event ${roomInfo.event_id}`); } else { _logger.logger.log(`Switching to room id ${roomInfo.room_id} at event ${roomInfo.event_id}`); } // Wait for the first sync to complete so that if a room does have an alias, // it would have been retrieved. if (!this.firstSyncComplete) { if (!this.firstSyncPromise) { _logger.logger.warn("Cannot view a room before first sync. room_id:", roomInfo.room_id); return; } await this.firstSyncPromise.promise; } let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(roomInfo.room_id); if (room) { // Not all timeline events are decrypted ahead of time anymore // Only the critical ones for a typical UI are // This will start the decryption process for all events when a // user views a room room.decryptAllEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { presentedId = theAlias; // Store display alias of the presented room in cache to speed future // navigation. (0, _RoomAliasCache.storeRoomAliasInCache)(theAlias, room.roomId); } // Store this as the ID of the last room accessed. This is so that we can // persist which room is being stored across refreshes and browser quits. localStorage?.setItem("mx_last_room_id", room.roomId); } // If we are redirecting to a Room Alias and it is for the room we already showing then replace history item let replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId; if ((0, _isLocalRoom.isLocalRoom)(this.state.currentRoomId)) { // Replace local room history items replaceLast = true; } if (roomInfo.room_id === this.state.currentRoomId) { // if we are re-viewing the same room then copy any state we already know roomInfo.threepid_invite = roomInfo.threepid_invite ?? this.state.threepidInvite; roomInfo.oob_data = roomInfo.oob_data ?? this.state.roomOobData; roomInfo.forceTimeline = roomInfo.forceTimeline ?? this.state.forceTimeline; roomInfo.justCreatedOpts = roomInfo.justCreatedOpts ?? this.state.roomJustCreatedOpts; } if (roomInfo.event_id && roomInfo.highlighted) { presentedId += "/" + roomInfo.event_id; } this.setState({ view: _Views.default.LOGGED_IN, currentRoomId: roomInfo.room_id ?? null, page_type: _PageTypes.default.RoomView, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, forceTimeline: roomInfo.forceTimeline, ready: true, roomJustCreatedOpts: roomInfo.justCreatedOpts }, () => { _ThemeController.default.isLogin = false; this.themeWatcher.recheck(); this.notifyNewScreen("room/" + presentedId, replaceLast); }); } viewSomethingBehindModal() { if (this.state.view !== _Views.default.LOGGED_IN) { this.viewWelcome(); return; } if (!this.state.currentRoomId && !this.state.currentUserId) { this.viewHome(); } } viewWelcome() { if ((0, _pages.shouldUseLoginForWelcome)(_SdkConfig.default.get())) { return this.viewLogin(); } this.setStateForNewView({ view: _Views.default.WELCOME }); this.notifyNewScreen("welcome"); _ThemeController.default.isLogin = true; this.themeWatcher.recheck(); } viewLogin(otherState) { this.setStateForNewView(_objectSpread({ view: _Views.default.LOGIN }, otherState)); this.notifyNewScreen("login"); _ThemeController.default.isLogin = true; this.themeWatcher.recheck(); } viewHome(justRegistered = false) { // The home page requires the "logged in" view, so we'll set that. this.setStateForNewView({ view: _Views.default.LOGGED_IN, justRegistered, currentRoomId: null }); this.setPage(_PageTypes.default.HomePage); this.notifyNewScreen("home"); _ThemeController.default.isLogin = false; this.themeWatcher.recheck(); } viewUser(userId, subAction) { // Wait for the first sync so that `getRoom` gives us a room object if it's // in the sync response const waitForSync = this.firstSyncPromise ? this.firstSyncPromise.promise : Promise.resolve(); waitForSync.then(() => { if (subAction === "chat") { this.chatCreateOrReuse(userId); return; } this.notifyNewScreen("user/" + userId); this.setState({ currentUserId: userId }); this.setPage(_PageTypes.default.UserView); }); } async createRoom(defaultPublic = false, defaultName, type) { const modal = _Modal.default.createDialog(_CreateRoomDialog.default, { type, defaultPublic, defaultName }); const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { (0, _createRoom.default)(_MatrixClientPeg.MatrixClientPeg.safeGet(), opts); } } chatCreateOrReuse(userId) { // Use a deferred action to reshow the dialog once the user has registered if (_MatrixClientPeg.MatrixClientPeg.safeGet().isGuest()) { _dispatcher.default.dispatch({ action: _actions.Action.DoAfterSyncPrepared, deferred_action: { action: _actions.Action.ViewStartChatOrReuse, user_id: userId } }); return; } // TODO: Immutable DMs replaces this const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); const dmRoom = (0, _findDMForUser.findDMForUser)(client, userId); if (dmRoom) { _dispatcher.default.