forked from mystiq/hydrogen-web
Merge branch 'master' into snowpack-ts-storage-1
This commit is contained in:
commit
904a2cbe74
55 changed files with 1282 additions and 683 deletions
|
@ -1 +0,0 @@
|
|||
src/matrix/storage/memory
|
2
.github/workflows/codechecks.js.yml
vendored
2
.github/workflows/codechecks.js.yml
vendored
|
@ -43,3 +43,5 @@ jobs:
|
|||
run: yarn test
|
||||
- name: Lint
|
||||
run: yarn run lint-ci
|
||||
- name: Typescript
|
||||
run: yarn run tsc
|
||||
|
|
24
.ts-eslintrc.js
Normal file
24
.ts-eslintrc.js
Normal file
|
@ -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
|
||||
}
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
],
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
this._setSection(() => {
|
||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||
createAndStartSessionContainer: () => {
|
||||
const sessionContainer = this._createSessionContainer();
|
||||
sessionContainer.startWithExistingSession(sessionId);
|
||||
return sessionContainer;
|
||||
},
|
||||
this._setSection(() => {
|
||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||
sessionContainer,
|
||||
ready: sessionContainer => this._showSession(sessionContainer)
|
||||
}));
|
||||
this._sessionLoadViewModel.start();
|
||||
|
|
|
@ -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:
|
||||
|
|
76
src/domain/login/CompleteSSOLoginViewModel.js
Normal file
76
src/domain/login/CompleteSSOLoginViewModel.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
239
src/domain/login/LoginViewModel.js
Normal file
239
src/domain/login/LoginViewModel.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
63
src/domain/login/PasswordLoginViewModel.js
Normal file
63
src/domain/login/PasswordLoginViewModel.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
38
src/domain/login/StartSSOLoginViewModel.js
Normal file
38
src/domain/login/StartSSOLoginViewModel.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=<token>
|
||||
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);
|
||||
|
|
|
@ -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 ... ?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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*/) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
30
src/matrix/login/LoginMethod.js
Normal file
30
src/matrix/login/LoginMethod.js
Normal file
|
@ -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");
|
||||
}
|
||||
}
|
29
src/matrix/login/PasswordLoginMethod.js
Normal file
29
src/matrix/login/PasswordLoginMethod.js
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
27
src/matrix/login/SSOLoginHelper.js
Normal file
27
src/matrix/login/SSOLoginHelper.js
Normal file
|
@ -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}`;
|
||||
}
|
||||
}
|
29
src/matrix/login/TokenLoginMethod.js
Normal file
29
src/matrix/login/TokenLoginMethod.js
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ export class EventEntry extends BaseEventEntry {
|
|||
}
|
||||
|
||||
updateFrom(other) {
|
||||
super.updateFrom(other);
|
||||
if (other._decryptionResult && !this._decryptionResult) {
|
||||
this._decryptionResult = other._decryptionResult;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,237 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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<Entry[]>} 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<Entry[]>} 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<Entry[]>} 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<Entry[]>} 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);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
53
src/matrix/well-known.js
Normal file
53
src/matrix/well-known.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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: {...},
|
||||
|
|
|
@ -25,6 +25,14 @@ export class History extends BaseObservableValue {
|
|||
}
|
||||
|
||||
get() {
|
||||
/*
|
||||
All URLS in Hydrogen will use <root>/#/segment/value/...
|
||||
But for SSO, we need to handle <root>/?loginToken=<TOKEN>
|
||||
Handle that as a special case for now.
|
||||
*/
|
||||
if (document.location.search.includes("loginToken")) {
|
||||
return document.location.search;
|
||||
}
|
||||
return document.location.hash;
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
30
src/platform/web/ui/login/CompleteSSOView.js
Normal file
30
src/platform/web/ui/login/CompleteSSOView.js
Normal file
|
@ -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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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`,
|
||||
const disabled = vm => vm.isBusy;
|
||||
|
||||
return t.div({className: "PreSessionScreen"}, [
|
||||
t.button({
|
||||
className: "button-utility LoginView_back",
|
||||
onClick: () => vm.goBack(),
|
||||
disabled
|
||||
});
|
||||
const password = t.input({
|
||||
id: "password",
|
||||
type: "password",
|
||||
placeholder: vm.i18n`Password`,
|
||||
disabled
|
||||
});
|
||||
const homeserver = t.input({
|
||||
}),
|
||||
t.div({className: "logo"}),
|
||||
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.defaultHomeServer,
|
||||
disabled
|
||||
});
|
||||
|
||||
return t.div({className: "PreSessionScreen"}, [
|
||||
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]),
|
||||
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),
|
||||
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))
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
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`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
57
src/platform/web/ui/login/PasswordLoginView.js
Normal file
57
src/platform/web/ui/login/PasswordLoginView.js
Normal file
|
@ -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`),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<script id="main" type="module">
|
||||
import {LoginView} from "./login/LoginView.js";
|
||||
const view = new LoginView(vm({
|
||||
defaultHomeServer: "https://hs.tld",
|
||||
defaultHomeserver: "https://hs.tld",
|
||||
login: () => alert("Logging in!"),
|
||||
cancelUrl: "#/session"
|
||||
}));
|
||||
|
@ -60,7 +60,7 @@
|
|||
loading: true,
|
||||
}),
|
||||
cancelUrl: "#/session",
|
||||
defaultHomeServer: "https://hs.tld",
|
||||
defaultHomeserver: "https://hs.tld",
|
||||
}));
|
||||
document.getElementById("login-loading").appendChild(view.mount());
|
||||
</script>
|
||||
|
|
40
src/utils/AbortableOperation.ts
Normal file
40
src/utils/AbortableOperation.ts
Normal file
|
@ -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<T> = (setAbortable: (a: IAbortable) => typeof a) => T;
|
||||
|
||||
export class AbortableOperation<T> {
|
||||
public readonly result: T;
|
||||
private _abortable: IAbortable | null;
|
||||
|
||||
constructor(run: RunFn<T>) {
|
||||
this._abortable = null;
|
||||
const setAbortable = abortable => {
|
||||
this._abortable = abortable;
|
||||
return abortable;
|
||||
};
|
||||
this.result = run(setAbortable);
|
||||
}
|
||||
|
||||
abort() {
|
||||
this._abortable?.abort();
|
||||
this._abortable = null;
|
||||
}
|
||||
}
|
|
@ -55,9 +55,9 @@ export class EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
onFirstSubscriptionAdded(name) {}
|
||||
onFirstSubscriptionAdded(/* name */) {}
|
||||
|
||||
onLastSubscriptionRemoved(name) {}
|
||||
onLastSubscriptionRemoved(/* name */) {}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
|
|
285
yarn.lock
285
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"
|
||||
|
|
Loading…
Reference in a new issue