matrix-react-sdk
Version:
SDK for matrix.org using React
1,085 lines (1,052 loc) • 307 kB
JavaScript
"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.