forked from mystiq/hydrogen-web
Merge branch 'snowpack-ts-storage-1' into snowpack-ts-storage-2
This commit is contained in:
commit
b081aa171c
50 changed files with 995 additions and 630 deletions
|
@ -1 +0,0 @@
|
||||||
src/matrix/storage/memory
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydrogen-web",
|
"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",
|
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|
|
@ -114,8 +114,8 @@ async function build({modernOnly, overrideImports, overrideCss}) {
|
||||||
await buildManifest(assets);
|
await buildManifest(assets);
|
||||||
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
|
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
|
||||||
assets.addToHashForAll("index.html", devHtml);
|
assets.addToHashForAll("index.html", devHtml);
|
||||||
let swSource = await fs.readFile(path.join(snowpackOutPath, "service-worker.js"), "utf8");
|
let swSource = await fs.readFile(path.join(snowpackOutPath, "sw.js"), "utf8");
|
||||||
assets.addToHashForAll("service-worker.js", swSource);
|
assets.addToHashForAll("sw.js", swSource);
|
||||||
|
|
||||||
const globalHash = assets.hashForAll();
|
const globalHash = assets.hashForAll();
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ async function buildHtml(doc, version, baseConfig, globalHash, modernOnly, asset
|
||||||
const configJSON = JSON.stringify(Object.assign({}, baseConfig, {
|
const configJSON = JSON.stringify(Object.assign({}, baseConfig, {
|
||||||
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||||
downloadSandbox: assets.resolve("download-sandbox.html"),
|
downloadSandbox: assets.resolve("download-sandbox.html"),
|
||||||
serviceWorker: "service-worker.js",
|
serviceWorker: "sw.js",
|
||||||
olm: {
|
olm: {
|
||||||
wasm: assets.resolve("olm.wasm"),
|
wasm: assets.resolve("olm.wasm"),
|
||||||
legacyBundle: assets.resolve("olm_legacy.js"),
|
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"));
|
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
|
// 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) {
|
async function buildCssBundles(buildFn, themes, assets, mainCssFile = null) {
|
||||||
|
|
|
@ -17,7 +17,6 @@ module.exports = {
|
||||||
'**/scripts/**',
|
'**/scripts/**',
|
||||||
'**/target/**',
|
'**/target/**',
|
||||||
'**/prototypes/**',
|
'**/prototypes/**',
|
||||||
'**/src/matrix/storage/memory/**',
|
|
||||||
'**/src/platform/web/legacy-polyfill.js',
|
'**/src/platform/web/legacy-polyfill.js',
|
||||||
'**/src/platform/web/worker/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 {SessionViewModel} from "./session/SessionViewModel.js";
|
||||||
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
|
||||||
import {LoginViewModel} from "./LoginViewModel.js";
|
import {LoginViewModel} from "./login/LoginViewModel.js";
|
||||||
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
|
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
|
||||||
|
@ -35,12 +35,14 @@ export class RootViewModel extends ViewModel {
|
||||||
async load() {
|
async load() {
|
||||||
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
|
this.track(this.navigation.observe("login").subscribe(() => this._applyNavigation()));
|
||||||
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
|
this.track(this.navigation.observe("session").subscribe(() => this._applyNavigation()));
|
||||||
|
this.track(this.navigation.observe("sso").subscribe(() => this._applyNavigation()));
|
||||||
this._applyNavigation(true);
|
this._applyNavigation(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _applyNavigation(shouldRestoreLastUrl) {
|
async _applyNavigation(shouldRestoreLastUrl) {
|
||||||
const isLogin = this.navigation.observe("login").get();
|
const isLogin = this.navigation.path.get("login")
|
||||||
const sessionId = this.navigation.observe("session").get();
|
const sessionId = this.navigation.path.get("session")?.value;
|
||||||
|
const loginToken = this.navigation.path.get("sso")?.value;
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
if (this.activeSection !== "login") {
|
if (this.activeSection !== "login") {
|
||||||
this._showLogin();
|
this._showLogin();
|
||||||
|
@ -65,7 +67,13 @@ export class RootViewModel extends ViewModel {
|
||||||
this._showSessionLoader(sessionId);
|
this._showSessionLoader(sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (loginToken) {
|
||||||
|
this.urlCreator.normalizeUrl();
|
||||||
|
if (this.activeSection !== "login") {
|
||||||
|
this._showLogin(loginToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
try {
|
try {
|
||||||
if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) {
|
if (!(shouldRestoreLastUrl && this.urlCreator.tryRestoreLastUrl())) {
|
||||||
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
||||||
|
@ -94,10 +102,10 @@ export class RootViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showLogin() {
|
_showLogin(loginToken) {
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._loginViewModel = new LoginViewModel(this.childOptions({
|
this._loginViewModel = new LoginViewModel(this.childOptions({
|
||||||
defaultHomeServer: this.platform.config["defaultHomeServer"],
|
defaultHomeserver: this.platform.config["defaultHomeServer"],
|
||||||
createSessionContainer: this._createSessionContainer,
|
createSessionContainer: this._createSessionContainer,
|
||||||
ready: sessionContainer => {
|
ready: sessionContainer => {
|
||||||
// we don't want to load the session container again,
|
// we don't want to load the session container again,
|
||||||
|
@ -111,6 +119,7 @@ export class RootViewModel extends ViewModel {
|
||||||
this._pendingSessionContainer = sessionContainer;
|
this._pendingSessionContainer = sessionContainer;
|
||||||
this.navigation.push("session", sessionContainer.sessionId);
|
this.navigation.push("session", sessionContainer.sessionId);
|
||||||
},
|
},
|
||||||
|
loginToken
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -123,13 +132,11 @@ export class RootViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
_showSessionLoader(sessionId) {
|
_showSessionLoader(sessionId) {
|
||||||
this._setSection(() => {
|
|
||||||
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
|
||||||
createAndStartSessionContainer: () => {
|
|
||||||
const sessionContainer = this._createSessionContainer();
|
const sessionContainer = this._createSessionContainer();
|
||||||
sessionContainer.startWithExistingSession(sessionId);
|
sessionContainer.startWithExistingSession(sessionId);
|
||||||
return sessionContainer;
|
this._setSection(() => {
|
||||||
},
|
this._sessionLoadViewModel = new SessionLoadViewModel(this.childOptions({
|
||||||
|
sessionContainer,
|
||||||
ready: sessionContainer => this._showSession(sessionContainer)
|
ready: sessionContainer => this._showSession(sessionContainer)
|
||||||
}));
|
}));
|
||||||
this._sessionLoadViewModel.start();
|
this._sessionLoadViewModel.start();
|
||||||
|
|
|
@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
|
import {LoadStatus} from "../matrix/SessionContainer.js";
|
||||||
import {SyncStatus} from "../matrix/Sync.js";
|
import {SyncStatus} from "../matrix/Sync.js";
|
||||||
import {ViewModel} from "./ViewModel.js";
|
import {ViewModel} from "./ViewModel.js";
|
||||||
|
|
||||||
export class SessionLoadViewModel extends ViewModel {
|
export class SessionLoadViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {createAndStartSessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
const {sessionContainer, ready, homeserver, deleteSessionOnCancel} = options;
|
||||||
this._createAndStartSessionContainer = createAndStartSessionContainer;
|
this._sessionContainer = sessionContainer;
|
||||||
this._ready = ready;
|
this._ready = ready;
|
||||||
this._homeserver = homeserver;
|
this._homeserver = homeserver;
|
||||||
this._deleteSessionOnCancel = deleteSessionOnCancel;
|
this._deleteSessionOnCancel = deleteSessionOnCancel;
|
||||||
|
@ -38,7 +38,6 @@ export class SessionLoadViewModel extends ViewModel {
|
||||||
try {
|
try {
|
||||||
this._loading = true;
|
this._loading = true;
|
||||||
this.emitChange("loading");
|
this.emitChange("loading");
|
||||||
this._sessionContainer = this._createAndStartSessionContainer();
|
|
||||||
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => {
|
||||||
this.emitChange("loadLabel");
|
this.emitChange("loadLabel");
|
||||||
// wait for initial sync, but not catchup sync
|
// wait for initial sync, but not catchup sync
|
||||||
|
@ -109,22 +108,9 @@ export class SessionLoadViewModel extends ViewModel {
|
||||||
return `Something went wrong: ${error && error.message}.`;
|
return `Something went wrong: ${error && error.message}.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statuses related to login are handled by respective login view models
|
||||||
if (sc) {
|
if (sc) {
|
||||||
switch (sc.loadStatus.get()) {
|
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:
|
case LoadStatus.SessionSetup:
|
||||||
return `Setting up your encryption keys…`;
|
return `Setting up your encryption keys…`;
|
||||||
case LoadStatus.Loading:
|
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}`;
|
const urlPath = `${this._stringifyPath(this._navigation.path.until("session"))}/open-room/${roomId}`;
|
||||||
return this._history.pathAsUrl(urlPath);
|
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) {
|
switch (parent?.type) {
|
||||||
case undefined:
|
case undefined:
|
||||||
// allowed root segments
|
// allowed root segments
|
||||||
return type === "login" || type === "session";
|
return type === "login" || type === "session" || type === "sso";
|
||||||
case "session":
|
case "session":
|
||||||
return type === "room" || type === "rooms" || type === "settings";
|
return type === "room" || type === "rooms" || type === "settings";
|
||||||
case "rooms":
|
case "rooms":
|
||||||
|
@ -152,6 +152,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) {
|
||||||
const userId = iterator.next().value;
|
const userId = iterator.next().value;
|
||||||
if (!userId) { break; }
|
if (!userId) { break; }
|
||||||
pushRightPanelSegment(segments, type, userId);
|
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 {
|
} else {
|
||||||
// might be undefined, which will be turned into true by Segment
|
// might be undefined, which will be turned into true by Segment
|
||||||
const value = iterator.next().value;
|
const value = iterator.next().value;
|
||||||
|
@ -181,7 +185,8 @@ export function stringifyPath(path) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "right-panel":
|
case "right-panel":
|
||||||
// Ignore right-panel in url
|
case "sso":
|
||||||
|
// Do not put these segments in URL
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
urlPath += `/${segment.type}`;
|
urlPath += `/${segment.type}`;
|
||||||
|
@ -228,6 +233,12 @@ export function tests() {
|
||||||
const urlPath = stringifyPath(path);
|
const urlPath = stringifyPath(path);
|
||||||
assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details");
|
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 => {
|
"parse grid url path with focused empty tile": assert => {
|
||||||
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
const segments = parseUrlPath("/session/1/rooms/a,b,c/3");
|
||||||
assert.equal(segments.length, 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?
|
// this ... cannot happen in the timeline?
|
||||||
// perhaps we can use this event to support a local echo (in a different fragment)
|
// 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 ... ?
|
// 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
|
// get lowerSortKey for tile at index tileAmount - 1
|
||||||
// tell timeline to unload till there (included given key)
|
// 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
|
// get upperSortKey for tile at index tiles.length - tileAmount
|
||||||
// tell timeline to unload till there (included given key)
|
// 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
|
// return whether the tile should be removed
|
||||||
// as SimpleTile only has one entry, the tile should be removed
|
// as SimpleTile only has one entry, the tile should be removed
|
||||||
removeEntry(entry) {
|
removeEntry(/*entry*/) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,12 +110,12 @@ export class SimpleTile extends ViewModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// let item know it has a new sibling
|
// let item know it has a new sibling
|
||||||
updatePreviousSibling(prev) {
|
updatePreviousSibling(/*prev*/) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// let item know it has a new sibling
|
// 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.enabledOnServer = null;
|
||||||
this.pushNotifications.serverError = null;
|
this.pushNotifications.serverError = null;
|
||||||
try {
|
try {
|
||||||
this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeServer();
|
this.pushNotifications.enabledOnServer = await this._session.checkPusherEnabledOnHomeserver();
|
||||||
this.emitChange("pushNotifications.enabledOnServer");
|
this.emitChange("pushNotifications.enabledOnServer");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.pushNotifications.serverError = err;
|
this.pushNotifications.serverError = err;
|
||||||
|
|
|
@ -46,7 +46,7 @@ const PICKLE_KEY = "DEFAULT_KEY";
|
||||||
const PUSHER_KEY = "pusher";
|
const PUSHER_KEY = "pusher";
|
||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
// sessionInfo contains deviceId, userId and homeServer
|
// sessionInfo contains deviceId, userId and homeserver
|
||||||
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
|
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
|
@ -636,7 +636,7 @@ export class Session {
|
||||||
return !!pusherData;
|
return !!pusherData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPusherEnabledOnHomeServer() {
|
async checkPusherEnabledOnHomeserver() {
|
||||||
const readTxn = await this._storage.readTxn([this._storage.storeNames.session]);
|
const readTxn = await this._storage.readTxn([this._storage.storeNames.session]);
|
||||||
const pusherData = await readTxn.session.get(PUSHER_KEY);
|
const pusherData = await readTxn.session.get(PUSHER_KEY);
|
||||||
if (!pusherData) {
|
if (!pusherData) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 {createEnum} from "../utils/enum.js";
|
||||||
|
import {lookupHomeserver} from "./well-known.js";
|
||||||
|
import {AbortableOperation} from "../utils/AbortableOperation";
|
||||||
import {ObservableValue} from "../observable/ObservableValue.js";
|
import {ObservableValue} from "../observable/ObservableValue.js";
|
||||||
import {HomeServerApi} from "./net/HomeServerApi.js";
|
import {HomeServerApi} from "./net/HomeServerApi.js";
|
||||||
import {Reconnector, ConnectionStatus} from "./net/Reconnector.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 {RequestScheduler} from "./net/RequestScheduler.js";
|
||||||
import {Sync, SyncStatus} from "./Sync.js";
|
import {Sync, SyncStatus} from "./Sync.js";
|
||||||
import {Session} from "./Session.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(
|
export const LoadStatus = createEnum(
|
||||||
"NotLoading",
|
"NotLoading",
|
||||||
|
@ -42,14 +48,6 @@ export const LoginFailure = createEnum(
|
||||||
"Unknown",
|
"Unknown",
|
||||||
);
|
);
|
||||||
|
|
||||||
function normalizeHomeserver(homeServer) {
|
|
||||||
try {
|
|
||||||
return new URL(homeServer).origin;
|
|
||||||
} catch (err) {
|
|
||||||
return new URL(`https://${homeServer}`).origin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SessionContainer {
|
export class SessionContainer {
|
||||||
constructor({platform, olmPromise, workerPromise}) {
|
constructor({platform, olmPromise, workerPromise}) {
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
|
@ -97,25 +95,61 @@ export class SessionContainer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async startWithLogin(homeServer, username, password) {
|
_parseLoginOptions(options, homeserver) {
|
||||||
if (this._status.get() !== LoadStatus.NotLoading) {
|
/*
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
this._resetStatus();
|
||||||
await this._platform.logger.run("login", async log => {
|
await this._platform.logger.run("login", async log => {
|
||||||
this._status.set(LoadStatus.Login);
|
this._status.set(LoadStatus.Login);
|
||||||
homeServer = normalizeHomeserver(homeServer);
|
|
||||||
const clock = this._platform.clock;
|
const clock = this._platform.clock;
|
||||||
let sessionInfo;
|
let sessionInfo;
|
||||||
try {
|
try {
|
||||||
const request = this._platform.request;
|
const request = this._platform.request;
|
||||||
const hsApi = new HomeServerApi({homeServer, request});
|
const hsApi = new HomeServerApi({homeserver: loginMethod.homeserver, request});
|
||||||
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response();
|
const loginData = await loginMethod.login(hsApi, "Hydrogen", log);
|
||||||
const sessionId = this.createNewSessionId();
|
const sessionId = this.createNewSessionId();
|
||||||
sessionInfo = {
|
sessionInfo = {
|
||||||
id: sessionId,
|
id: sessionId,
|
||||||
deviceId: loginData.device_id,
|
deviceId: loginData.device_id,
|
||||||
userId: loginData.user_id,
|
userId: loginData.user_id,
|
||||||
homeServer: homeServer,
|
homeServer: loginMethod.homeserver, // deprecate this over time
|
||||||
|
homeserver: loginMethod.homeserver,
|
||||||
accessToken: loginData.access_token,
|
accessToken: loginData.access_token,
|
||||||
lastUsed: clock.now()
|
lastUsed: clock.now()
|
||||||
};
|
};
|
||||||
|
@ -164,7 +198,7 @@ export class SessionContainer {
|
||||||
createMeasure: clock.createMeasure
|
createMeasure: clock.createMeasure
|
||||||
});
|
});
|
||||||
const hsApi = new HomeServerApi({
|
const hsApi = new HomeServerApi({
|
||||||
homeServer: sessionInfo.homeServer,
|
homeserver: sessionInfo.homeServer,
|
||||||
accessToken: sessionInfo.accessToken,
|
accessToken: sessionInfo.accessToken,
|
||||||
request: this._platform.request,
|
request: this._platform.request,
|
||||||
reconnector: this._reconnector,
|
reconnector: this._reconnector,
|
||||||
|
@ -176,7 +210,7 @@ export class SessionContainer {
|
||||||
id: sessionInfo.id,
|
id: sessionInfo.id,
|
||||||
deviceId: sessionInfo.deviceId,
|
deviceId: sessionInfo.deviceId,
|
||||||
userId: sessionInfo.userId,
|
userId: sessionInfo.userId,
|
||||||
homeServer: sessionInfo.homeServer,
|
homeserver: sessionInfo.homeServer,
|
||||||
};
|
};
|
||||||
const olm = await this._olmPromise;
|
const olm = await this._olmPromise;
|
||||||
let olmWorker = null;
|
let olmWorker = null;
|
||||||
|
@ -186,7 +220,7 @@ export class SessionContainer {
|
||||||
this._requestScheduler = new RequestScheduler({hsApi, clock});
|
this._requestScheduler = new RequestScheduler({hsApi, clock});
|
||||||
this._requestScheduler.start();
|
this._requestScheduler.start();
|
||||||
const mediaRepository = new MediaRepository({
|
const mediaRepository = new MediaRepository({
|
||||||
homeServer: sessionInfo.homeServer,
|
homeserver: sessionInfo.homeServer,
|
||||||
platform: this._platform,
|
platform: this._platform,
|
||||||
});
|
});
|
||||||
this._session = new Session({
|
this._session = new Session({
|
||||||
|
@ -270,6 +304,10 @@ export class SessionContainer {
|
||||||
return this._error;
|
return this._error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get loginFailure() {
|
||||||
|
return this._loginFailure;
|
||||||
|
}
|
||||||
|
|
||||||
/** only set at loadStatus InitialSync, CatchupSync or Ready */
|
/** only set at loadStatus InitialSync, CatchupSync or Ready */
|
||||||
get sync() {
|
get sync() {
|
||||||
return this._sync;
|
return this._sync;
|
||||||
|
@ -319,4 +357,10 @@ export class SessionContainer {
|
||||||
this._sessionId = null;
|
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";
|
import {HomeServerRequest} from "./HomeServerRequest.js";
|
||||||
|
|
||||||
export class HomeServerApi {
|
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?
|
// 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
|
// 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._accessToken = accessToken;
|
||||||
this._requestFn = request;
|
this._requestFn = request;
|
||||||
this._reconnector = reconnector;
|
this._reconnector = reconnector;
|
||||||
|
@ -134,6 +134,10 @@ export class HomeServerApi {
|
||||||
return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options);
|
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) {
|
passwordLogin(username, password, initialDeviceDisplayName, options = null) {
|
||||||
return this._unauthedRequest("POST", this._url("/login"), null, {
|
return this._unauthedRequest("POST", this._url("/login"), null, {
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
|
@ -146,6 +150,18 @@ export class HomeServerApi {
|
||||||
}, options);
|
}, 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) {
|
createFilter(userId, filter, options = null) {
|
||||||
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
|
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options);
|
||||||
}
|
}
|
||||||
|
@ -218,7 +234,7 @@ export function tests() {
|
||||||
"superficial happy path for GET": async assert => {
|
"superficial happy path for GET": async assert => {
|
||||||
const hsApi = new HomeServerApi({
|
const hsApi = new HomeServerApi({
|
||||||
request: () => new MockRequest().respond(200, 42),
|
request: () => new MockRequest().respond(200, 42),
|
||||||
homeServer: "https://hs.tld"
|
homeserver: "https://hs.tld"
|
||||||
});
|
});
|
||||||
const result = await hsApi._get("foo", null, null, null).response();
|
const result = await hsApi._get("foo", null, null, null).response();
|
||||||
assert.strictEqual(result, 42);
|
assert.strictEqual(result, 42);
|
||||||
|
|
|
@ -18,8 +18,8 @@ import {encodeQueryParams} from "./common.js";
|
||||||
import {decryptAttachment} from "../e2ee/attachment.js";
|
import {decryptAttachment} from "../e2ee/attachment.js";
|
||||||
|
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor({homeServer, platform}) {
|
constructor({homeserver, platform}) {
|
||||||
this._homeServer = homeServer;
|
this._homeserver = homeserver;
|
||||||
this._platform = platform;
|
this._platform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export class MediaRepository {
|
||||||
const parts = this._parseMxcUrl(url);
|
const parts = this._parseMxcUrl(url);
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = 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 httpUrl + "?" + encodeQueryParams({width: Math.round(width), height: Math.round(height), method});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -37,7 +37,7 @@ export class MediaRepository {
|
||||||
const parts = this._parseMxcUrl(url);
|
const parts = this._parseMxcUrl(url);
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = 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 {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,7 +147,7 @@ export class RelationWriter {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_aggregateAnnotation(annotationEvent, targetStorageEntry, log) {
|
_aggregateAnnotation(annotationEvent, targetStorageEntry/*, log*/) {
|
||||||
// TODO: do we want to verify it is a m.reaction event somehow?
|
// TODO: do we want to verify it is a m.reaction event somehow?
|
||||||
const relation = getRelation(annotationEvent);
|
const relation = getRelation(annotationEvent);
|
||||||
if (!relation) {
|
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);
|
this.emitAdd(key, mappedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key, _value) {
|
onRemove(key/*, _value*/) {
|
||||||
const mappedValue = this._mappedValues.get(key);
|
const mappedValue = this._mappedValues.get(key);
|
||||||
if (this._mappedValues.delete(key)) {
|
if (this._mappedValues.delete(key)) {
|
||||||
this.emitRemove(key, mappedValue);
|
this.emitRemove(key, mappedValue);
|
||||||
|
|
|
@ -156,7 +156,7 @@ export function tests() {
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 5});
|
assert.deepEqual(value, {value: 5});
|
||||||
},
|
},
|
||||||
onUpdate(key, value, params) {
|
onUpdate(key, value/*, params*/) {
|
||||||
update_fired += 1;
|
update_fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 7});
|
assert.deepEqual(value, {value: 7});
|
||||||
|
|
|
@ -221,7 +221,7 @@ export class Platform {
|
||||||
if (mimeType) {
|
if (mimeType) {
|
||||||
input.setAttribute("accept", mimeType);
|
input.setAttribute("accept", mimeType);
|
||||||
}
|
}
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise(resolve => {
|
||||||
const checkFile = () => {
|
const checkFile = () => {
|
||||||
input.removeEventListener("change", checkFile, true);
|
input.removeEventListener("change", checkFile, true);
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
|
@ -240,6 +240,10 @@ export class Platform {
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openUrl(url) {
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
parseHTML(html) {
|
parseHTML(html) {
|
||||||
return parseHTML(html);
|
return parseHTML(html);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
downloadSandbox: "assets/download-sandbox.html",
|
downloadSandbox: "assets/download-sandbox.html",
|
||||||
defaultHomeServer: "matrix.org",
|
defaultHomeServer: "matrix.org",
|
||||||
// NOTE: uncomment this if you want the service worker for local development
|
// 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
|
// NOTE: provide push config if you want push notifs for local development
|
||||||
// see assets/config.json for what the config looks like
|
// see assets/config.json for what the config looks like
|
||||||
// push: {...},
|
// push: {...},
|
||||||
|
|
|
@ -25,6 +25,14 @@ export class History extends BaseObservableValue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get() {
|
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;
|
return document.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,14 @@ export class SettingsStorage {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setString(key, value) {
|
||||||
|
this._set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getString(key) {
|
||||||
|
return window.localStorage.getItem(`${this._prefix}${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
async remove(key) {
|
async remove(key) {
|
||||||
window.localStorage.removeItem(`${this._prefix}${key}`);
|
window.localStorage.removeItem(`${this._prefix}${key}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,19 +50,19 @@ limitations under the License.
|
||||||
margin: 0 20px;
|
margin: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.LoginView {
|
.PasswordLoginView {
|
||||||
padding: 0.4em;
|
padding: 0 0.4em 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView {
|
.SessionLoadStatusView, .LoginView_query-spinner {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView > :not(:first-child) {
|
.SessionLoadStatusView> :not(:first-child), .LoginView_query-spinner> :not(:first-child) {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SessionLoadStatusView p {
|
.SessionLoadStatusView p, .LoginView_query-spinner p {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -70,3 +70,29 @@ limitations under the License.
|
||||||
.SessionLoadStatusView .spinner {
|
.SessionLoadStatusView .spinner {
|
||||||
--size: 20px;
|
--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;
|
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) {
|
@media screen and (min-width: 600px) {
|
||||||
.PreSessionScreen {
|
.PreSessionScreen {
|
||||||
box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 6px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
|
|
@ -121,7 +121,7 @@ export class ListView {
|
||||||
this.onListChanged();
|
this.onListChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(idx, _value) {
|
onRemove(idx/*, _value*/) {
|
||||||
this.onBeforeListChanged();
|
this.onBeforeListChanged();
|
||||||
const [child] = this._childInstances.splice(idx, 1);
|
const [child] = this._childInstances.splice(idx, 1);
|
||||||
child.root().remove();
|
child.root().remove();
|
||||||
|
@ -129,7 +129,7 @@ export class ListView {
|
||||||
this.onListChanged();
|
this.onListChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMove(fromIdx, toIdx, value) {
|
onMove(fromIdx, toIdx/*, value*/) {
|
||||||
this.onBeforeListChanged();
|
this.onBeforeListChanged();
|
||||||
const [child] = this._childInstances.splice(fromIdx, 1);
|
const [child] = this._childInstances.splice(fromIdx, 1);
|
||||||
this._childInstances.splice(toIdx, 0, child);
|
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 {TemplateView} from "../general/TemplateView.js";
|
||||||
import {hydrogenGithubLink} from "./common.js";
|
import {hydrogenGithubLink} from "./common.js";
|
||||||
|
import {PasswordLoginView} from "./PasswordLoginView.js";
|
||||||
|
import {CompleteSSOView} from "./CompleteSSOView.js";
|
||||||
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
import {SessionLoadStatusView} from "./SessionLoadStatusView.js";
|
||||||
|
import {spinner} from "../common.js";
|
||||||
|
|
||||||
export class LoginView extends TemplateView {
|
export class LoginView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t, vm) {
|
||||||
const disabled = vm => !!vm.isBusy;
|
const disabled = vm => vm.isBusy;
|
||||||
const username = t.input({
|
|
||||||
id: "username",
|
return t.div({className: "PreSessionScreen"}, [
|
||||||
type: "text",
|
t.button({
|
||||||
placeholder: vm.i18n`Username`,
|
className: "button-utility LoginView_back",
|
||||||
|
onClick: () => vm.goBack(),
|
||||||
disabled
|
disabled
|
||||||
});
|
}),
|
||||||
const password = t.input({
|
t.div({className: "logo"}),
|
||||||
id: "password",
|
t.h1([vm.i18n`Sign In`]),
|
||||||
type: "password",
|
t.mapView(vm => vm.completeSSOLoginViewModel, vm => vm ? new CompleteSSOView(vm) : null),
|
||||||
placeholder: vm.i18n`Password`,
|
t.if(vm => vm.showHomeserver, (t, vm) => t.div({ className: "LoginView_sso form form-row" },
|
||||||
disabled
|
[
|
||||||
});
|
t.label({for: "homeserver"}, vm.i18n`Homeserver`),
|
||||||
const homeserver = t.input({
|
t.input({
|
||||||
id: "homeserver",
|
id: "homeserver",
|
||||||
type: "text",
|
type: "text",
|
||||||
placeholder: vm.i18n`Your matrix homeserver`,
|
placeholder: vm.i18n`Your matrix homeserver`,
|
||||||
value: vm.defaultHomeServer,
|
value: vm.homeserver,
|
||||||
disabled
|
disabled,
|
||||||
});
|
onInput: event => vm.setHomeserver(event.target.value),
|
||||||
|
onChange: () => vm.queryHomeserver(),
|
||||||
return t.div({className: "PreSessionScreen"}, [
|
}),
|
||||||
t.div({className: "logo"}),
|
t.p({className: {
|
||||||
t.div({className: "LoginView form"}, [
|
LoginView_forwardInfo: true,
|
||||||
t.h1([vm.i18n`Sign In`]),
|
hidden: vm => !vm.resolvedHomeserver
|
||||||
t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)),
|
}}, vm => vm.i18n`You will connect to ${vm.resolvedHomeserver}.`),
|
||||||
t.form({
|
t.if(vm => vm.errorMessage, (t, vm) => t.p({className: "error"}, vm.i18n(vm.errorMessage))),
|
||||||
onSubmit: evnt => {
|
]
|
||||||
evnt.preventDefault();
|
)),
|
||||||
vm.login(username.value, password.value, homeserver.value);
|
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.div({className: "form-row"}, [t.label({for: "username"}, vm.i18n`Username`), username]),
|
t.mapView(vm => vm.startSSOLoginViewModel, vm => vm ? new StartSSOLoginView(vm) : null),
|
||||||
t.div({className: "form-row"}, [t.label({for: "password"}, vm.i18n`Password`), password]),
|
|
||||||
t.div({className: "form-row"}, [t.label({for: "homeserver"}, vm.i18n`Homeserver`), homeserver]),
|
|
||||||
t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadStatusView(loadViewModel) : null),
|
t.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
|
// use t.mapView rather than t.if to create a new view when the view model changes too
|
||||||
t.p(hydrogenGithubLink(t))
|
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 {TemplateView} from "../../general/TemplateView.js";
|
||||||
import {Popup} from "../../general/Popup.js";
|
import {Popup} from "../../general/Popup.js";
|
||||||
import {Menu} from "../../general/Menu.js";
|
import {Menu} from "../../general/Menu.js";
|
||||||
import {TextMessageView} from "./timeline/TextMessageView.js";
|
|
||||||
import {viewClassForEntry} from "./TimelineList.js"
|
import {viewClassForEntry} from "./TimelineList.js"
|
||||||
|
|
||||||
export class MessageComposer extends TemplateView {
|
export class MessageComposer extends TemplateView {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import {TemplateView} from "../../general/TemplateView.js";
|
import {TemplateView} from "../../general/TemplateView.js";
|
||||||
|
|
||||||
export class RoomArchivedView extends TemplateView {
|
export class RoomArchivedView extends TemplateView {
|
||||||
render(t, vm) {
|
render(t) {
|
||||||
return t.div({className: "RoomArchivedView"}, t.h3(vm => vm.description));
|
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)),
|
link: linkPart => tag.a({href: linkPart.url, className: "link", target: "_blank", rel: "noopener" }, renderParts(linkPart.inlines)),
|
||||||
pill: renderPill,
|
pill: renderPill,
|
||||||
format: formatPart => tag[formatPart.format](renderParts(formatPart.children)),
|
format: formatPart => tag[formatPart.format](renderParts(formatPart.children)),
|
||||||
rule: rulePart => tag.hr(),
|
rule: () => tag.hr(),
|
||||||
list: renderList,
|
list: renderList,
|
||||||
image: renderImage,
|
image: renderImage,
|
||||||
newline: () => tag.br()
|
newline: () => tag.br()
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<script id="main" type="module">
|
<script id="main" type="module">
|
||||||
import {LoginView} from "./login/LoginView.js";
|
import {LoginView} from "./login/LoginView.js";
|
||||||
const view = new LoginView(vm({
|
const view = new LoginView(vm({
|
||||||
defaultHomeServer: "https://hs.tld",
|
defaultHomeserver: "https://hs.tld",
|
||||||
login: () => alert("Logging in!"),
|
login: () => alert("Logging in!"),
|
||||||
cancelUrl: "#/session"
|
cancelUrl: "#/session"
|
||||||
}));
|
}));
|
||||||
|
@ -60,7 +60,7 @@
|
||||||
loading: true,
|
loading: true,
|
||||||
}),
|
}),
|
||||||
cancelUrl: "#/session",
|
cancelUrl: "#/session",
|
||||||
defaultHomeServer: "https://hs.tld",
|
defaultHomeserver: "https://hs.tld",
|
||||||
}));
|
}));
|
||||||
document.getElementById("login-loading").appendChild(view.mount());
|
document.getElementById("login-loading").appendChild(view.mount());
|
||||||
</script>
|
</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() {
|
export function tests() {
|
||||||
|
|
Loading…
Reference in a new issue