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