UNPKG

matrix-react-sdk

Version:
1,408 lines (1,102 loc) 278 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard3 = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports.isLoggedIn = isLoggedIn; exports.default = exports.Views = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _interopRequireWildcard2 = _interopRequireDefault(require("@babel/runtime/helpers/interopRequireWildcard")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard3(require("react")); var _matrix = require("matrix-js-sdk/src/matrix"); var _errors = require("matrix-js-sdk/src/errors"); var _roomMember = require("matrix-js-sdk/src/models/room-member"); require("focus-visible"); require("what-input"); var _Analytics = _interopRequireDefault(require("../../Analytics")); var _CountlyAnalytics = _interopRequireDefault(require("../../CountlyAnalytics")); 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 _Tinter = _interopRequireDefault(require("../../Tinter")); var sdk = _interopRequireWildcard3(require("../../index")); var _RoomInvite = require("../../RoomInvite"); var Rooms = _interopRequireWildcard3(require("../../Rooms")); var _linkifyMatrix = _interopRequireDefault(require("../../linkify-matrix")); var Lifecycle = _interopRequireWildcard3(require("../../Lifecycle")); require("../../stores/LifecycleStore"); 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.js"); var _ErrorUtils = require("../../utils/ErrorUtils"); var _ResizeNotifier = _interopRequireDefault(require("../../utils/ResizeNotifier")); var _AutoDiscoveryUtils = _interopRequireDefault(require("../../utils/AutoDiscoveryUtils")); var _DMRoomMap = _interopRequireDefault(require("../../utils/DMRoomMap")); var _ThemeWatcher = _interopRequireDefault(require("../../settings/watchers/ThemeWatcher")); var _FontWatcher = require("../../settings/watchers/FontWatcher"); var _RoomAliasCache = require("../../RoomAliasCache"); var _promise = require("../../utils/promise"); var _ToastStore = _interopRequireDefault(require("../../stores/ToastStore")); var StorageManager = _interopRequireWildcard3(require("../../utils/StorageManager")); 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 _membership = require("../../utils/membership"); var _CreateCommunityPrototypeDialog = _interopRequireDefault(require("../views/dialogs/CreateCommunityPrototypeDialog")); var _ThreepidInviteStore = _interopRequireDefault(require("../../stores/ThreepidInviteStore")); var _UIFeature = require("../../settings/UIFeature"); var _CommunityPrototypeStore = require("../../stores/CommunityPrototypeStore"); var _DialPadModal = _interopRequireDefault(require("../views/voip/DialPadModal")); var _MobileGuideToast = require("../../toasts/MobileGuideToast"); var _pages = require("../../utils/pages"); var _SpaceStore = _interopRequireDefault(require("../../stores/SpaceStore")); var _replaceableComponent = require("../../utils/replaceableComponent"); var _RoomListStore = _interopRequireDefault(require("../../stores/room-list/RoomListStore")); var _models = require("../../stores/room-list/models"); var _Security = _interopRequireDefault(require("../../customisations/Security")); var _performance = _interopRequireWildcard3(require("../../performance")); var _dec, _class, _class2, _temp; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } /** constants for MatrixChat.state.view */ let Views; exports.Views = Views; (function (Views) { Views[Views["LOADING"] = 0] = "LOADING"; Views[Views["WELCOME"] = 1] = "WELCOME"; Views[Views["LOGIN"] = 2] = "LOGIN"; Views[Views["REGISTER"] = 3] = "REGISTER"; Views[Views["FORGOT_PASSWORD"] = 4] = "FORGOT_PASSWORD"; Views[Views["COMPLETE_SECURITY"] = 5] = "COMPLETE_SECURITY"; Views[Views["E2E_SETUP"] = 6] = "E2E_SETUP"; Views[Views["LOGGED_IN"] = 7] = "LOGGED_IN"; Views[Views["SOFT_LOGOUT"] = 8] = "SOFT_LOGOUT"; })(Views || (exports.Views = Views = {})); const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"]; // 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', 'view_create_group']; let MatrixChat = (_dec = (0, _replaceableComponent.replaceableComponent)("structures.MatrixChat"), _dec(_class = (_temp = _class2 = class MatrixChat extends _react.default.PureComponent /*:: <IProps, IState>*/ { constructor(props, context) { super(props, context); (0, _defineProperty2.default)(this, "firstSyncComplete", void 0); (0, _defineProperty2.default)(this, "firstSyncPromise", void 0); (0, _defineProperty2.default)(this, "screenAfterLogin", void 0); (0, _defineProperty2.default)(this, "windowWidth", void 0); (0, _defineProperty2.default)(this, "pageChanging", void 0); (0, _defineProperty2.default)(this, "tokenLogin", void 0); (0, _defineProperty2.default)(this, "accountPassword", void 0); (0, _defineProperty2.default)(this, "accountPasswordTimer", void 0); (0, _defineProperty2.default)(this, "focusComposer", void 0); (0, _defineProperty2.default)(this, "subTitleStatus", 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, "onAction", payload => { // console.log(`MatrixClientPeg.onAction: ${payload.action}`); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // Start the onboarding process for certain actions if (_MatrixClientPeg.MatrixClientPeg.get() && _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: 'do_after_sync_prepared', 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 ID 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.get().setIdentityServerUrl(null); localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_url"); } else { _MatrixClientPeg.MatrixClientPeg.get().setIdentityServerUrl(fullUrl); localStorage.removeItem("mx_is_access_token"); // clear token localStorage.setItem("mx_is_url", fullUrl); // XXX: Do we still need this? } // redispatch the change with a more specific action _dispatcher.default.dispatch({ action: 'id_server_changed' }); } break; case 'logout': _dispatcher.default.dispatch({ action: "hangup_all" }); Lifecycle.logout(); break; case 'require_registration': (0, _Registration.startAnyRegistrationFlow)(payload); break; case 'start_registration': if (Lifecycle.isSoftLogout()) { this.onSoftLogout(); break; } // This starts the full registration flow if (payload.screenAfterLogin) { this.screenAfterLogin = payload.screenAfterLogin; } this.startRegistration(payload.params || {}); break; case 'start_login': if (Lifecycle.isSoftLogout()) { this.onSoftLogout(); break; } if (payload.screenAfterLogin) { this.screenAfterLogin = payload.screenAfterLogin; } this.viewLogin(); break; case 'start_password_recovery': this.setStateForNewView({ view: Views.FORGOT_PASSWORD }); this.notifyNewScreen('forgot_password'); break; case 'start_chat': (0, _createRoom.default)({ dmUserId: payload.user_id }); break; case 'leave_room': this.leaveRoom(payload.room_id); break; case 'forget_room': this.forgetRoom(payload.room_id); break; case 'reject_invite': _Modal.default.createTrackedDialog('Reject invitation', '', QuestionDialog, { title: (0, _languageHandler._t)('Reject invitation'), description: (0, _languageHandler._t)('Are you sure you want to reject the invitation?'), onFinished: confirm => { if (confirm) { // FIXME: controller shouldn't be loading a view :( const Loader = sdk.getComponent("elements.Spinner"); const modal = _Modal.default.createDialog(Loader, null, 'mx_Dialog_spinner'); _MatrixClientPeg.MatrixClientPeg.get().leave(payload.room_id).then(() => { modal.close(); if (this.state.currentRoomId === payload.room_id) { _dispatcher.default.dispatch({ action: 'view_home_page' }); } }, err => { modal.close(); _Modal.default.createTrackedDialog('Failed to reject invitation', '', _ErrorDialog.default, { title: (0, _languageHandler._t)('Failed to reject invitation'), description: err.toString() }); }); } } }); break; case 'view_user_info': this.viewUser(payload.userId, payload.subAction); break; case 'view_room': { // 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.ViewUserSettings: { const tabPayload = payload; const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); _Modal.default.createTrackedDialog('User settings', '', UserSettingsDialog, { initialTabId: tabPayload.initialTabId }, /*className=*/ null, /*isPriority=*/ false, /*isStatic=*/ true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; } case 'view_create_room': this.createRoom(payload.public); break; case 'view_create_group': { let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); if (_SettingsStore.default.getValue("feature_communities_v2_prototypes")) { CreateGroupDialog = _CreateCommunityPrototypeDialog.default; } _Modal.default.createTrackedDialog('Create Community', '', CreateGroupDialog); break; } case _actions.Action.ViewRoomDirectory: { if (_SpaceStore.default.instance.activeSpace) { _dispatcher.default.dispatch({ action: "view_room", room_id: _SpaceStore.default.instance.activeSpace.roomId }); } else { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); _Modal.default.createTrackedDialog('Room directory', '', RoomDirectory, { initialText: payload.initialText }, 'mx_RoomDirectory_dialogWrapper', false, true); } // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); break; } case 'view_my_groups': this.setPage(_PageTypes.default.MyGroups); this.notifyNewScreen('groups'); break; case 'view_group': this.viewGroup(payload); break; case 'view_welcome_page': this.viewWelcome(); break; case 'view_home_page': this.viewHome(payload.justRegistered); break; case 'view_start_chat_or_reuse': this.chatCreateOrReuse(payload.user_id); break; case 'view_create_chat': (0, _RoomInvite.showStartChatInviteDialog)(payload.initialText || ""); break; case 'view_invite': (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 'toggle_my_groups': // persist that the user has interacted with this, use it to dismiss the beta dot localStorage.setItem("mx_seenSpacesBeta", "1"); // We just dispatch the page change rather than have to worry about // what the logic is for each of these branches. if (this.state.page_type === _PageTypes.default.MyGroups) { _dispatcher.default.dispatch({ action: 'view_last_screen' }); } else { _dispatcher.default.dispatch({ action: 'view_my_groups' }); } break; case 'hide_left_panel': this.setState({ collapseLhs: true }, () => { this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first case 'show_left_panel': this.setState({ collapseLhs: false }, () => { this.state.resizeNotifier.notifyLeftHandleResized(); }); break; case _actions.Action.OpenDialPad: _Modal.default.createTrackedDialog('Dial pad', '', _DialPadModal.default, {}, "mx_Dialog_dialPadWrapper"); break; case 'on_logged_in': if ( // Skip this handling for token login as that always calls onLoggedIn itself !this.tokenLogin && !Lifecycle.isSoftLogout() && this.state.view !== Views.LOGIN && this.state.view !== Views.REGISTER && this.state.view !== Views.COMPLETE_SECURITY && this.state.view !== Views.E2E_SETUP) { this.onLoggedIn(); } break; case 'on_client_not_viable': this.onSoftLogout(); break; case 'on_logged_out': this.onLoggedOut(); break; case 'will_start_client': this.setState({ ready: false }, () => { // if the client is about to start, we are, by definition, not ready. // Set ready to false now, then it'll be set to true when the sync // listener we set below fires. this.onWillStartClient(); }); break; case 'client_started': this.onClientStarted(); break; case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; case 'aria_hide_main_app': this.setState({ hideToSRUsers: true }); break; case 'aria_unhide_main_app': this.setState({ hideToSRUsers: false }); break; case 'accept_cookies': _SettingsStore.default.setValue("analyticsOptIn", null, _SettingLevel.SettingLevel.DEVICE, true); _SettingsStore.default.setValue("showCookieBar", null, _SettingLevel.SettingLevel.DEVICE, false); (0, _AnalyticsToast.hideToast)(); if (_Analytics.default.canEnable()) { _Analytics.default.enable(); } if (_CountlyAnalytics.default.instance.canEnable()) { _CountlyAnalytics.default.instance.enable( /* anonymous = */ false); } break; case 'reject_cookies': _SettingsStore.default.setValue("analyticsOptIn", null, _SettingLevel.SettingLevel.DEVICE, false); _SettingsStore.default.setValue("showCookieBar", null, _SettingLevel.SettingLevel.DEVICE, false); (0, _AnalyticsToast.hideToast)(); break; } }); (0, _defineProperty2.default)(this, "handleResize", () => { const hideLhsThreshold = 1000; const showLhsThreshold = 1000; if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { _dispatcher.default.dispatch({ action: 'hide_left_panel' }); } if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { _dispatcher.default.dispatch({ action: 'show_left_panel' }); } this.state.resizeNotifier.notifyWindowResized(); this.windowWidth = window.innerWidth; }); (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 /*: IMatrixClientCreds*/ , password /*: string*/ ) => { return this.onUserCompletedLoginFlow(credentials, password); }); (0, _defineProperty2.default)(this, "onServerConfigChange", (serverConfig /*: ValidatedServerConfig*/ ) => { this.setState({ serverConfig }); }); (0, _defineProperty2.default)(this, "makeRegistrationUrl", (params /*: {[key: string]: string}*/ ) => { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; } return this.props.makeRegistrationUrl(params); }); (0, _defineProperty2.default)(this, "onUserCompletedLoginFlow", async (credentials /*: IMatrixClientCreds*/ , password /*: string*/ ) => { this.accountPassword = password; // self-destruct the password after 5mins if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); this.accountPasswordTimer = setTimeout(() => { this.accountPassword = null; this.accountPasswordTimer = null; }, 60 * 5 * 1000); // 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); }); (0, _defineProperty2.default)(this, "onCompleteSecurityE2eSetupFinished", () => { this.onLoggedIn(); }); this.state = { view: Views.LOADING, collapseLhs: false, hideToSRUsers: false, syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. resizeNotifier: new _ResizeNotifier.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, _promise.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.windowWidth = 10000; this.handleResize(); window.addEventListener('resize', this.handleResize); this.pageChanging = false; // check we have the right tint applied for this theme. // N.B. we don't call the whole of setTheme() here as we may be // racing with the theme CSS download finishing from index.js _Tinter.default.tint(); // For PersistentElement this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize); // Force users to go through the soft logout page if they're soft logged out if (Lifecycle.isSoftLogout()) { // When the session loads it'll be detected as soft logged out and a dispatch // will be sent out to say that, triggering this MatrixChat to show the soft // logout page. Lifecycle.loadSession(); } this.accountPassword = null; this.accountPasswordTimer = null; this.dispatcherRef = _dispatcher.default.register(this.onAction); this.themeWatcher = new _ThemeWatcher.default(); this.fontWatcher = new _FontWatcher.FontWatcher(); this.themeWatcher.start(); this.fontWatcher.start(); this.focusComposer = false; // object field used for tracking the status info appended to the title tag. // we don't do it as react state as i'm scared about triggering needless react refreshes. this.subTitleStatus = ''; // this can technically be done anywhere but doing this here keeps all // the routing url path logic together. if (this.onAliasClick) { _linkifyMatrix.default.onAliasClick = this.onAliasClick; } if (this.onUserClick) { _linkifyMatrix.default.onUserClick = this.onUserClick; } if (this.onGroupClick) { _linkifyMatrix.default.onGroupClick = this.onGroupClick; } // the first thing to do is to try the token params in the query-string // if the session isn't soft logged out (ie: is a clean session being logged in) if (!Lifecycle.isSoftLogout()) { Lifecycle.attemptTokenLogin(this.props.realQueryParams, this.props.defaultDeviceDisplayName, this.getFragmentAfterLogin()).then(async loggedIn => { if (this.props.realQueryParams?.loginToken) { // remove the loginToken from the URL regardless this.props.onTokenLoginCompleted(); } if (loggedIn) { this.tokenLogin = true; // Create and start the client await Lifecycle.restoreFromLocalStorage({ ignoreGuest: true }); return this.postLoginSetup(); } // if the user has followed a login or register link, don't reanimate // the old creds, but rather go straight to the relevant page const firstScreen = this.screenAfterLogin ? this.screenAfterLogin.screen : null; if (firstScreen === 'login' || firstScreen === 'register' || firstScreen === 'forgot_password') { this.showScreenAfterLogin(); return; } return this.loadSession(); }); } if (_SettingsStore.default.getValue("analyticsOptIn")) { _Analytics.default.enable(); } _CountlyAnalytics.default.instance.enable( /* anonymous = */ true); } async postLoginSetup() { const cli = _MatrixClientPeg.MatrixClientPeg.get(); const cryptoEnabled = cli.isCryptoEnabled(); if (!cryptoEnabled) { this.onLoggedIn(); } const promisesList = [this.firstSyncPromise.promise]; if (cryptoEnabled) { // wait for the client to finish downloading cross-signing keys for us so we // know whether or not we have keys set up on this account promisesList.push(cli.downloadKeys([cli.getUserId()])); } // 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; } const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId()); if (crossSigningIsSetUp) { if (_Security.default.SHOW_ENCRYPTION_SETUP_UI === false) { this.onLoggedIn(); } else { this.setStateForNewView({ view: Views.COMPLETE_SECURITY }); } } else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) { this.setStateForNewView({ view: Views.E2E_SETUP }); } else { this.onLoggedIn(); } this.setState({ pendingInitialSync: false }); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage // eslint-disable-next-line camelcase UNSAFE_componentWillUpdate(props, state) { if (this.shouldTrackPageChange(this.state, state)) { this.startPageChangeTimer(); } } componentDidUpdate(prevProps, prevState) { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); _Analytics.default.trackPageChange(durationMs); _CountlyAnalytics.default.instance.trackPageChange(durationMs); } if (this.focusComposer) { _dispatcher.default.fire(_actions.Action.FocusComposer); this.focusComposer = false; } } componentWillUnmount() { Lifecycle.stopMatrixClient(); _dispatcher.default.unregister(this.dispatcherRef); this.themeWatcher.stop(); this.fontWatcher.stop(); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize); if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer); } getFallbackHsUrl() { if (this.props.serverConfig && this.props.serverConfig.isDefault) { return this.props.config.fallback_hs_url; } else { return null; } } getServerProperties() { let props = this.state.serverConfig; if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = _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()) { _dispatcher.default.dispatch({ action: 'start_registration' }); } else { _dispatcher.default.dispatch({ action: "view_welcome_page" }); } } else if (_SettingsStore.default.getValue("analyticsOptIn")) { _CountlyAnalytics.default.instance.enable( /* anonymous = */ false); } }); // 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 /*: IState*/ , state /*: IState*/ ) { return prevState.currentRoomId !== state.currentRoomId || prevState.view !== state.view || prevState.page_type !== state.page_type; } setStateForNewView(state /*: Partial<IState>*/ ) { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); } const newState = { currentUserId: null, justRegistered: false }; Object.assign(newState, state); this.setState(newState); } setPage(pageType /*: string*/ ) { this.setState({ page_type: pageType }); } async startRegistration(params /*: {[key: string]: string}*/ ) { const newState /*: Partial<IState>*/ = { view: Views.REGISTER }; // Only honour params if they are all present, otherwise we reset // HS and IS URLs when switching to registration. if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) { newState.serverConfig = await _AutoDiscoveryUtils.default.validateServerConfigWithStaticUrls(params.hs_url, params.is_url); newState.register_client_secret = params.client_secret; newState.register_session_id = params.session_id; newState.register_id_sid = params.sid; } this.setStateForNewView(newState); _ThemeController.default.isLogin = true; this.themeWatcher.recheck(); this.notifyNewScreen('register'); } // switch view to the given room // // @param {Object} roomInfo Object containing data about the room to be joined // @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given. // @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given. // @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member. // @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the // context of that particular event. // @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL // and alter the EventTile to appear highlighted. // @param {Object=} roomInfo.threepid_invite Object containing data about the third party // we received to join the room, if any. // @param {Object=} roomInfo.oob_data Object of additional data about the room // that has been passed out-of-band (eg. // room name and avatar from an invite email) viewRoom(roomInfo /*: IRoomInfo*/ ) { this.focusComposer = true; if (roomInfo.room_alias) { console.log(`Switching to room alias ${roomInfo.room_alias} at event ` + roomInfo.event_id); } else { console.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. let waitFor = Promise.resolve(null); if (!this.firstSyncComplete) { if (!this.firstSyncPromise) { console.warn('Cannot view a room before first sync. room_id:', roomInfo.room_id); return; } waitFor = this.firstSyncPromise.promise; } return waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { // Not all timeline events are decrypted ahead of time anymore // Only the critical ones for a typical UI are // This will start the decryption process for all events when a // user views a room room.decryptAllEvents(); const theAlias = Rooms.getDisplayAliasForRoom(room); if (theAlias) { presentedId = theAlias; // Store display alias of the presented room in cache to speed future // navigation. (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. if (localStorage) { 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 const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId; if (roomInfo.event_id && roomInfo.highlighted) { presentedId += "/" + roomInfo.event_id; } this.setState({ view: Views.LOGGED_IN, currentRoomId: roomInfo.room_id || null, page_type: _PageTypes.default.RoomView, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, ready: true, roomJustCreatedOpts: roomInfo.justCreatedOpts }, () => { this.notifyNewScreen('room/' + presentedId, replaceLast); }); }); } async viewGroup(payload) { const groupId = payload.group_id; // Wait for the first sync to complete if (!this.firstSyncComplete) { if (!this.firstSyncPromise) { console.warn('Cannot view a group before first sync. group_id:', groupId); return; } await this.firstSyncPromise.promise; } this.setState({ view: Views.LOGGED_IN, currentGroupId: groupId, currentGroupIsNew: payload.group_is_new }); this.setPage(_PageTypes.default.GroupView); this.notifyNewScreen('group/' + groupId); } viewSomethingBehindModal() { if (this.state.view !== Views.LOGGED_IN) { this.viewWelcome(); return; } if (!this.state.currentGroupId && !this.state.currentRoomId) { this.viewHome(); } } viewWelcome() { if ((0, _pages.shouldUseLoginForWelcome)(_SdkConfig.default.get())) { return this.viewLogin(); } this.setStateForNewView({ view: Views.WELCOME }); this.notifyNewScreen('welcome'); _ThemeController.default.isLogin = true; this.themeWatcher.recheck(); } viewLogin(otherState /*: any*/ ) { this.setStateForNewView(_objectSpread({ view: Views.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.LOGGED_IN, justRegistered }); this.setPage(_PageTypes.default.HomePage); this.notifyNewScreen('home'); _ThemeController.default.isLogin = false; this.themeWatcher.recheck(); } viewUser(userId /*: string*/ , subAction /*: string*/ ) { // 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) { const communityId = _CommunityPrototypeStore.CommunityPrototypeStore.instance.getSelectedCommunityId(); if (communityId) { // double check the user will have permission to associate this room with the community if (!_CommunityPrototypeStore.CommunityPrototypeStore.instance.isAdminOf(communityId)) { _Modal.default.createTrackedDialog('Pre-failure to create room', '', _ErrorDialog.default, { title: (0, _languageHandler._t)("Cannot create rooms in this community"), description: (0, _languageHandler._t)("You do not have permission to create rooms in this community.") }); return; } } const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); const modal = _Modal.default.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic }); const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { (0, _createRoom.default)(opts); } } chatCreateOrReuse(userId /*: string*/ ) { // Use a deferred action to reshow the dialog once the user has registered if (_MatrixClientPeg.MatrixClientPeg.get().isGuest()) { // No point in making 2 DMs with welcome bot. This assumes view_set_mxid will // result in a new DM with the welcome user. if (userId !== this.props.config.welcomeUserId) { _dispatcher.default.dispatch({ action: 'do_after_sync_prepared', deferred_action: { action: 'view_start_chat_or_reuse', user_id: userId } }); } _dispatcher.default.dispatch({ action: 'require_registration', // If the set_mxid dialog is cancelled, view /welcome because if the // browser was pointing at /user/@someone:domain?action=chat, the URL // needs to be reset so that they can revisit /user/.. // (and trigger // `_chatCreateOrReuse` again) go_welcome_on_cancel: true, screen_after: { screen: `user/${this.props.config.welcomeUserId}`, params: { action: 'chat' } } }); return; } // TODO: Immutable DMs replaces this const client = _MatrixClientPeg.MatrixClientPeg.get(); const dmRoomMap = new _DMRoomMap.default(client); const dmRooms = dmRoomMap.getDMRoomsForUserId(userId); if (dmRooms.length > 0) { _dispatcher.default.dispatch({ action: 'view_room', room_id: dmRooms[0] }); } else { _dispatcher.default.dispatch({ action: 'start_chat', user_id: userId }); } } leaveRoomWarnings(roomId /*: string*/ ) { const roomToLeave = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId); const isSpace = _SettingsStore.default.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. const warnings = []; const memberCount = roomToLeave.currentState.getJoinedMemberCount(); if (memberCount === 1) { warnings.push( /*#__PURE__*/_react.default.createElement("span", { className: "warning", key: "only_member_warning" }, ' ' /* Whitespace, otherwise the sentences get smashed together */ , (0, _languageHandler._t)("You are the only person here. " + "If you leave, no one will be able to join in the future, including you."))); return warnings; } const joinRules = roomToLeave.currentState.getStateEvents('m.room.join_rules', ''); if (joinRules) { const rule = joinRules.getContent().join_rule; if (rule !== "public") { warnings.push( /*#__PURE__*/_react.default.createElement("span", { className: "warning", key: "non_public_warning" }, ' ' /* Whitespace, otherwise the sentences get smashed together */ , isSpace ? (0, _languageHandler._t)("This space is not public. You will not be able to rejoin without an invite.") : (0, _languageHandler._t)("This room is not public. You will not be able to rejoin without an invite."))); } } return warnings; } leaveRoom(roomId /*: string*/ ) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const roomToLeave = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); const isSpace = _SettingsStore.default.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); _Modal.default.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { title: isSpace ? (0, _languageHandler._t)("Leave space") : (0, _languageHandler._t)("Leave room"), description: /*#__PURE__*/_react.default.createElement("span", null, isSpace ? (0, _languageHandler._t)("Are you sure you want to leave the space '%(spaceName)s'?", { spaceName: roomToLeave.name }) : (0, _languageHandler._t)("Are you sure you want to leave the room '%(roomName)s'?", { roomName: roomToLeave.name }), warnings), button: (0, _languageHandler._t)("Leave"), onFinished: shouldLeave => { if (shouldLeave) { const d = (0, _membership.leaveRoomBehaviour)(roomId); // FIXME: controller shouldn't be loading a view :( const Loader = sdk.getComponent("elements.Spinner"); const modal = _Modal.default.createDialog(Loader, null, 'mx_Dialog_spinner'); d.finally(() => modal.close()); _dispatcher.default.dispatch({ action: "after_leave_room", room_id: roomId }); } } }); } forgetRoom(roomId /*: string*/ ) { const room = _MatrixClientPeg.MatrixClientPeg.get().getRoom(roomId); _MatrixClientPeg.MatrixClientPeg.get().forget(roomId).then(() => { // Switch to home page if we're currently viewing the forgotten room if (this.state.currentRoomId === roomId) { _dispatcher.default.dispatch({ action: "view_home_page" }); } // We have to manually update the room list because the forgotten room will not // be notified to us, therefore the room list will have no other way of knowing // the room is forgotten. _RoomListStore.default.instance.manualRoomUpdate(room, _models.RoomUpdateCause.RoomRemoved); }).catch(err => { const errCode = err.errcode || (0, _languageHandler._td)("unknown error code"); _Modal.default.createTrackedDialog("Failed to forget room", '', _ErrorDialog.default, { title: (0, _languageHandler._t)("Failed to forget room %(errCode)s", { errCode }), description: err && err.message ? err.message : (0, _languageHandler._t)("Operation failed") }); }); } /** * Starts a chat with the welcome user, if the user doesn't already have one * @returns {string} The room ID of the new room, or null if no room was created */ async startWelcomeUserChat() { // We can end up with multiple tabs post-registration where the user // might then end up with a session and we don't want them all making // a chat with the welcome user: try to de-dupe. // We need to wait for the first sync to complete for this to // work though. let waitFor; if (!this.firstSyncComplete) { waitFor = this.firstSyncPromise.promise; } else { waitFor = Promise.resolve(); } await waitFor; const welcomeUserRooms = _DMRoomMap.default.shared().getDMRoomsForUserId(this.props.config.welcomeUserId); if (welcomeUserRooms.length === 0) { const roomId = await (0, _createRoom.default)({ dmUserId: this.props.config.welcomeUserId, // Only view the welcome user if we're NOT looking at a room andView: !this.state.currentRoomId, spinner: false // we're already showing one: we don't need another one }); // This is a bit of a hack, but since the deduplication relies // on m.direct being up to date, we need to force a sync // of the database, otherwise if the user goes to the other // tab before the next save happens (a few minutes), the // saved sync will be restored from the db and this code will // run without the update to m.direct, making another welcome // user room (it doesn't wait for new data from the server, just // the saved sync to be loaded). const saveWelcomeUser = ev => { if (ev.getType() === 'm.direct' && ev.getContent() && ev.getContent()[this.props.config.welcomeUserId]) { _MatrixClientPeg.MatrixClientPeg.get().store.save(true); _MatrixClientPeg.MatrixClientPeg.get().removeListener("accountData", saveWelcomeUser); } }; _MatrixClientPeg.MatrixClientPeg.get().on("accountData", saveWelcomeUser); return roomId; } return null; } /** * Called when a new logged in session has started */ async onLoggedIn() { _ThemeController.default.isLogin = false; this.themeWatcher.recheck(); this.setStateForNewView({ view: Views.LOGGED_IN }); // If a specific screen is set to be shown after login, show that above // all else, as it probably means the user clicked on something already. if (this.screenAfterLogin && this.screenAfterLogin.screen) { this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params); this.screenAfterLogin = null; } else if (_MatrixClientPeg.MatrixClientPeg.currentUserIsJustRegistered()) { _MatrixClientPeg.MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && (0, _languageHandler.getCurrentLanguage)().startsWith("en")) { const welcomeUserRoom = await this.startWelcomeUserChat(); if (welcomeUserRoom === null) { // We didn't redirect to the welcome user room, so show // the homepage. _dispatcher.default.dispatch({ action: 'view_home_page', justRegistered: true }); } } else if (_