diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 732865cb..00000000 --- a/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -src/matrix/storage/memory \ No newline at end of file diff --git a/.github/workflows/codechecks.js.yml b/.github/workflows/codechecks.js.yml index 2e15c8e7..6ad2f42e 100644 --- a/.github/workflows/codechecks.js.yml +++ b/.github/workflows/codechecks.js.yml @@ -42,4 +42,6 @@ jobs: - name: Unit tests run: yarn test - name: Lint - run: yarn run lint-ci \ No newline at end of file + run: yarn run lint-ci + - name: Typescript + run: yarn run tsc diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js new file mode 100644 index 00000000..1974e07b --- /dev/null +++ b/.ts-eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + root: true, + env: { + "browser": true, + "es6": true + }, + extends: [ + // "plugin:@typescript-eslint/recommended", + // "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + parser: '@typescript-eslint/parser', + parserOptions: { + "ecmaVersion": 2020, + "sourceType": "module", + "project": "./tsconfig.json" + }, + plugins: [ + '@typescript-eslint', + ], + rules: { + "@typescript-eslint/no-floating-promises": 2, + "@typescript-eslint/no-misused-promises": 2 + } +}; diff --git a/package.json b/package.json index ad1e7489..c7e0cd9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydrogen-web", - "version": "0.2.5", + "version": "0.2.7", "description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB", "main": "index.js", "directories": { @@ -8,6 +8,7 @@ }, "scripts": { "lint": "eslint --cache src/", + "lint-ts": "eslint src/ -c .ts-eslintrc.js --ext .ts", "lint-ci": "eslint src/", "test": "impunity --entry-point src/main.js --force-esm-dirs lib/ src/", "start": "snowpack dev --port 3000", @@ -32,11 +33,13 @@ "@rollup/plugin-json": "^4.1.0", "@rollup/plugin-multi-entry": "^4.0.0", "@rollup/plugin-node-resolve": "^9.0.0", + "@typescript-eslint/eslint-plugin": "^4.29.2", + "@typescript-eslint/parser": "^4.29.2", "autoprefixer": "^10.2.6", "cheerio": "^1.0.0-rc.3", "commander": "^6.0.0", "core-js": "^3.6.5", - "eslint": "^7.25.0", + "eslint": "^7.32.0", "fake-indexeddb": "^3.1.2", "finalhandler": "^1.1.1", "impunity": "^1.0.1", diff --git a/scripts/build.mjs b/scripts/build.mjs index 82235cc4..0674cb93 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -114,8 +114,8 @@ async function build({modernOnly, overrideImports, overrideCss}) { await buildManifest(assets); // all assets have been added, create a hash from all assets name to cache unhashed files like index.html assets.addToHashForAll("index.html", devHtml); - let swSource = await fs.readFile(path.join(snowpackOutPath, "service-worker.js"), "utf8"); - assets.addToHashForAll("service-worker.js", swSource); + let swSource = await fs.readFile(path.join(snowpackOutPath, "sw.js"), "utf8"); + assets.addToHashForAll("sw.js", swSource); const globalHash = assets.hashForAll(); @@ -174,7 +174,7 @@ async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, asset const configJSON = JSON.stringify(Object.assign({}, baseConfig, { worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null, downloadSandbox: assets.resolve("download-sandbox.html"), - serviceWorker: "service-worker.js", + serviceWorker: "sw.js", olm: { wasm: assets.resolve("olm.wasm"), legacyBundle: assets.resolve("olm_legacy.js"), @@ -342,7 +342,7 @@ async function buildServiceWorker(swSource, version, globalHash, assets) { swSource = replaceStringInSource("NOTIFICATION_BADGE_ICON", assets.resolve("icon.png")); // service worker should not have a hashed name as it is polled by the browser for updates - await assets.writeUnhashed("service-worker.js", swSource); + await assets.writeUnhashed("sw.js", swSource); } async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) { diff --git a/snowpack.config.js b/snowpack.config.js index 3de1d723..68f33242 100644 --- a/snowpack.config.js +++ b/snowpack.config.js @@ -17,7 +17,6 @@ module.exports = { '**/scripts/**', '**/target/**', '**/prototypes/**', - '**/src/matrix/storage/memory/**', '**/src/platform/web/legacy-polyfill.js', '**/src/platform/web/worker/polyfill.js' ], diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js deleted file mode 100644 index 1b52e1a5..00000000 --- a/src/domain/LoginViewModel.js +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {ViewModel} from "./ViewModel.js"; -import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; - -export class LoginViewModel extends ViewModel { - constructor(options) { - super(options); - const {ready, defaultHomeServer, createSessionContainer} = options; - this._createSessionContainer = createSessionContainer; - this._ready = ready; - this._defaultHomeServer = defaultHomeServer; - this._sessionContainer = null; - this._loadViewModel = null; - this._loadViewModelSubscription = null; - } - - get defaultHomeServer() { return this._defaultHomeServer; } - - get loadViewModel() {return this._loadViewModel; } - - get isBusy() { - if (!this._loadViewModel) { - return false; - } else { - return this._loadViewModel.loading; - } - } - - async login(username, password, homeserver) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - if (this._loadViewModel) { - this._loadViewModel = this.disposeTracked(this._loadViewModel); - } - this._loadViewModel = this.track(new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: () => { - this._sessionContainer = this._createSessionContainer(); - this._sessionContainer.startWithLogin(homeserver, username, password); - return this._sessionContainer; - }, - ready: sessionContainer => { - // make sure we don't delete the session in dispose when navigating away - this._sessionContainer = null; - this._ready(sessionContainer); - }, - homeserver, - }))); - this._loadViewModel.start(); - this.emitChange("loadViewModel"); - this._loadViewModelSubscription = this.track(this._loadViewModel.disposableOn("change", () => { - if (!this._loadViewModel.loading) { - this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); - } - this.emitChange("isBusy"); - })); - } - - get cancelUrl() { - return this.urlCreator.urlForSegment("session"); - } - - dispose() { - super.dispose(); - if (this._sessionContainer) { - // if we move away before we're done with initial sync - // delete the session - this._sessionContainer.deleteSession(); - } - } -} diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index fca8d779..d9949c77 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -import {LoginViewModel} from "./LoginViewModel.js"; +import {LoginViewModel} from "./login/LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel.js"; @@ -35,12 +35,14 @@ export class RootViewModel extends ViewModel { async load() { this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation())); this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation())); + this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation())); this._applyNavigation(true); } async _applyNavigation(shouldRestoreLastUrl) { - const isLogin = this.navigation.observe("login").get(); - const sessionId = this.navigation.observe("session").get(); + const isLogin = this.navigation.path.get("login") + const sessionId = this.navigation.path.get("session")?.value; + const loginToken = this.navigation.path.get("sso")?.value; if (isLogin) { if (this.activeSection !== "login") { this._showLogin(); @@ -65,7 +67,13 @@ export class RootViewModel extends ViewModel { this._showSessionLoader(sessionId); } } - } else { + } else if (loginToken) { + this.urlCreator.normalizeUrl(); + if (this.activeSection !== "login") { + this._showLogin(loginToken); + } + } + else { try { if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) { const sessionInfos = await this.platform.sessionInfoStorage.getAll(); @@ -94,10 +102,10 @@ export class RootViewModel extends ViewModel { } } - _showLogin() { + _showLogin(loginToken) { this._setSection(() => { this._loginViewModel = new LoginViewModel(this.childOptions({ - defaultHomeServer: this.platform.config["defaultHomeServer"], + defaultHomeserver: this.platform.config["defaultHomeServer"], createSessionContainer: this._createSessionContainer, ready: sessionContainer => { // we don't want to load the session container again, @@ -111,6 +119,7 @@ export class RootViewModel extends ViewModel { this._pendingSessionContainer = sessionContainer; this.navigation.push("session", sessionContainer.sessionId); }, + loginToken })); }); } @@ -123,13 +132,11 @@ export class RootViewModel extends ViewModel { } _showSessionLoader(sessionId) { + const sessionContainer = this._createSessionContainer(); + sessionContainer.startWithExistingSession(sessionId); this._setSection(() => { this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({ - createAndStartSessionContainer: () => { - const sessionContainer = this._createSessionContainer(); - sessionContainer.startWithExistingSession(sessionId); - return sessionContainer; - }, + sessionContainer, ready: sessionContainer => this._showSession(sessionContainer) })); this._sessionLoadViewModel.start(); diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 0b785e47..10cbb851 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; +import {LoadStatus} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; import {ViewModel} from "./ViewModel.js"; export class SessionLoadViewModel extends ViewModel { constructor(options) { super(options); - const {createAndStartSessionContainer, ready, homeserver, deleteSessionOnCancel} = options; - this._createAndStartSessionContainer = createAndStartSessionContainer; + const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options; + this._sessionContainer = sessionContainer; this._ready = ready; this._homeserver = homeserver; this._deleteSessionOnCancel = deleteSessionOnCancel; @@ -38,7 +38,6 @@ export class SessionLoadViewModel extends ViewModel { try { this._loading = true; this.emitChange("loading"); - this._sessionContainer = this._createAndStartSessionContainer(); this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { this.emitChange("loadLabel"); // wait for initial sync, but not catchup sync @@ -109,22 +108,9 @@ export class SessionLoadViewModel extends ViewModel { return `Something went wrong: ${error && error.message}.`; } + // Statuses related to login are handled by respective login view models if (sc) { switch (sc.loadStatus.get()) { - case LoadStatus.NotLoading: - return `Preparing…`; - case LoadStatus.Login: - return `Checking your login and password…`; - case LoadStatus.LoginFailed: - switch (sc.loginFailure) { - case LoginFailure.LoginFailure: - return `Your username and/or password don't seem to be correct.`; - case LoginFailure.Connection: - return `Can't connect to ${this._homeserver}.`; - case LoginFailure.Unknown: - return `Something went wrong while checking your login and password.`; - } - break; case LoadStatus.SessionSetup: return `Setting up your encryption keys…`; case LoadStatus.Loading: diff --git a/src/domain/login/CompleteSSOLoginViewModel.js b/src/domain/login/CompleteSSOLoginViewModel.js new file mode 100644 index 00000000..7821313a --- /dev/null +++ b/src/domain/login/CompleteSSOLoginViewModel.js @@ -0,0 +1,76 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {LoginFailure} from "../../matrix/SessionContainer.js"; + +export class CompleteSSOLoginViewModel extends ViewModel { + constructor(options) { + super(options); + const { + loginToken, + sessionContainer, + attemptLogin, + } = options; + this._loginToken = loginToken; + this._sessionContainer = sessionContainer; + this._attemptLogin = attemptLogin; + this._errorMessage = ""; + this.performSSOLoginCompletion(); + } + + get errorMessage() { return this._errorMessage; } + + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); + } + + async performSSOLoginCompletion() { + if (!this._loginToken) { + return; + } + const homeserver = await this.platform.settingsStorage.getString("sso_ongoing_login_homeserver"); + let loginOptions; + try { + loginOptions = await this._sessionContainer.queryLogin(homeserver).result; + } + catch (err) { + this._showError(err.message); + return; + } + if (!loginOptions.token) { + this.navigation.push("session"); + return; + } + const status = await this._attemptLogin(loginOptions.token(this._loginToken)); + let error = ""; + switch (status) { + case LoginFailure.Credentials: + error = this.i18n`Your login token is invalid.`; + break; + case LoginFailure.Connection: + error = this.i18n`Can't connect to ${homeserver}.`; + break; + case LoginFailure.Unknown: + error = this.i18n`Something went wrong while checking your login token.`; + break; + } + if (error) { + this._showError(error); + } + } +} diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.js new file mode 100644 index 00000000..9cdf9290 --- /dev/null +++ b/src/domain/login/LoginViewModel.js @@ -0,0 +1,239 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; +import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; +import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; +import {LoadStatus} from "../../matrix/SessionContainer.js"; +import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; + +export class LoginViewModel extends ViewModel { + constructor(options) { + super(options); + const {ready, defaultHomeserver, createSessionContainer, loginToken} = options; + this._createSessionContainer = createSessionContainer; + this._ready = ready; + this._loginToken = loginToken; + this._sessionContainer = this._createSessionContainer(); + this._loginOptions = null; + this._passwordLoginViewModel = null; + this._startSSOLoginViewModel = null; + this._completeSSOLoginViewModel = null; + this._loadViewModel = null; + this._loadViewModelSubscription = null; + this._homeserver = defaultHomeserver; + this._queriedHomeserver = null; + this._errorMessage = ""; + this._hideHomeserver = false; + this._isBusy = false; + this._abortHomeserverQueryTimeout = null; + this._abortQueryOperation = null; + this._initViewModels(); + } + + get passwordLoginViewModel() { return this._passwordLoginViewModel; } + get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } + get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } + get homeserver() { return this._homeserver; } + get resolvedHomeserver() { return this._loginOptions?.homeserver; } + get errorMessage() { return this._errorMessage; } + get showHomeserver() { return !this._hideHomeserver; } + get loadViewModel() {return this._loadViewModel; } + get isBusy() { return this._isBusy; } + get isFetchingLoginOptions() { return !!this._abortQueryOperation; } + + goBack() { + this.navigation.push("session"); + } + + async _initViewModels() { + if (this._loginToken) { + this._hideHomeserver = true; + this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( + this.childOptions( + { + sessionContainer: this._sessionContainer, + attemptLogin: loginMethod => this.attemptLogin(loginMethod), + loginToken: this._loginToken + }))); + this.emitChange("completeSSOLoginViewModel"); + } + else { + await this.queryHomeserver(); + } + } + + _showPasswordLogin() { + this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( + this.childOptions({ + loginOptions: this._loginOptions, + attemptLogin: loginMethod => this.attemptLogin(loginMethod) + }))); + this.emitChange("passwordLoginViewModel"); + } + + _showSSOLogin() { + this._startSSOLoginViewModel = this.track( + new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) + ); + this.emitChange("startSSOLoginViewModel"); + } + + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); + } + + _setBusy(status) { + this._isBusy = status; + this._passwordLoginViewModel?.setBusy(status); + this._startSSOLoginViewModel?.setBusy(status); + this.emitChange("isBusy"); + } + + async attemptLogin(loginMethod) { + this._setBusy(true); + this._sessionContainer.startWithLogin(loginMethod); + const loadStatus = this._sessionContainer.loadStatus; + const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); + await handle.promise; + this._setBusy(false); + const status = loadStatus.get(); + if (status === LoadStatus.LoginFailed) { + return this._sessionContainer.loginFailure; + } + this._hideHomeserver = true; + this.emitChange("hideHomeserver"); + this._disposeViewModels(); + this._createLoadViewModel(); + return null; + } + + _createLoadViewModel() { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + this._loadViewModel = this.disposeTracked(this._loadViewModel); + this._loadViewModel = this.track( + new SessionLoadViewModel( + this.childOptions({ + ready: (sessionContainer) => { + // make sure we don't delete the session in dispose when navigating away + this._sessionContainer = null; + this._ready(sessionContainer); + }, + sessionContainer: this._sessionContainer, + homeserver: this._homeserver + }) + ) + ); + this._loadViewModel.start(); + this.emitChange("loadViewModel"); + this._loadViewModelSubscription = this.track( + this._loadViewModel.disposableOn("change", () => { + if (!this._loadViewModel.loading) { + this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); + } + this._setBusy(false); + }) + ); + } + + _disposeViewModels() { + this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); + this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); + this.emitChange("disposeViewModels"); + } + + async setHomeserver(newHomeserver) { + this._homeserver = newHomeserver; + // clear everything set by queryHomeserver + this._loginOptions = null; + this._queriedHomeserver = null; + this._showError(""); + this._disposeViewModels(); + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); + this.emitChange(); // multiple fields changing + // also clear the timeout if it is still running + this.disposeTracked(this._abortHomeserverQueryTimeout); + const timeout = this.clock.createTimeout(1000); + this._abortHomeserverQueryTimeout = this.track(() => timeout.abort()); + try { + await timeout.elapsed(); + } catch (err) { + if (err.name === "AbortError") { + return; // still typing, don't query + } else { + throw err; + } + } + this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); + this.queryHomeserver(); + } + + async queryHomeserver() { + // don't repeat a query we've just done + if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { + return; + } + this._queriedHomeserver = this._homeserver; + // given that setHomeserver already clears everything set here, + // and that is the only way to change the homeserver, + // we don't need to reset things again here. + // However, clear things set by setHomeserver: + // if query is called before the typing timeout hits (e.g. field lost focus), + // cancel the timeout so we don't query again. + this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); + // cancel ongoing query operation, if any + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); + try { + const queryOperation = this._sessionContainer.queryLogin(this._homeserver); + this._abortQueryOperation = this.track(() => queryOperation.abort()); + this.emitChange("isFetchingLoginOptions"); + this._loginOptions = await queryOperation.result; + this.emitChange("resolvedHomeserver"); + } + catch (e) { + if (e.name === "AbortError") { + return; //aborted, bail out + } else { + this._loginOptions = null; + } + } finally { + this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); + this.emitChange("isFetchingLoginOptions"); + } + if (this._loginOptions) { + if (this._loginOptions.sso) { this._showSSOLogin(); } + if (this._loginOptions.password) { this._showPasswordLogin(); } + if (!this._loginOptions.sso && !this._loginOptions.password) { + this._showError("This homeserver supports neither SSO nor password based login flows"); + } + } + else { + this._showError(`Could not query login methods supported by ${this.homeserver}`); + } + } + + dispose() { + super.dispose(); + if (this._sessionContainer) { + // if we move away before we're done with initial sync + // delete the session + this._sessionContainer.deleteSession(); + } + } +} diff --git a/src/domain/login/PasswordLoginViewModel.js b/src/domain/login/PasswordLoginViewModel.js new file mode 100644 index 00000000..7ea239e6 --- /dev/null +++ b/src/domain/login/PasswordLoginViewModel.js @@ -0,0 +1,63 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; +import {LoginFailure} from "../../matrix/SessionContainer.js"; + +export class PasswordLoginViewModel extends ViewModel { + constructor(options) { + super(options); + const {loginOptions, attemptLogin} = options; + this._loginOptions = loginOptions; + this._attemptLogin = attemptLogin; + this._isBusy = false; + this._errorMessage = ""; + } + + get isBusy() { return this._isBusy; } + get errorMessage() { return this._errorMessage; } + + setBusy(status) { + this._isBusy = status; + this.emitChange("isBusy"); + } + + _showError(message) { + this._errorMessage = message; + this.emitChange("errorMessage"); + } + + async login(username, password) { + this._errorMessage = ""; + this.emitChange("errorMessage"); + const status = await this._attemptLogin(this._loginOptions.password(username, password)); + let error = ""; + switch (status) { + case LoginFailure.Credentials: + error = this.i18n`Your username and/or password don't seem to be correct.`; + break; + case LoginFailure.Connection: + error = this.i18n`Can't connect to ${this._loginOptions.homeserver}.`; + break; + case LoginFailure.Unknown: + error = this.i18n`Something went wrong while checking your login and password.`; + break; + } + if (error) { + this._showError(error); + } + } +} diff --git a/src/domain/login/StartSSOLoginViewModel.js b/src/domain/login/StartSSOLoginViewModel.js new file mode 100644 index 00000000..54218d22 --- /dev/null +++ b/src/domain/login/StartSSOLoginViewModel.js @@ -0,0 +1,38 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "../ViewModel.js"; + +export class StartSSOLoginViewModel extends ViewModel{ + constructor(options) { + super(options); + this._sso = options.loginOptions.sso; + this._isBusy = false; + } + + get isBusy() { return this._isBusy; } + + setBusy(status) { + this._isBusy = status; + this.emitChange("isBusy"); + } + + async startSSOLogin() { + await this.platform.settingsStorage.setString("sso_ongoing_login_homeserver", this._sso.homeserver); + const link = this._sso.createSSORedirectURL(this.urlCreator.createSSOCallbackURL()); + this.platform.openUrl(link); + } +} diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index 28488129..586eec8a 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -120,4 +120,14 @@ export class URLRouter { const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`; return this._history.pathAsUrl(urlPath); } + + createSSOCallbackURL() { + return window.location.origin; + } + + normalizeUrl() { + // Remove any queryParameters from the URL + // Gets rid of the loginToken after SSO + this._history.replaceUrlSilently(`${window.location.origin}/${window.location.hash}`); + } } diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index dbac16ac..d21bcad4 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -30,7 +30,7 @@ function allowsChild(parent, child) { switch (parent?.type) { case undefined: // allowed root segments - return type === "login" || type === "session"; + return type === "login" || type === "session" || type === "sso"; case "session": return type === "room" || type === "rooms" || type === "settings"; case "rooms": @@ -152,6 +152,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { const userId = iterator.next().value; if (!userId) { break; } pushRightPanelSegment(segments, type, userId); + } else if (type.includes("loginToken")) { + // Special case for SSO-login with query parameter loginToken= + const loginToken = type.split("=").pop(); + segments.push(new Segment("sso", loginToken)); } else { // might be undefined, which will be turned into true by Segment const value = iterator.next().value; @@ -181,7 +185,8 @@ export function stringifyPath(path) { } break; case "right-panel": - // Ignore right-panel in url + case "sso": + // Do not put these segments in URL continue; default: urlPath += `/${segment.type}`; @@ -228,6 +233,12 @@ export function tests() { const urlPath = stringifyPath(path); assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); }, + "Parse loginToken query parameter into SSO segment": assert => { + const segments = parseUrlPath("?loginToken=a1232aSD123"); + assert.equal(segments.length, 1); + assert.equal(segments[0].type, "sso"); + assert.equal(segments[0].value, "a1232aSD123"); + }, "parse grid url path with focused empty tile": assert => { const segments = parseUrlPath("/session/1/rooms/a,b,c/3"); assert.equal(segments.length, 3); diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 0acb2859..10062af2 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -219,7 +219,7 @@ export class TilesCollection extends BaseObservableList { } } - onMove(fromIdx, toIdx, value) { + onMove(/*fromIdx, toIdx, value*/) { // this ... cannot happen in the timeline? // perhaps we can use this event to support a local echo (in a different fragment) // to be moved to the key of the remote echo, so we don't loose state ... ? diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 7b2765f5..a08ab060 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -59,7 +59,7 @@ export class TimelineViewModel extends ViewModel { } } - unloadAtTop(tileAmount) { + unloadAtTop(/*tileAmount*/) { // get lowerSortKey for tile at index tileAmount - 1 // tell timeline to unload till there (included given key) } @@ -68,7 +68,7 @@ export class TimelineViewModel extends ViewModel { } - unloadAtBottom(tileAmount) { + unloadAtBottom(/*tileAmount*/) { // get upperSortKey for tile at index tiles.length - tileAmount // tell timeline to unload till there (included given key) } diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 68037a14..3c370b72 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -101,7 +101,7 @@ export class SimpleTile extends ViewModel { // return whether the tile should be removed // as SimpleTile only has one entry, the tile should be removed - removeEntry(entry) { + removeEntry(/*entry*/) { return true; } @@ -110,12 +110,12 @@ export class SimpleTile extends ViewModel { return false; } // let item know it has a new sibling - updatePreviousSibling(prev) { + updatePreviousSibling(/*prev*/) { } // let item know it has a new sibling - updateNextSibling(next) { + updateNextSibling(/*next*/) { } diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js index 221e39da..7120f5fb 100644 --- a/src/domain/session/settings/SettingsViewModel.js +++ b/src/domain/session/settings/SettingsViewModel.js @@ -151,7 +151,7 @@ export class SettingsViewModel extends ViewModel { this.pushNotifications.enabledOnServer = null; this.pushNotifications.serverError = null; try { - this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeServer(); + this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeserver(); this.emitChange("pushNotifications.enabledOnServer"); } catch (err) { this.pushNotifications.serverError = err; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 5a046117..63aece42 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -46,7 +46,7 @@ const PICKLE_KEY = "DEFAULT_KEY"; const PUSHER_KEY = "pusher"; export class Session { - // sessionInfo contains deviceId, userId and homeServer + // sessionInfo contains deviceId, userId and homeserver constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) { this._platform = platform; this._storage = storage; @@ -636,7 +636,7 @@ export class Session { return !!pusherData; } - async checkPusherEnabledOnHomeServer() { + async checkPusherEnabledOnHomeserver() { const readTxn = await this._storage.readTxn([this._storage.storeNames.session]); const pusherData = await readTxn.session.get(PUSHER_KEY); if (!pusherData) { diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 07c4a870..f375fdd7 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,5 +1,6 @@ /* Copyright 2020 Bruno Windels +Copyright 2020, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +16,8 @@ limitations under the License. */ import {createEnum} from "../utils/enum.js"; +import {lookupHomeserver} from "./well-known.js"; +import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue.js"; import {HomeServerApi} from "./net/HomeServerApi.js"; import {Reconnector, ConnectionStatus} from "./net/Reconnector.js"; @@ -23,6 +26,9 @@ import {MediaRepository} from "./net/MediaRepository.js"; import {RequestScheduler} from "./net/RequestScheduler.js"; import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; +import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js"; +import {TokenLoginMethod} from "./login/TokenLoginMethod.js"; +import {SSOLoginHelper} from "./login/SSOLoginHelper.js"; export const LoadStatus = createEnum( "NotLoading", @@ -42,14 +48,6 @@ export const LoginFailure = createEnum( "Unknown", ); -function normalizeHomeserver(homeServer) { - try { - return new URL(homeServer).origin; - } catch (err) { - return new URL(`https://${homeServer}`).origin; - } -} - export class SessionContainer { constructor({platform, olmPromise, workerPromise}) { this._platform = platform; @@ -97,25 +95,61 @@ export class SessionContainer { }); } - async startWithLogin(homeServer, username, password) { - if (this._status.get() !== LoadStatus.NotLoading) { + _parseLoginOptions(options, homeserver) { + /* + Take server response and return new object which has two props password and sso which + implements LoginMethod + */ + const flows = options.flows; + const result = {homeserver}; + for (const flow of flows) { + if (flow.type === "m.login.password") { + result.password = (username, password) => new PasswordLoginMethod({homeserver, username, password}); + } + else if (flow.type === "m.login.sso" && flows.find(flow => flow.type === "m.login.token")) { + result.sso = new SSOLoginHelper(homeserver); + } + else if (flow.type === "m.login.token") { + result.token = loginToken => new TokenLoginMethod({homeserver, loginToken}); + } + } + return result; + } + + queryLogin(homeserver) { + return new AbortableOperation(async setAbortable => { + homeserver = await lookupHomeserver(homeserver, (url, options) => { + return setAbortable(this._platform.request(url, options)); + }); + const hsApi = new HomeServerApi({homeserver, request: this._platform.request}); + const response = await setAbortable(hsApi.getLoginFlows()).response(); + return this._parseLoginOptions(response, homeserver); + }); + } + + async startWithLogin(loginMethod) { + const currentStatus = this._status.get(); + if (currentStatus !== LoadStatus.LoginFailed && + currentStatus !== LoadStatus.NotLoading && + currentStatus !== LoadStatus.Error) { return; } + this._resetStatus(); await this._platform.logger.run("login", async log => { this._status.set(LoadStatus.Login); - homeServer = normalizeHomeserver(homeServer); const clock = this._platform.clock; let sessionInfo; try { const request = this._platform.request; - const hsApi = new HomeServerApi({homeServer, request}); - const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response(); + const hsApi = new HomeServerApi({homeserver: loginMethod.homeserver, request}); + const loginData = await loginMethod.login(hsApi, "Hydrogen", log); const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, deviceId: loginData.device_id, userId: loginData.user_id, - homeServer: homeServer, + homeServer: loginMethod.homeserver, // deprecate this over time + homeserver: loginMethod.homeserver, accessToken: loginData.access_token, lastUsed: clock.now() }; @@ -164,7 +198,7 @@ export class SessionContainer { createMeasure: clock.createMeasure }); const hsApi = new HomeServerApi({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, accessToken: sessionInfo.accessToken, request: this._platform.request, reconnector: this._reconnector, @@ -176,7 +210,7 @@ export class SessionContainer { id: sessionInfo.id, deviceId: sessionInfo.deviceId, userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, }; const olm = await this._olmPromise; let olmWorker = null; @@ -186,7 +220,7 @@ export class SessionContainer { this._requestScheduler = new RequestScheduler({hsApi, clock}); this._requestScheduler.start(); const mediaRepository = new MediaRepository({ - homeServer: sessionInfo.homeServer, + homeserver: sessionInfo.homeServer, platform: this._platform, }); this._session = new Session({ @@ -270,6 +304,10 @@ export class SessionContainer { return this._error; } + get loginFailure() { + return this._loginFailure; + } + /** only set at loadStatus InitialSync, CatchupSync or Ready */ get sync() { return this._sync; @@ -319,4 +357,10 @@ export class SessionContainer { this._sessionId = null; } } + + _resetStatus() { + this._status.set(LoadStatus.NotLoading); + this._error = null; + this._loginFailure = null; + } } diff --git a/src/matrix/login/LoginMethod.js b/src/matrix/login/LoginMethod.js new file mode 100644 index 00000000..ece18871 --- /dev/null +++ b/src/matrix/login/LoginMethod.js @@ -0,0 +1,30 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export class LoginMethod { + constructor({homeserver}) { + this.homeserver = homeserver; + } + + // eslint-disable-next-line no-unused-vars + async login(hsApi, deviceName, log) { + /* + Regardless of the login method, SessionContainer.startWithLogin() + can do SomeLoginMethod.login() + */ + throw("Not Implemented"); + } +} diff --git a/src/matrix/login/PasswordLoginMethod.js b/src/matrix/login/PasswordLoginMethod.js new file mode 100644 index 00000000..5c90ccf8 --- /dev/null +++ b/src/matrix/login/PasswordLoginMethod.js @@ -0,0 +1,29 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {LoginMethod} from "./LoginMethod.js"; + +export class PasswordLoginMethod extends LoginMethod { + constructor(options) { + super(options); + this.username = options.username; + this.password = options.password; + } + + async login(hsApi, deviceName, log) { + return await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response(); + } +} diff --git a/src/matrix/login/SSOLoginHelper.js b/src/matrix/login/SSOLoginHelper.js new file mode 100644 index 00000000..a15c8ef9 --- /dev/null +++ b/src/matrix/login/SSOLoginHelper.js @@ -0,0 +1,27 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export class SSOLoginHelper{ + constructor(homeserver) { + this._homeserver = homeserver; + } + + get homeserver() { return this._homeserver; } + + createSSORedirectURL(returnURL) { + return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`; + } +} diff --git a/src/matrix/login/TokenLoginMethod.js b/src/matrix/login/TokenLoginMethod.js new file mode 100644 index 00000000..e55cedcf --- /dev/null +++ b/src/matrix/login/TokenLoginMethod.js @@ -0,0 +1,29 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {LoginMethod} from "./LoginMethod.js"; +import {makeTxnId} from "../common.js"; + +export class TokenLoginMethod extends LoginMethod { + constructor(options) { + super(options); + this._loginToken = options.loginToken; + } + + async login(hsApi, deviceName, log) { + return await hsApi.tokenLogin(this._loginToken, makeTxnId(), deviceName, {log}).response(); + } +} diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index a9b63f8e..4b53b28b 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -19,10 +19,10 @@ import {encodeQueryParams, encodeBody} from "./common.js"; import {HomeServerRequest} from "./HomeServerRequest.js"; export class HomeServerApi { - constructor({homeServer, accessToken, request, reconnector}) { + constructor({homeserver, accessToken, request, reconnector}) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write - this._homeserver = homeServer; + this._homeserver = homeserver; this._accessToken = accessToken; this._requestFn = request; this._reconnector = reconnector; @@ -134,6 +134,10 @@ export class HomeServerApi { return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); } + getLoginFlows() { + return this._unauthedRequest("GET", this._url("/login"), null, null, null); + } + passwordLogin(username, password, initialDeviceDisplayName, options = null) { return this._unauthedRequest("POST", this._url("/login"), null, { "type": "m.login.password", @@ -146,6 +150,18 @@ export class HomeServerApi { }, options); } + tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { + return this._unauthedRequest("POST", this._url("/login"), null, { + "type": "m.login.token", + "identifier": { + "type": "m.id.user", + }, + "token": loginToken, + "txn_id": txnId, + "initial_device_display_name": initialDeviceDisplayName + }, options); + } + createFilter(userId, filter, options = null) { return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); } @@ -218,7 +234,7 @@ export function tests() { "superficial happy path for GET": async assert => { const hsApi = new HomeServerApi({ request: () => new MockRequest().respond(200, 42), - homeServer: "https://hs.tld" + homeserver: "https://hs.tld" }); const result = await hsApi._get("foo", null, null, null).response(); assert.strictEqual(result, 42); diff --git a/src/matrix/net/MediaRepository.js b/src/matrix/net/MediaRepository.js index f7e47cfd..3f718c85 100644 --- a/src/matrix/net/MediaRepository.js +++ b/src/matrix/net/MediaRepository.js @@ -18,8 +18,8 @@ import {encodeQueryParams} from "./common.js"; import {decryptAttachment} from "../e2ee/attachment.js"; export class MediaRepository { - constructor({homeServer, platform}) { - this._homeServer = homeServer; + constructor({homeserver, platform}) { + this._homeserver = homeserver; this._platform = platform; } @@ -27,7 +27,7 @@ export class MediaRepository { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; - const httpUrl = `${this._homeServer}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; return httpUrl + "?" + encodeQueryParams({width: Math.round(width), height: Math.round(height), method}); } return null; @@ -37,7 +37,7 @@ export class MediaRepository { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; - return `${this._homeServer}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; + return `${this._homeserver}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`; } else { return null; } diff --git a/src/matrix/room/timeline/entries/BaseEntry.ts b/src/matrix/room/timeline/entries/BaseEntry.ts index c592ecb6..1ef6a863 100644 --- a/src/matrix/room/timeline/entries/BaseEntry.ts +++ b/src/matrix/room/timeline/entries/BaseEntry.ts @@ -22,20 +22,15 @@ interface FragmentIdComparer { compare: (a: number, b: number) => number } -export class BaseEntry { - protected _fragmentIdComparer: FragmentIdComparer - - constructor(fragmentIdComparer: FragmentIdComparer) { - this._fragmentIdComparer = fragmentIdComparer; +export abstract class BaseEntry { + constructor( + protected readonly _fragmentIdComparer: FragmentIdComparer + ) { } - get fragmentId(): number { - throw new Error("unimplemented"); - } - - get entryIndex(): number { - throw new Error("unimplemented"); - } + abstract get fragmentId(): number; + abstract get entryIndex(): number; + abstract updateFrom(other: BaseEntry): void; compare(otherEntry: BaseEntry): number { if (this.fragmentId === otherEntry.fragmentId) { @@ -53,6 +48,4 @@ export class BaseEntry { asEventKey(): EventKey { return new EventKey(this.fragmentId, this.entryIndex); } - - updateFrom(other: BaseEntry) {} } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 55219cd6..89d3f379 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -33,7 +33,6 @@ export class EventEntry extends BaseEventEntry { } updateFrom(other) { - super.updateFrom(other); if (other._decryptionResult && !this._decryptionResult) { this._decryptionResult = other._decryptionResult; } diff --git a/src/matrix/room/timeline/persistence/RelationWriter.js b/src/matrix/room/timeline/persistence/RelationWriter.js index 4944cc64..afc99e6d 100644 --- a/src/matrix/room/timeline/persistence/RelationWriter.js +++ b/src/matrix/room/timeline/persistence/RelationWriter.js @@ -147,7 +147,7 @@ export class RelationWriter { return true; } - _aggregateAnnotation(annotationEvent, targetStorageEntry, log) { + _aggregateAnnotation(annotationEvent, targetStorageEntry/*, log*/) { // TODO: do we want to verify it is a m.reaction event somehow? const relation = getRelation(annotationEvent); if (!relation) { diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js deleted file mode 100644 index a76c16f3..00000000 --- a/src/matrix/storage/memory/Storage.js +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {Transaction} from "./Transaction.js"; -import { STORE_MAP, STORE_NAMES } from "../common"; - -export class Storage { - constructor(initialStoreValues = {}) { - this._validateStoreNames(Object.keys(initialStoreValues)); - this.storeNames = STORE_MAP; - this._storeValues = STORE_NAMES.reduce((values, name) => { - values[name] = initialStoreValues[name] || null; - }, {}); - } - - _validateStoreNames(storeNames) { - const idx = storeNames.findIndex(name => !STORE_MAP.hasOwnProperty(name)); - if (idx !== -1) { - throw new Error(`Invalid store name ${storeNames[idx]}`); - } - } - - _createTxn(storeNames, writable) { - this._validateStoreNames(storeNames); - const storeValues = storeNames.reduce((values, name) => { - return values[name] = this._storeValues[name]; - }, {}); - return Promise.resolve(new Transaction(storeValues, writable)); - } - - readTxn(storeNames) { - // TODO: avoid concurrency - return this._createTxn(storeNames, false); - } - - readWriteTxn(storeNames) { - // TODO: avoid concurrency - return this._createTxn(storeNames, true); - } -} diff --git a/src/matrix/storage/memory/Transaction.js b/src/matrix/storage/memory/Transaction.js deleted file mode 100644 index 894db805..00000000 --- a/src/matrix/storage/memory/Transaction.js +++ /dev/null @@ -1,73 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {RoomTimelineStore} from "./stores/RoomTimelineStore.js"; - -export class Transaction { - constructor(storeValues, writable) { - this._storeValues = storeValues; - this._txnStoreValues = {}; - this._writable = writable; - } - - _store(name, mapper) { - if (!this._txnStoreValues.hasOwnProperty(name)) { - if (!this._storeValues.hasOwnProperty(name)) { - throw new Error(`Transaction wasn't opened for store ${name}`); - } - const store = mapper(this._storeValues[name]); - const clone = store.cloneStoreValue(); - // extra prevention for writing - if (!this._writable) { - Object.freeze(clone); - } - this._txnStoreValues[name] = clone; - } - return mapper(this._txnStoreValues[name]); - } - - get session() { - throw new Error("not yet implemented"); - // return this._store("session", storeValue => new SessionStore(storeValue)); - } - - get roomSummary() { - throw new Error("not yet implemented"); - // return this._store("roomSummary", storeValue => new RoomSummaryStore(storeValue)); - } - - get roomTimeline() { - return this._store("roomTimeline", storeValue => new RoomTimelineStore(storeValue)); - } - - get roomState() { - throw new Error("not yet implemented"); - // return this._store("roomState", storeValue => new RoomStateStore(storeValue)); - } - - complete() { - for(let name of Object.keys(this._txnStoreValues)) { - this._storeValues[name] = this._txnStoreValues[name]; - } - this._txnStoreValues = null; - return Promise.resolve(); - } - - abort() { - this._txnStoreValues = null; - return Promise.resolve(); - } -} diff --git a/src/matrix/storage/memory/stores/RoomTimelineStore.js b/src/matrix/storage/memory/stores/RoomTimelineStore.js deleted file mode 100644 index 5be20eae..00000000 --- a/src/matrix/storage/memory/stores/RoomTimelineStore.js +++ /dev/null @@ -1,237 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {SortKey} from "../../room/timeline/SortKey.js"; -import {sortedIndex} from "../../../utils/sortedIndex.js"; -import {Store} from "./Store.js"; - -function compareKeys(key, entry) { - if (key.roomId === entry.roomId) { - return key.sortKey.compare(entry.sortKey); - } else { - return key.roomId < entry.roomId ? -1 : 1; - } -} - -class Range { - constructor(timeline, lower, upper, lowerOpen, upperOpen) { - this._timeline = timeline; - this._lower = lower; - this._upper = upper; - this._lowerOpen = lowerOpen; - this._upperOpen = upperOpen; - } - - /** projects the range onto the timeline array */ - project(roomId, maxCount = Number.MAX_SAFE_INTEGER) { - // determine lowest and highest allowed index. - // Important not to bleed into other roomIds here. - const lowerKey = {roomId, sortKey: this._lower || SortKey.minKey }; - // apply lower key being open (excludes given key) - let minIndex = sortedIndex(this._timeline, lowerKey, compareKeys); - if (this._lowerOpen && minIndex < this._timeline.length && compareKeys(lowerKey, this._timeline[minIndex]) === 0) { - minIndex += 1; - } - const upperKey = {roomId, sortKey: this._upper || SortKey.maxKey }; - // apply upper key being open (excludes given key) - let maxIndex = sortedIndex(this._timeline, upperKey, compareKeys); - if (this._upperOpen && maxIndex < this._timeline.length && compareKeys(upperKey, this._timeline[maxIndex]) === 0) { - maxIndex -= 1; - } - // find out from which edge we should grow - // if upper or lower bound - // again, important not to go below minIndex or above maxIndex - // to avoid bleeding into other rooms - let startIndex, endIndex; - if (!this._lower && this._upper) { - startIndex = Math.max(minIndex, maxIndex - maxCount); - endIndex = maxIndex; - } else if (this._lower && !this._upper) { - startIndex = minIndex; - endIndex = Math.min(maxIndex, minIndex + maxCount); - } else { - startIndex = minIndex; - endIndex = maxIndex; - } - - // if startIndex is out of range, make range empty - if (startIndex === this._timeline.length) { - startIndex = endIndex = 0; - } - const count = endIndex - startIndex; - return {startIndex, count}; - } - - select(roomId, maxCount) { - const {startIndex, count} = this.project(roomId, this._timeline, maxCount); - return this._timeline.slice(startIndex, startIndex + count); - } -} - -export class RoomTimelineStore extends Store { - constructor(timeline, writable) { - super(timeline || [], writable); - } - - get _timeline() { - return this._storeValue; - } - - /** Creates a range that only includes the given key - * @param {SortKey} sortKey the key - * @return {Range} the created range - */ - onlyRange(sortKey) { - return new Range(this._timeline, sortKey, sortKey); - } - - /** Creates a range that includes all keys before sortKey, and optionally also the key itself. - * @param {SortKey} sortKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the upper end. - * @return {Range} the created range - */ - upperBoundRange(sortKey, open=false) { - return new Range(this._timeline, undefined, sortKey, undefined, open); - } - - /** Creates a range that includes all keys after sortKey, and optionally also the key itself. - * @param {SortKey} sortKey the key - * @param {boolean} [open=false] whether the key is included (false) or excluded (true) from the range at the lower end. - * @return {Range} the created range - */ - lowerBoundRange(sortKey, open=false) { - return new Range(this._timeline, sortKey, undefined, open); - } - - /** Creates a range that includes all keys between `lower` and `upper`, and optionally the given keys as well. - * @param {SortKey} lower the lower key - * @param {SortKey} upper the upper key - * @param {boolean} [lowerOpen=false] whether the lower key is included (false) or excluded (true) from the range. - * @param {boolean} [upperOpen=false] whether the upper key is included (false) or excluded (true) from the range. - * @return {Range} the created range - */ - boundRange(lower, upper, lowerOpen=false, upperOpen=false) { - return new Range(this._timeline, lower, upper, lowerOpen, upperOpen); - } - - /** Looks up the last `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - lastEvents(roomId, amount) { - return this.eventsBefore(roomId, SortKey.maxKey, amount); - } - - /** Looks up the first `amount` entries in the timeline for `roomId`. - * @param {string} roomId - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - firstEvents(roomId, amount) { - return this.eventsAfter(roomId, SortKey.minKey, amount); - } - - /** Looks up `amount` entries after `sortKey` in the timeline for `roomId`. - * The entry for `sortKey` is not included. - * @param {string} roomId - * @param {SortKey} sortKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - eventsAfter(roomId, sortKey, amount) { - const events = this.lowerBoundRange(sortKey, true).select(roomId, amount); - return Promise.resolve(events); - } - - /** Looks up `amount` entries before `sortKey` in the timeline for `roomId`. - * The entry for `sortKey` is not included. - * @param {string} roomId - * @param {SortKey} sortKey - * @param {number} amount - * @return {Promise} a promise resolving to an array with 0 or more entries, in ascending order. - */ - eventsBefore(roomId, sortKey, amount) { - const events = this.upperBoundRange(sortKey, true).select(roomId, amount); - return Promise.resolve(events); - } - - /** Looks up the first, if any, event entry (so excluding gap entries) after `sortKey`. - * @param {string} roomId - * @param {SortKey} sortKey - * @return {Promise<(?Entry)>} a promise resolving to entry, if any. - */ - nextEvent(roomId, sortKey) { - const searchSpace = this.lowerBoundRange(sortKey, true).select(roomId); - const event = searchSpace.find(entry => !!entry.event); - return Promise.resolve(event); - } - - /** Looks up the first, if any, event entry (so excluding gap entries) before `sortKey`. - * @param {string} roomId - * @param {SortKey} sortKey - * @return {Promise<(?Entry)>} a promise resolving to entry, if any. - */ - previousEvent(roomId, sortKey) { - const searchSpace = this.upperBoundRange(sortKey, true).select(roomId); - const event = searchSpace.reverse().find(entry => !!entry.event); - return Promise.resolve(event); - } - - /** Inserts a new entry into the store. The combination of roomId and sortKey should not exist yet, or an error is thrown. - * @param {Entry} entry the entry to insert - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. - * @throws {StorageError} ... - */ - insert(entry) { - this.assertWritable(); - const insertIndex = sortedIndex(this._timeline, entry, compareKeys); - if (insertIndex < this._timeline.length) { - const existingEntry = this._timeline[insertIndex]; - if (compareKeys(entry, existingEntry) === 0) { - return Promise.reject(new Error("entry already exists")); - } - } - this._timeline.splice(insertIndex, 0, entry); - return Promise.resolve(); - } - - /** Updates the entry into the store with the given [roomId, sortKey] combination. - * If not yet present, will insert. Might be slower than add. - * @param {Entry} entry the entry to update. - * @return {Promise<>} a promise resolving to undefined if the operation was successful, or a StorageError if not. - */ - update(entry) { - this.assertWritable(); - let update = false; - const updateIndex = sortedIndex(this._timeline, entry, compareKeys); - if (updateIndex < this._timeline.length) { - const existingEntry = this._timeline[updateIndex]; - if (compareKeys(entry, existingEntry) === 0) { - update = true; - } - } - this._timeline.splice(updateIndex, update ? 1 : 0, entry); - return Promise.resolve(); - } - - get(roomId, sortKey) { - const range = this.onlyRange(sortKey); - const {startIndex, count} = range.project(roomId); - const event = count ? this._timeline[startIndex] : undefined; - return Promise.resolve(event); - } -} diff --git a/src/matrix/storage/memory/stores/Store.js b/src/matrix/storage/memory/stores/Store.js deleted file mode 100644 index c7218ab8..00000000 --- a/src/matrix/storage/memory/stores/Store.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -export class Store { - constructor(storeValue, writable) { - this._storeValue = storeValue; - this._writable = writable; - } - - // makes a copy deep enough that any modifications in the store - // won't affect the original - // used for transactions - cloneStoreValue() { - // assumes 1 level deep is enough, and that values will be replaced - // rather than updated. - if (Array.isArray(this._storeValue)) { - return this._storeValue.slice(); - } else if (typeof this._storeValue === "object") { - return Object.assign({}, this._storeValue); - } else { - return this._storeValue; - } - } - - assertWritable() { - if (!this._writable) { - throw new Error("Tried to write in read-only transaction"); - } - } -} diff --git a/src/matrix/well-known.js b/src/matrix/well-known.js new file mode 100644 index 00000000..00c91f27 --- /dev/null +++ b/src/matrix/well-known.js @@ -0,0 +1,53 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +function normalizeHomeserver(homeserver) { + try { + return new URL(homeserver).origin; + } catch (err) { + return new URL(`https://${homeserver}`).origin; + } +} + +async function getWellKnownResponse(homeserver, request) { + const requestOptions = {format: "json", timeout: 30000, method: "GET"}; + try { + const wellKnownUrl = `${homeserver}/.well-known/matrix/client`; + return await request(wellKnownUrl, requestOptions).response(); + } catch (err) { + if (err.name === "ConnectionError") { + // don't fail lookup on a ConnectionError, + // there might be a missing CORS header on a 404 response or something, + // which won't be a problem necessarily with homeserver requests later on ... + return null; + } else { + throw err; + } + } +} + +export async function lookupHomeserver(homeserver, request) { + homeserver = normalizeHomeserver(homeserver); + const wellKnownResponse = await getWellKnownResponse(homeserver, request); + if (wellKnownResponse && wellKnownResponse.status === 200) { + const {body} = wellKnownResponse; + const wellKnownHomeserver = body["m.homeserver"]?.["base_url"]; + if (typeof wellKnownHomeserver === "string") { + homeserver = normalizeHomeserver(wellKnownHomeserver); + } + } + return homeserver; +} diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js index 47013df8..2a810058 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.js @@ -42,7 +42,7 @@ export class MappedMap extends BaseObservableMap { this.emitAdd(key, mappedValue); } - onRemove(key, _value) { + onRemove(key/*, _value*/) { const mappedValue = this._mappedValues.get(key); if (this._mappedValues.delete(key)) { this.emitRemove(key, mappedValue); diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index b72cd039..8f5a0922 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -156,7 +156,7 @@ export function tests() { assert.equal(key, 1); assert.deepEqual(value, {value: 5}); }, - onUpdate(key, value, params) { + onUpdate(key, value/*, params*/) { update_fired += 1; assert.equal(key, 1); assert.deepEqual(value, {value: 7}); diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index f1410106..40f47101 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -221,7 +221,7 @@ export class Platform { if (mimeType) { input.setAttribute("accept", mimeType); } - const promise = new Promise((resolve, reject) => { + const promise = new Promise(resolve => { const checkFile = () => { input.removeEventListener("change", checkFile, true); const file = input.files[0]; @@ -240,6 +240,10 @@ export class Platform { return promise; } + openUrl(url) { + location.href = url; + } + parseHTML(html) { return parseHTML(html); } diff --git a/src/platform/web/docroot/index.html b/src/platform/web/docroot/index.html index 90af082a..0b266ff3 100644 --- a/src/platform/web/docroot/index.html +++ b/src/platform/web/docroot/index.html @@ -26,7 +26,7 @@ downloadSandbox: "assets/download-sandbox.html", defaultHomeServer: "matrix.org", // NOTE: uncomment this if you want the service worker for local development - // serviceWorker: "service-worker.js", + // serviceWorker: "sw.js", // NOTE: provide push config if you want push notifs for local development // see assets/config.json for what the config looks like // push: {...}, diff --git a/src/platform/web/docroot/service-worker.js b/src/platform/web/docroot/sw.js similarity index 100% rename from src/platform/web/docroot/service-worker.js rename to src/platform/web/docroot/sw.js diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index 92927d3f..68e4ef78 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -25,6 +25,14 @@ export class History extends BaseObservableValue { } get() { + /* + All URLS in Hydrogen will use /#/segment/value/... + But for SSO, we need to handle /?loginToken= + Handle that as a special case for now. + */ + if (document.location.search.includes("loginToken")) { + return document.location.search; + } return document.location.hash; } diff --git a/src/platform/web/dom/SettingsStorage.js b/src/platform/web/dom/SettingsStorage.js index 4e4c18c7..1590cec5 100644 --- a/src/platform/web/dom/SettingsStorage.js +++ b/src/platform/web/dom/SettingsStorage.js @@ -43,6 +43,14 @@ export class SettingsStorage { return defaultValue; } + async setString(key, value) { + this._set(key, value); + } + + async getString(key) { + return window.localStorage.getItem(`${this._prefix}${key}`); + } + async remove(key) { window.localStorage.removeItem(`${this._prefix}${key}`); } diff --git a/src/platform/web/ui/css/login.css b/src/platform/web/ui/css/login.css index aefdac42..ca376dee 100644 --- a/src/platform/web/ui/css/login.css +++ b/src/platform/web/ui/css/login.css @@ -36,7 +36,7 @@ limitations under the License. align-items: center; } -.SessionPickerView .session-info > :not(:first-child) { +.SessionPickerView .session-info> :not(:first-child) { margin-left: 8px; } @@ -50,19 +50,19 @@ limitations under the License. margin: 0 20px; } -.LoginView { - padding: 0.4em; +.PasswordLoginView { + padding: 0 0.4em 0.4em; } -.SessionLoadStatusView { +.SessionLoadStatusView, .LoginView_query-spinner { display: flex; } -.SessionLoadStatusView > :not(:first-child) { +.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) { margin-left: 12px; } -.SessionLoadStatusView p { +.SessionLoadStatusView p, .LoginView_query-spinner p { flex: 1; margin: 0; } @@ -70,3 +70,29 @@ limitations under the License. .SessionLoadStatusView .spinner { --size: 20px; } + +.StartSSOLoginView { + display: flex; + flex-direction: column; + padding: 0 0.4em 0; +} + +.StartSSOLoginView_button { + flex: 1; + margin-top: 12px; +} + +.LoginView_separator { + justify-content: center; + display: flex; + margin: 8px; +} + +.CompleteSSOView_title { + display: flex; + justify-content: center; +} + +.LoginView_sso { + padding: 0.4em 0.4em 0; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 08a872b8..1b0bc9e4 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -223,6 +223,31 @@ a.button-action { padding-top: 16px; } +.StartSSOLoginView_button { + border: 1px solid #03B381; + border-radius: 8px; +} + +.LoginView_back { + background-image: url("./icons/chevron-left.svg"); + background-color: transparent; +} + +.LoginView_separator { + font-weight: 500; + font-size: 1.5rem; +} + +.LoginView_forwardInfo { + font-size: 0.9em; + margin-left: 1em; + color: #777; +} + +.CompleteSSOView_title { + font-weight: 500; +} + @media screen and (min-width: 600px) { .PreSessionScreen { box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1); diff --git a/src/platform/web/ui/general/ListView.js b/src/platform/web/ui/general/ListView.js index 884eedc4..74aa9d87 100644 --- a/src/platform/web/ui/general/ListView.js +++ b/src/platform/web/ui/general/ListView.js @@ -121,7 +121,7 @@ export class ListView { this.onListChanged(); } - onRemove(idx, _value) { + onRemove(idx/*, _value*/) { this.onBeforeListChanged(); const [child] = this._childInstances.splice(idx, 1); child.root().remove(); @@ -129,7 +129,7 @@ export class ListView { this.onListChanged(); } - onMove(fromIdx, toIdx, value) { + onMove(fromIdx, toIdx/*, value*/) { this.onBeforeListChanged(); const [child] = this._childInstances.splice(fromIdx, 1); this._childInstances.splice(toIdx, 0, child); diff --git a/src/platform/web/ui/login/CompleteSSOView.js b/src/platform/web/ui/login/CompleteSSOView.js new file mode 100644 index 00000000..63614acf --- /dev/null +++ b/src/platform/web/ui/login/CompleteSSOView.js @@ -0,0 +1,30 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../general/TemplateView.js"; +import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; + +export class CompleteSSOView extends TemplateView { + render(t) { + return t.div({ className: "CompleteSSOView" }, + [ + t.p({ className: "CompleteSSOView_title" }, "Finishing up your SSO Login"), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), + ] + ); + } +} diff --git a/src/platform/web/ui/login/LoginView.js b/src/platform/web/ui/login/LoginView.js index 683bf42d..aa89ccca 100644 --- a/src/platform/web/ui/login/LoginView.js +++ b/src/platform/web/ui/login/LoginView.js @@ -16,61 +16,63 @@ limitations under the License. import {TemplateView} from "../general/TemplateView.js"; import {hydrogenGithubLink} from "./common.js"; +import {PasswordLoginView} from "./PasswordLoginView.js"; +import {CompleteSSOView} from "./CompleteSSOView.js"; import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; +import {spinner} from "../common.js"; export class LoginView extends TemplateView { render(t, vm) { - const disabled = vm => !!vm.isBusy; - const username = t.input({ - id: "username", - type: "text", - placeholder: vm.i18n`Username`, - disabled - }); - const password = t.input({ - id: "password", - type: "password", - placeholder: vm.i18n`Password`, - disabled - }); - const homeserver = t.input({ - id: "homeserver", - type: "text", - placeholder: vm.i18n`Your matrix homeserver`, - value: vm.defaultHomeServer, - disabled - }); - + const disabled = vm => vm.isBusy; + return t.div({className: "PreSessionScreen"}, [ + t.button({ + className: "button-utility LoginView_back", + onClick: () => vm.goBack(), + disabled + }), t.div({className: "logo"}), - t.div({className: "LoginView form"}, [ - t.h1([vm.i18n`Sign In`]), - t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), - t.form({ - onSubmit: evnt => { - evnt.preventDefault(); - vm.login(username.value, password.value, homeserver.value); - } - }, [ - t.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]), - t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]), - t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]), - t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), - t.div({className: "button-row"}, [ - t.a({ - className: "button-action secondary", - href: vm.cancelUrl - }, [vm.i18n`Go Back`]), - t.button({ - className: "button-action primary", - type: "submit" - }, vm.i18n`Log In`), - ]), - ]), - // use t.mapView rather than t.if to create a new view when the view model changes too - t.p(hydrogenGithubLink(t)) - ]) + t.h1([vm.i18n`Sign In`]), + t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null), + t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" }, + [ + t.label({for: "homeserver"}, vm.i18n`Homeserver`), + t.input({ + id: "homeserver", + type: "text", + placeholder: vm.i18n`Your matrix homeserver`, + value: vm.homeserver, + disabled, + onInput: event => vm.setHomeserver(event.target.value), + onChange: () => vm.queryHomeserver(), + }), + t.p({className: { + LoginView_forwardInfo: true, + hidden: vm => !vm.resolvedHomeserver + }}, vm => vm.i18n`You will connect to ${vm.resolvedHomeserver}.`), + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), + ] + )), + t.if(vm => vm.isFetchingLoginOptions, t => t.div({className: "LoginView_query-spinner"}, [spinner(t), t.p("Fetching available login options...")])), + t.mapView(vm => vm.passwordLoginViewModel, vm => vm ? new PasswordLoginView(vm): null), + t.if(vm => vm.passwordLoginViewModel && vm.startSSOLoginViewModel, t => t.p({className: "LoginView_separator"}, vm.i18n`or`)), + t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null), + // use t.mapView rather than t.if to create a new view when the view model changes too + t.p(hydrogenGithubLink(t)) ]); } } +class StartSSOLoginView extends TemplateView { + render(t, vm) { + return t.div({ className: "StartSSOLoginView" }, + t.button({ + className: "StartSSOLoginView_button button-action secondary", + type: "button", + onClick: () => vm.startSSOLogin(), + disabled: vm => vm.isBusy + }, vm.i18n`Log in with SSO`) + ); + } +} diff --git a/src/platform/web/ui/login/PasswordLoginView.js b/src/platform/web/ui/login/PasswordLoginView.js new file mode 100644 index 00000000..130f30ae --- /dev/null +++ b/src/platform/web/ui/login/PasswordLoginView.js @@ -0,0 +1,57 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "../general/TemplateView.js"; + +export class PasswordLoginView extends TemplateView { + render(t, vm) { + const disabled = vm => !!vm.isBusy; + const username = t.input({ + id: "username", + type: "text", + placeholder: vm.i18n`Username`, + disabled + }); + const password = t.input({ + id: "password", + type: "password", + placeholder: vm.i18n`Password`, + disabled + }); + + return t.div({className: "PasswordLoginView form"}, [ + t.if(vm => vm.error, t => t.div({ className: "error" }, vm => vm.error)), + t.form({ + onSubmit: evnt => { + evnt.preventDefault(); + vm.login(username.value, password.value); + } + }, [ + t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))), + t.div({ className: "form-row" }, [t.label({ for: "username" }, vm.i18n`Username`), username]), + t.div({ className: "form-row" }, [t.label({ for: "password" }, vm.i18n`Password`), password]), + t.div({ className: "button-row" }, [ + t.button({ + className: "button-action primary", + type: "submit", + disabled + }, vm.i18n`Log In`), + ]), + ]) + ]); + } +} + diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index e1d8f13c..c6e8a1ee 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -17,7 +17,6 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; -import {TextMessageView} from "./timeline/TextMessageView.js"; import {viewClassForEntry} from "./TimelineList.js" export class MessageComposer extends TemplateView { diff --git a/src/platform/web/ui/session/room/RoomArchivedView.js b/src/platform/web/ui/session/room/RoomArchivedView.js index e5e489ed..80b18a08 100644 --- a/src/platform/web/ui/session/room/RoomArchivedView.js +++ b/src/platform/web/ui/session/room/RoomArchivedView.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView.js"; export class RoomArchivedView extends TemplateView { - render(t, vm) { + render(t) { return t.div({className: "RoomArchivedView"}, t.h3(vm => vm.description)); } -} \ No newline at end of file +} diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index ef4d61d5..fcafaf27 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -97,7 +97,7 @@ const formatFunction = { link: linkPart => tag.a({href: linkPart.url, className: "link", target: "_blank", rel: "noopener" }, renderParts(linkPart.inlines)), pill: renderPill, format: formatPart => tag[formatPart.format](renderParts(formatPart.children)), - rule: rulePart => tag.hr(), + rule: () => tag.hr(), list: renderList, image: renderImage, newline: () => tag.br() diff --git a/src/platform/web/ui/view-gallery.html b/src/platform/web/ui/view-gallery.html index 7887d44f..675b2b5b 100644 --- a/src/platform/web/ui/view-gallery.html +++ b/src/platform/web/ui/view-gallery.html @@ -43,7 +43,7 @@ diff --git a/src/utils/AbortableOperation.ts b/src/utils/AbortableOperation.ts new file mode 100644 index 00000000..0cc49e10 --- /dev/null +++ b/src/utils/AbortableOperation.ts @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +interface IAbortable { + abort(); +} + +type RunFn = (setAbortable: (a: IAbortable) => typeof a) => T; + +export class AbortableOperation { + public readonly result: T; + private _abortable: IAbortable | null; + + constructor(run: RunFn) { + this._abortable = null; + const setAbortable = abortable => { + this._abortable = abortable; + return abortable; + }; + this.result = run(setAbortable); + } + + abort() { + this._abortable?.abort(); + this._abortable = null; + } +} diff --git a/src/utils/EventEmitter.js b/src/utils/EventEmitter.js index 2d2e4458..5dd56ac3 100644 --- a/src/utils/EventEmitter.js +++ b/src/utils/EventEmitter.js @@ -55,9 +55,9 @@ export class EventEmitter { } } - onFirstSubscriptionAdded(name) {} + onFirstSubscriptionAdded(/* name */) {} - onLastSubscriptionRemoved(name) {} + onLastSubscriptionRemoved(/* name */) {} } export function tests() { diff --git a/yarn.lock b/yarn.lock index 7e547281..638e0cd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -867,25 +867,60 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" -"@eslint/eslintrc@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.0.tgz#99cc0a0584d72f1df38b900fb062ba995f395547" - integrity sha512-2ZPCc+uNbjV5ERJr+aKSPRwZgKd2z11x0EgLvb1PURmUrn9QNRXFqje0Ldq454PfAVyaJYyrDvvIKSFP4NnBog== +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== dependencies: ajv "^6.12.4" debug "^4.1.1" espree "^7.3.0" - globals "^12.1.0" + globals "^13.9.0" ignore "^4.0.6" import-fresh "^3.2.1" js-yaml "^3.13.1" minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" + integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== + "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": version "3.2.3" resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@npmcli/arborist@^2.6.4": version "2.8.0" resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-2.8.0.tgz#ff078287eba44595383eb58ad8aa8540bc8aae9e" @@ -1154,6 +1189,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== +"@types/json-schema@^7.0.7": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + "@types/keyv@*": version "3.1.2" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.2.tgz#5d97bb65526c20b6e0845f6b0d2ade4f28604ee5" @@ -1185,6 +1225,75 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@^4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.2.tgz#f54dc0a32b8f61c6024ab8755da05363b733838d" + integrity sha512-x4EMgn4BTfVd9+Z+r+6rmWxoAzBaapt4QFqE+d8L8sUtYZYLDTK6VG/y/SMMWA5t1/BVU5Kf+20rX4PtWzUYZg== + dependencies: + "@typescript-eslint/experimental-utils" "4.29.2" + "@typescript-eslint/scope-manager" "4.29.2" + debug "^4.3.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.1.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.29.2.tgz#5f67fb5c5757ef2cb3be64817468ba35c9d4e3b7" + integrity sha512-P6mn4pqObhftBBPAv4GQtEK7Yos1fz/MlpT7+YjH9fTxZcALbiiPKuSIfYP/j13CeOjfq8/fr9Thr2glM9ub7A== + dependencies: + "@types/json-schema" "^7.0.7" + "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/typescript-estree" "4.29.2" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/parser@^4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.29.2.tgz#1c7744f4c27aeb74610c955d3dce9250e95c370a" + integrity sha512-WQ6BPf+lNuwteUuyk1jD/aHKqMQ9jrdCn7Gxt9vvBnzbpj7aWEf+aZsJ1zvTjx5zFxGCt000lsbD9tQPEL8u6g== + dependencies: + "@typescript-eslint/scope-manager" "4.29.2" + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/typescript-estree" "4.29.2" + debug "^4.3.1" + +"@typescript-eslint/scope-manager@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.29.2.tgz#442b0f029d981fa402942715b1718ac7fcd5aa1b" + integrity sha512-mfHmvlQxmfkU8D55CkZO2sQOueTxLqGvzV+mG6S/6fIunDiD2ouwsAoiYCZYDDK73QCibYjIZmGhpvKwAB5BOA== + dependencies: + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/visitor-keys" "4.29.2" + +"@typescript-eslint/types@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.29.2.tgz#fc0489c6b89773f99109fb0aa0aaddff21f52fcd" + integrity sha512-K6ApnEXId+WTGxqnda8z4LhNMa/pZmbTFkDxEBLQAbhLZL50DjeY0VIDCml/0Y3FlcbqXZrABqrcKxq+n0LwzQ== + +"@typescript-eslint/typescript-estree@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.29.2.tgz#a0ea8b98b274adbb2577100ba545ddf8bf7dc219" + integrity sha512-TJ0/hEnYxapYn9SGn3dCnETO0r+MjaxtlWZ2xU+EvytF0g4CqTpZL48SqSNn2hXsPolnewF30pdzR9a5Lj3DNg== + dependencies: + "@typescript-eslint/types" "4.29.2" + "@typescript-eslint/visitor-keys" "4.29.2" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/visitor-keys@4.29.2": + version "4.29.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.29.2.tgz#d2da7341f3519486f50655159f4e5ecdcb2cd1df" + integrity sha512-bDgJLQ86oWHJoZ1ai4TZdgXzJxsea3Ee9u9wsTAvjChdj2WLcVsgWYAPeY7RQMn16tKrlQaBnpKv7KBfs4EQag== + dependencies: + "@typescript-eslint/types" "4.29.2" + eslint-visitor-keys "^2.0.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -1321,6 +1430,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + asap@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" @@ -1461,7 +1575,7 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -braces@~3.0.2: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1874,7 +1988,7 @@ debug@2.6.9, debug@^2.6.0: dependencies: ms "2.0.0" -debug@4: +debug@4, debug@^4.3.1: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -1979,6 +2093,13 @@ dezalgo@^1.0.0: asap "^2.0.0" wrappy "1" +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -2214,6 +2335,11 @@ escape-string-regexp@^1.0.3, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + esinstall@^1.0.0, esinstall@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/esinstall/-/esinstall-1.1.7.tgz#ceabeb4b8685bf48c805a503e292dfafe4e0cb22" @@ -2255,6 +2381,13 @@ eslint-utils@^2.1.0: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" @@ -2265,28 +2398,31 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint@^7.25.0: - version "7.25.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.25.0.tgz#1309e4404d94e676e3e831b3a3ad2b050031eb67" - integrity sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw== +eslint@^7.32.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.0" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" enquirer "^2.3.5" + escape-string-regexp "^4.0.0" eslint-scope "^5.1.1" eslint-utils "^2.1.0" eslint-visitor-keys "^2.0.0" espree "^7.3.1" esquery "^1.4.0" esutils "^2.0.2" + fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" + glob-parent "^5.1.2" globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" @@ -2295,7 +2431,7 @@ eslint@^7.25.0: js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" - lodash "^4.17.21" + lodash.merge "^4.6.2" minimatch "^3.0.4" natural-compare "^1.4.0" optionator "^0.9.1" @@ -2304,7 +2440,7 @@ eslint@^7.25.0: semver "^7.2.1" strip-ansi "^6.0.0" strip-json-comments "^3.1.0" - table "^6.0.4" + table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" @@ -2419,11 +2555,22 @@ fake-indexeddb@^3.1.2: realistic-structured-clone "^2.0.1" setimmediate "^1.0.5" -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-glob@^3.1.1: + version "3.2.7" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" + integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -2434,6 +2581,13 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fastq@^1.6.0: + version "1.11.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.11.1.tgz#5d8175aae17db61947f8b162cfc7f63264d22807" + integrity sha512-HOnr8Mc60eNYl1gzwp6r5RoUyAn5/glBolUzP/Ez6IFVPMPirxn/9phgL6zhOtaTy7ISwPvQ+wT+hfcRZh/bzw== + dependencies: + reusify "^1.0.4" + fdir@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/fdir/-/fdir-5.1.0.tgz#973e4934e6a3666b59ebdfc56f60bb8e9b16acb8" @@ -2605,7 +2759,7 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -glob-parent@^5.0.0, glob-parent@~5.1.2: +glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -2641,13 +2795,6 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== - dependencies: - type-fest "^0.8.1" - globals@^13.6.0: version "13.8.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.8.0.tgz#3e20f504810ce87a8d72e55aecf8435b50f4c1b3" @@ -2655,6 +2802,25 @@ globals@^13.6.0: dependencies: type-fest "^0.20.2" +globals@^13.9.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7" + integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.3: + version "11.0.4" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5" + integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + got@^11.1.4: version "11.8.2" resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" @@ -2847,6 +3013,11 @@ ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -3239,6 +3410,11 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + lodash.truncate@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" @@ -3254,7 +3430,7 @@ lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3332,11 +3508,24 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + meriyah@^3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-3.1.6.tgz#56c9c0edb63f9640c7609a39a413c60b038e4451" integrity sha512-JDOSi6DIItDc33U5N52UdV6P8v+gn+fqZKfbAfHzdWApRQyQWdcvxPvAr9t01bI2rBxGvSrKRQSCg3SkZC1qeg== +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + mime-db@1.49.0, "mime-db@>= 1.43.0 < 2": version "1.49.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" @@ -3921,7 +4110,7 @@ periscopic@^2.0.3: estree-walker "^2.0.2" is-reference "^1.1.4" -picomatch@^2.0.4, picomatch@^2.3.0: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== @@ -4153,6 +4342,11 @@ qs@~6.5.2: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" @@ -4365,6 +4559,11 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -4415,6 +4614,13 @@ rollup@~2.37.1: optionalDependencies: fsevents "~2.1.2" +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@^5.0.1, safe-buffer@^5.1.2: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -4541,7 +4747,7 @@ skypack@^0.3.0: rollup "^2.23.0" validate-npm-package-name "^3.0.0" -slash@~3.0.0: +slash@^3.0.0, slash@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== @@ -4794,10 +5000,10 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -table@^6.0.4: - version "6.7.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.7.0.tgz#26274751f0ee099c547f6cb91d3eff0d61d155b2" - integrity sha512-SAM+5p6V99gYiiy2gT5ArdzgM1dLDed0nkrWmG6Fry/bUS/m9x83BwpJUOf1Qj/x2qJd+thL6IkIx7qPGRxqBw== +table@^6.0.9: + version "6.7.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.7.1.tgz#ee05592b7143831a8c94f3cee6aae4c1ccef33e2" + integrity sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== dependencies: ajv "^8.0.1" lodash.clonedeep "^4.5.0" @@ -4865,11 +5071,23 @@ treeverse@^1.0.4: resolved "https://registry.yarnpkg.com/treeverse/-/treeverse-1.0.4.tgz#a6b0ebf98a1bca6846ddc7ecbc900df08cb9cd5f" integrity sha512-whw60l7r+8ZU8Tu/Uc2yxtc4ZTZbR/PF3u1IPNKGQ6p8EICLb3Z2lAgoqw9bqYd8IkgnsaOcLzYHFckjqNsf0g== +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -4894,11 +5112,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - typedarray-to-buffer@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"