From 65cca83f7fceb3094a2676384566d2d8ce2016dd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Mar 2020 22:24:38 +0200 Subject: [PATCH 01/93] notes how to implement reconnecting and status reporting --- doc/impl-thoughts/RECONNECTING.md | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index fb9ff506..f818b6fc 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -1 +1,34 @@ -# Reconnecting \ No newline at end of file +# Reconnecting + +`HomeServerApi` notifies `Reconnecter` of network call failure + +`Reconnecter` listens for online/offline event + +`Reconnecter` polls `/versions` with a `RetryDelay` (implemented as ExponentialRetryDelay, also used by SendScheduler if no retry_after_ms is given) + +`Reconnecter` emits an event when sync and message sending should retry + +`Sync` listen to `Reconnecter` +`Sync` notifies when the catchup sync has happened + +`Reconnecter` has state: + - disconnected (and retrying at x seconds from timestamp) + - reconnecting (call /versions, and if successful /sync) + - connected + +`Reconnecter` has a method to try to connect now + +`SessionStatus` can be: + - disconnected (and retrying at x seconds from timestamp) + - reconnecting + - connected (and syncing) + + - doing catchup sync + - sending x / y messages + +rooms should report how many messages they have queued up, and each time they sent one? + +`SendReporter` (passed from `Session` to `Room`, passed down to `SendQueue`), with: + - setPendingEventCount(roomId, count) + +`Session` listens to `Reconnecter` to update it's status, but perhaps we wait to send messages until catchup sync is done From b6a5a02a33fd83bf29be2c1644e3b522675d4f35 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Mar 2020 23:56:03 +0200 Subject: [PATCH 02/93] WIP --- src/matrix/Reconnecter.js | 164 ++++++++++++++++++++++++++++++++++++++ src/matrix/hs-api.js | 15 ++-- 2 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 src/matrix/Reconnecter.js diff --git a/src/matrix/Reconnecter.js b/src/matrix/Reconnecter.js new file mode 100644 index 00000000..ccff2203 --- /dev/null +++ b/src/matrix/Reconnecter.js @@ -0,0 +1,164 @@ +class Clock { + // use cases + // StopWatch: not sure I like that name ... but measure time difference from start to current time + // Timeout: wait for a given number of ms, and be able to interrupt the wait + // Clock.timeout() -> creates a new timeout? + // Now: get current timestamp + // Clock.now(), or pass Clock.now so others can do now() + // + // should use subinterfaces so we can only pass part needed to other constructors + // +} + + +// need to prevent memory leaks here! +export class DomOnlineDetected { + constructor(reconnecter) { + // window.addEventListener('offline', () => appendOnlineStatus(false)); + // window.addEventListener('online', () => appendOnlineStatus(true)); + // appendOnlineStatus(navigator.onLine); + // on online, reconnecter.tryNow() + } +} + +export class ExponentialRetryDelay { + constructor(start = 2000, delay) { + this._start = start; + this._current = start; + this._delay = delay; + this._max = 60 * 5 * 1000; //5 min + this._timer = null; + } + + async waitForRetry() { + this._timer = this._delay(this._current); + try { + await this._timer.timeout(); + // only increase delay if we didn't get interrupted + const seconds = this._current / 1000; + const powerOfTwo = (seconds * seconds) * 1000; + this._current = Math.max(this._max, powerOfTwo); + } finally { + this._timer = null; + } + } + + reset() { + this._current = this._start; + if (this._timer) { + this._timer.abort(); + } + } + + get nextValue() { + return this._current; + } +} + +// we need a clock interface that gives us both timestamps and a timer that we can interrupt? + +// state +// - offline +// - waiting to reconnect +// - reconnecting +// - online +// +// + +function createEnum(...values) { + const obj = {}; + for (const value of values) { + obj[value] = value; + } + return Object.freeze(obj); +} + +export const ConnectionState = createEnum( + "Offline", + "Waiting", + "Reconnecting", + "Online" +); + +export class Reconnecter { + constructor({hsApi, retryDelay, clock}) { + this._online + this._retryDelay = retryDelay; + this._currentDelay = null; + this._hsApi = hsApi; + this._clock = clock; + // assume online, and do our thing when something fails + this._state = ConnectionState.Online; + this._isReconnecting = false; + this._versionsResponse = null; + } + + get lastVersionsResponse() { + return this._versionsResponse; + } + + get state() { + return this._state; + } + + get retryIn() { + return this._stateSince.measure(); + } + + onRequestFailed() { + if (!this._isReconnecting) { + this._setState(ConnectionState.Offline); + // do something with versions response of loop here? + // we might want to pass it to session to know what server supports? + // so emit it ... + this._reconnectLoop(); + // start loop + } + } + + // don't throw from here + tryNow() { + // skip waiting + if (this._currentDelay) { + this._currentDelay.abort(); + } + } + + _setState(state) { + if (state !== this._state) { + this._state = state; + if (this._state === ConnectionState.Waiting) { + this._stateSince = this._clock.stopwatch(); + } else { + this._stateSince = null; + } + this.emit("change", state); + } + } + + async _reconnectLoop() { + this._isReconnecting = true; + this._retryDelay.reset(); + this._versionsResponse = null; + + while (!this._versionsResponse) { + try { + this._setState(ConnectionState.Reconnecting); + const versionsRequest = this._hsApi.versions(10000); + this._versionsResponse = await versionsRequest.response(); + this._setState(ConnectionState.Online); + } catch (err) { + this._setState(ConnectionState.Waiting); + this._currentDelay = this._retryDelay.next(); + try { + await this._currentDelay + } catch (err) { + // waiting interrupted, we should retry immediately, + // swallow error + } finally { + this._currentDelay = null; + } + } + } + } +} diff --git a/src/matrix/hs-api.js b/src/matrix/hs-api.js index ec8adad0..4a2747d7 100644 --- a/src/matrix/hs-api.js +++ b/src/matrix/hs-api.js @@ -40,7 +40,7 @@ export default class HomeServerApi { return `${this._homeserver}/_matrix/client/r0${csPath}`; } - _request(method, csPath, queryParams = {}, body) { + _request(method, url, queryParams = {}, body) { const queryString = Object.entries(queryParams) .filter(([, value]) => value !== undefined) .map(([name, value]) => { @@ -50,7 +50,7 @@ export default class HomeServerApi { return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; }) .join("&"); - const url = this._url(`${csPath}?${queryString}`); + url = `${url}?${queryString}`; let bodyString; const headers = new Headers(); if (this._accessToken) { @@ -70,15 +70,15 @@ export default class HomeServerApi { } _post(csPath, queryParams, body) { - return this._request("POST", csPath, queryParams, body); + return this._request("POST", this._url(csPath), queryParams, body); } _put(csPath, queryParams, body) { - return this._request("PUT", csPath, queryParams, body); + return this._request("PUT", this._url(csPath), queryParams, body); } _get(csPath, queryParams, body) { - return this._request("GET", csPath, queryParams, body); + return this._request("GET", this._url(csPath), queryParams, body); } sync(since, filter, timeout) { @@ -108,4 +108,9 @@ export default class HomeServerApi { createFilter(userId, filter) { return this._post(`/user/${encodeURIComponent(userId)}/filter`, undefined, filter); } + + versions(timeout) { + // TODO: implement timeout + return this._request("GET", `${this._homeserver}/_matrix/client/versions`); + } } From bc69e49cfb1b48d3f283a264eac333c7ac113f67 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Mar 2020 10:13:25 +0200 Subject: [PATCH 03/93] WIP2 --- src/matrix/Reconnecter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/Reconnecter.js b/src/matrix/Reconnecter.js index ccff2203..863a263c 100644 --- a/src/matrix/Reconnecter.js +++ b/src/matrix/Reconnecter.js @@ -142,6 +142,8 @@ export class Reconnecter { this._versionsResponse = null; while (!this._versionsResponse) { + // TODO: should we wait first or request first? + // as we've just failed a request? I guess no harm in trying immediately try { this._setState(ConnectionState.Reconnecting); const versionsRequest = this._hsApi.versions(10000); From c980f682c64bbcbc153bce7aac0b7c87bcda20af Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 4 Apr 2020 17:34:46 +0200 Subject: [PATCH 04/93] create DOMClock, abstraction of clock functionalities for DOM --- src/matrix/error.js | 3 +-- src/matrix/net/fetch.js | 4 ++-- src/matrix/net/replay.js | 6 ++--- src/matrix/sync.js | 4 ++-- src/utils/DOMClock.js | 49 ++++++++++++++++++++++++++++++++++++++++ src/utils/error.js | 2 ++ 6 files changed, 59 insertions(+), 9 deletions(-) create mode 100644 src/utils/DOMClock.js create mode 100644 src/utils/error.js diff --git a/src/matrix/error.js b/src/matrix/error.js index cf6db9c5..42dcac73 100644 --- a/src/matrix/error.js +++ b/src/matrix/error.js @@ -12,8 +12,7 @@ export class HomeServerError extends Error { } } -export class RequestAbortError extends Error { -} +export {AbortError} from "../utils/error.js"; export class NetworkError extends Error { } diff --git a/src/matrix/net/fetch.js b/src/matrix/net/fetch.js index 48a13969..36713475 100644 --- a/src/matrix/net/fetch.js +++ b/src/matrix/net/fetch.js @@ -1,5 +1,5 @@ import { - RequestAbortError, + AbortError, NetworkError } from "../error.js"; @@ -50,7 +50,7 @@ export default function fetchRequest(url, options) { return {status, body}; }, err => { if (err.name === "AbortError") { - throw new RequestAbortError(); + throw new AbortError(); } else if (err instanceof TypeError) { // Network errors are reported as TypeErrors, see // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful diff --git a/src/matrix/net/replay.js b/src/matrix/net/replay.js index b6ddcb74..f47e6d47 100644 --- a/src/matrix/net/replay.js +++ b/src/matrix/net/replay.js @@ -1,5 +1,5 @@ import { - RequestAbortError, + AbortError, NetworkError } from "../error.js"; @@ -23,7 +23,7 @@ class RequestLogItem { handleError(err) { this.end = performance.now(); this.error = { - aborted: err instanceof RequestAbortError, + aborted: err instanceof AbortError, network: err instanceof NetworkError, message: err.message, }; @@ -96,7 +96,7 @@ class ReplayRequestResult { if (this._item.error || this._aborted) { const error = this._item.error; if (error.aborted || this._aborted) { - throw new RequestAbortError(error.message); + throw new AbortError(error.message); } else if (error.network) { throw new NetworkError(error.message); } else { diff --git a/src/matrix/sync.js b/src/matrix/sync.js index fb31e422..bc5f5797 100644 --- a/src/matrix/sync.js +++ b/src/matrix/sync.js @@ -1,4 +1,4 @@ -import {RequestAbortError} from "./error.js"; +import {AbortError} from "./error.js"; import EventEmitter from "../EventEmitter.js"; const INCREMENTAL_TIMEOUT = 30000; @@ -57,7 +57,7 @@ export default class Sync extends EventEmitter { syncToken = await this._syncRequest(syncToken, INCREMENTAL_TIMEOUT); } catch (err) { this._isSyncing = false; - if (!(err instanceof RequestAbortError)) { + if (!(err instanceof AbortError)) { console.error("stopping sync because of error"); console.error(err); this.emit("status", "error", err); diff --git a/src/utils/DOMClock.js b/src/utils/DOMClock.js new file mode 100644 index 00000000..eeb5ebcc --- /dev/null +++ b/src/utils/DOMClock.js @@ -0,0 +1,49 @@ +import {AbortError} from "./error.js"; + +class DOMTimeout { + constructor(ms) { + this._reject = null; + this._promise = new Promise((resolve, reject) => { + this._reject = reject; + setTimeout(() => { + this._reject = null; + resolve(); + }, ms); + }); + } + + get elapsed() { + return this._promise; + } + + abort() { + if (this._reject) { + this._reject(new AbortError()); + this._reject = null; + } + } +} + +class DOMTimeMeasure { + constructor() { + this._start = window.performance.now(); + } + + measure() { + return window.performance.now() - this._start; + } +} + +export class DOMClock { + createMeasure() { + return new DOMTimeMeasure(); + } + + createTimeout(ms) { + return new DOMTimeout(ms); + } + + now() { + return Date.now(); + } +} diff --git a/src/utils/error.js b/src/utils/error.js new file mode 100644 index 00000000..f10f7590 --- /dev/null +++ b/src/utils/error.js @@ -0,0 +1,2 @@ +export class AbortError extends Error { +} \ No newline at end of file From ef267ca3313eedd9fa3c7a42c9f265d3f3cd108a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 5 Apr 2020 15:11:15 +0200 Subject: [PATCH 05/93] WIP2 --- doc/impl-thoughts/RECONNECTING.md | 16 ++-- src/domain/BrawlViewModel.js | 16 +++- src/main.js | 6 +- src/matrix/{Reconnecter.js => Reconnector.js} | 93 ++++++++----------- src/matrix/hs-api.js | 71 +++++++++----- src/matrix/session.js | 2 +- src/matrix/sync.js | 1 + src/utils/DOMClock.js | 7 +- 8 files changed, 118 insertions(+), 94 deletions(-) rename src/matrix/{Reconnecter.js => Reconnector.js} (56%) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index f818b6fc..f648481f 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -1,22 +1,22 @@ # Reconnecting -`HomeServerApi` notifies `Reconnecter` of network call failure +`HomeServerApi` notifies `Reconnector` of network call failure -`Reconnecter` listens for online/offline event +`Reconnector` listens for online/offline event -`Reconnecter` polls `/versions` with a `RetryDelay` (implemented as ExponentialRetryDelay, also used by SendScheduler if no retry_after_ms is given) +`Reconnector` polls `/versions` with a `RetryDelay` (implemented as ExponentialRetryDelay, also used by SendScheduler if no retry_after_ms is given) -`Reconnecter` emits an event when sync and message sending should retry +`Reconnector` emits an event when sync and message sending should retry -`Sync` listen to `Reconnecter` +`Sync` listen to `Reconnector` `Sync` notifies when the catchup sync has happened -`Reconnecter` has state: +`Reconnector` has state: - disconnected (and retrying at x seconds from timestamp) - reconnecting (call /versions, and if successful /sync) - connected -`Reconnecter` has a method to try to connect now +`Reconnector` has a method to try to connect now `SessionStatus` can be: - disconnected (and retrying at x seconds from timestamp) @@ -31,4 +31,4 @@ rooms should report how many messages they have queued up, and each time they se `SendReporter` (passed from `Session` to `Room`, passed down to `SendQueue`), with: - setPendingEventCount(roomId, count) -`Session` listens to `Reconnecter` to update it's status, but perhaps we wait to send messages until catchup sync is done +`Session` listens to `Reconnector` to update it's status, but perhaps we wait to send messages until catchup sync is done diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index c9ebfa91..20bddd7f 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -126,7 +126,11 @@ export default class BrawlViewModel extends EventEmitter { try { this._loading = true; this._loadingText = "Loading your conversations…"; - const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken); + const reconnector = new Reconnector( + new ExponentialRetryDelay(2000, this._clock.createTimeout), + this._clock.createMeasure + ); + const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, reconnector); const storage = await this._storageFactory.create(sessionInfo.id); // no need to pass access token to session const filteredSessionInfo = { @@ -136,10 +140,16 @@ export default class BrawlViewModel extends EventEmitter { }; const session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); // show spinner now, with title loading stored data? - this.emit("change", "activeSection"); await session.load(); - const sync = new Sync({hsApi, storage, session}); + const sync = new Sync({hsApi, storage, session}); + + reconnector.on("state", state => { + if (state === ConnectionState.Online) { + sync.start(); + session.notifyNetworkAvailable(reconnector.lastVersionsResponse); + } + }); const needsInitialSync = !session.syncToken; if (!needsInitialSync) { diff --git a/src/main.js b/src/main.js index 9e973bb9..b8e4ab33 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import StorageFactory from "./matrix/storage/idb/create.js"; import SessionsStore from "./matrix/sessions-store/localstorage/SessionsStore.js"; import BrawlViewModel from "./domain/BrawlViewModel.js"; import BrawlView from "./ui/web/BrawlView.js"; +import DOMClock from "./utils/DOMClock.js"; export default async function main(container) { try { @@ -17,14 +18,13 @@ export default async function main(container) { // const recorder = new RecordRequester(fetchRequest); // const request = recorder.request; // window.getBrawlFetchLog = () => recorder.log(); - // normal network: const request = fetchRequest; const vm = new BrawlViewModel({ storageFactory: new StorageFactory(), - createHsApi: (homeServer, accessToken = null) => new HomeServerApi({homeServer, accessToken, request}), + createHsApi: (homeServer, accessToken, reconnector) => new HomeServerApi({homeServer, accessToken, request, reconnector}), sessionStore: new SessionsStore("brawl_sessions_v1"), - clock: Date //just for `now` fn + clock: new DOMClock(), }); await vm.load(); const view = new BrawlView(vm); diff --git a/src/matrix/Reconnecter.js b/src/matrix/Reconnector.js similarity index 56% rename from src/matrix/Reconnecter.js rename to src/matrix/Reconnector.js index 863a263c..2125380e 100644 --- a/src/matrix/Reconnecter.js +++ b/src/matrix/Reconnector.js @@ -1,53 +1,49 @@ -class Clock { - // use cases - // StopWatch: not sure I like that name ... but measure time difference from start to current time - // Timeout: wait for a given number of ms, and be able to interrupt the wait - // Clock.timeout() -> creates a new timeout? - // Now: get current timestamp - // Clock.now(), or pass Clock.now so others can do now() - // - // should use subinterfaces so we can only pass part needed to other constructors - // -} - - // need to prevent memory leaks here! export class DomOnlineDetected { - constructor(reconnecter) { + constructor(reconnector) { // window.addEventListener('offline', () => appendOnlineStatus(false)); // window.addEventListener('online', () => appendOnlineStatus(true)); // appendOnlineStatus(navigator.onLine); - // on online, reconnecter.tryNow() + // on online, reconnector.tryNow() } } export class ExponentialRetryDelay { - constructor(start = 2000, delay) { + constructor(start = 2000, createTimeout) { this._start = start; this._current = start; - this._delay = delay; + this._createTimeout = createTimeout; this._max = 60 * 5 * 1000; //5 min - this._timer = null; + this._timeout = null; } async waitForRetry() { - this._timer = this._delay(this._current); + this._timeout = this._createTimeout(this._current); try { - await this._timer.timeout(); + await this._timeout.elapsed(); // only increase delay if we didn't get interrupted const seconds = this._current / 1000; const powerOfTwo = (seconds * seconds) * 1000; this._current = Math.max(this._max, powerOfTwo); + } catch(err) { + // swallow AbortError, means skipWaiting was called + if (!(err instanceof AbortError)) { + throw err; + } } finally { - this._timer = null; + this._timeout = null; + } + } + + skipWaiting() { + if (this._timeout) { + this._timeout.abort(); } } reset() { this._current = this._start; - if (this._timer) { - this._timer.abort(); - } + this.skipWaiting(); } get nextValue() { @@ -80,13 +76,12 @@ export const ConnectionState = createEnum( "Online" ); -export class Reconnecter { - constructor({hsApi, retryDelay, clock}) { +export class Reconnector { + constructor({retryDelay, createTimeMeasure}) { this._online this._retryDelay = retryDelay; this._currentDelay = null; - this._hsApi = hsApi; - this._clock = clock; + this._createTimeMeasure = createTimeMeasure; // assume online, and do our thing when something fails this._state = ConnectionState.Online; this._isReconnecting = false; @@ -102,25 +97,22 @@ export class Reconnecter { } get retryIn() { - return this._stateSince.measure(); + if (this._state === ConnectionState.Waiting) { + return this._retryDelay.nextValue - this._stateSince.measure(); + } + return 0; } - onRequestFailed() { + onRequestFailed(hsApi) { if (!this._isReconnecting) { this._setState(ConnectionState.Offline); - // do something with versions response of loop here? - // we might want to pass it to session to know what server supports? - // so emit it ... - this._reconnectLoop(); - // start loop + this._reconnectLoop(hsApi); } } - // don't throw from here tryNow() { - // skip waiting - if (this._currentDelay) { - this._currentDelay.abort(); + if (this._retryDelay) { + this._retryDelay.skipWaiting(); } } @@ -128,7 +120,7 @@ export class Reconnecter { if (state !== this._state) { this._state = state; if (this._state === ConnectionState.Waiting) { - this._stateSince = this._clock.stopwatch(); + this._stateSince = this._createTimeMeasure(); } else { this._stateSince = null; } @@ -136,30 +128,23 @@ export class Reconnecter { } } - async _reconnectLoop() { + async _reconnectLoop(hsApi) { this._isReconnecting = true; - this._retryDelay.reset(); this._versionsResponse = null; + this._retryDelay.reset(); while (!this._versionsResponse) { - // TODO: should we wait first or request first? - // as we've just failed a request? I guess no harm in trying immediately try { this._setState(ConnectionState.Reconnecting); - const versionsRequest = this._hsApi.versions(10000); + // use 10s timeout, because we don't want to be waiting for + // a stale connection when we just came online again + const versionsRequest = hsApi.versions({timeout: 10000}); this._versionsResponse = await versionsRequest.response(); this._setState(ConnectionState.Online); } catch (err) { + // NetworkError or AbortError from timeout this._setState(ConnectionState.Waiting); - this._currentDelay = this._retryDelay.next(); - try { - await this._currentDelay - } catch (err) { - // waiting interrupted, we should retry immediately, - // swallow error - } finally { - this._currentDelay = null; - } + await this._retryDelay.waitForRetry(); } } } diff --git a/src/matrix/hs-api.js b/src/matrix/hs-api.js index 4a2747d7..314a9324 100644 --- a/src/matrix/hs-api.js +++ b/src/matrix/hs-api.js @@ -1,5 +1,6 @@ import { HomeServerError, + NetworkError, } from "./error.js"; class RequestWrapper { @@ -28,19 +29,21 @@ class RequestWrapper { } export default class HomeServerApi { - constructor({homeServer, accessToken, request}) { + constructor({homeServer, accessToken, request, createTimeout, reconnector}) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write this._homeserver = homeServer; this._accessToken = accessToken; this._requestFn = request; + this._createTimeout = createTimeout; + this._reconnector = reconnector; } _url(csPath) { return `${this._homeserver}/_matrix/client/r0${csPath}`; } - _request(method, url, queryParams = {}, body) { + _request(method, url, queryParams = {}, body, options) { const queryString = Object.entries(queryParams) .filter(([, value]) => value !== undefined) .map(([name, value]) => { @@ -66,51 +69,73 @@ export default class HomeServerApi { headers, body: bodyString, }); - return new RequestWrapper(method, url, requestResult); + + if (options.timeout) { + const timeout = this._createTimeout(options.timeout); + // abort request if timeout finishes first + timeout.elapsed().then( + () => requestResult.abort(), + () => {} // ignore AbortError + ); + // abort timeout if request finishes first + requestResult.response().then(() => timeout.abort()); + } + + + const wrapper = new RequestWrapper(method, url, requestResult); + + if (this._reconnector) { + wrapper.response().catch(err => { + if (err instanceof NetworkError) { + this._reconnector.onRequestFailed(this); + } + }); + } + + return wrapper; } - _post(csPath, queryParams, body) { - return this._request("POST", this._url(csPath), queryParams, body); + _post(csPath, queryParams, body, options) { + return this._request("POST", this._url(csPath), queryParams, body, options); } - _put(csPath, queryParams, body) { - return this._request("PUT", this._url(csPath), queryParams, body); + _put(csPath, queryParams, body, options) { + return this._request("PUT", this._url(csPath), queryParams, body, options); } - _get(csPath, queryParams, body) { - return this._request("GET", this._url(csPath), queryParams, body); + _get(csPath, queryParams, body, options) { + return this._request("GET", this._url(csPath), queryParams, body, options); } - sync(since, filter, timeout) { - return this._get("/sync", {since, timeout, filter}); + sync(since, filter, timeout, options = null) { + return this._get("/sync", {since, timeout, filter}, null, options); } // params is from, dir and optionally to, limit, filter. - messages(roomId, params) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params); + messages(roomId, params, options = null) { + return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); } - send(roomId, eventType, txnId, content) { - return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content); + send(roomId, eventType, txnId, content, options = null) { + return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); } - passwordLogin(username, password) { - return this._post("/login", undefined, { + passwordLogin(username, password, options = null) { + return this._post("/login", null, { "type": "m.login.password", "identifier": { "type": "m.id.user", "user": username }, "password": password - }); + }, options); } - createFilter(userId, filter) { - return this._post(`/user/${encodeURIComponent(userId)}/filter`, undefined, filter); + createFilter(userId, filter, options = null) { + return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); } - versions(timeout) { - // TODO: implement timeout - return this._request("GET", `${this._homeserver}/_matrix/client/versions`); + versions(options = null) { + return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, options); } } diff --git a/src/matrix/session.js b/src/matrix/session.js index 33cd16aa..837cb51a 100644 --- a/src/matrix/session.js +++ b/src/matrix/session.js @@ -40,7 +40,7 @@ export default class Session { })); } - notifyNetworkAvailable() { + notifyNetworkAvailable(lastVersionResponse) { for (const [, room] of this._rooms) { room.resumeSending(); } diff --git a/src/matrix/sync.js b/src/matrix/sync.js index bc5f5797..56208198 100644 --- a/src/matrix/sync.js +++ b/src/matrix/sync.js @@ -33,6 +33,7 @@ export default class Sync extends EventEmitter { return this._isSyncing; } + // this should not throw? // returns when initial sync is done async start() { if (this._isSyncing) { diff --git a/src/utils/DOMClock.js b/src/utils/DOMClock.js index eeb5ebcc..c943b109 100644 --- a/src/utils/DOMClock.js +++ b/src/utils/DOMClock.js @@ -3,22 +3,25 @@ import {AbortError} from "./error.js"; class DOMTimeout { constructor(ms) { this._reject = null; + this._handle = null; this._promise = new Promise((resolve, reject) => { this._reject = reject; - setTimeout(() => { + this._handle = setTimeout(() => { this._reject = null; resolve(); }, ms); }); } - get elapsed() { + elapsed() { return this._promise; } abort() { if (this._reject) { this._reject(new AbortError()); + clearTimeout(this._handle); + this._handle = null; this._reject = null; } } From 378b75c98a7626184f7394186839ce07fbfac8ad Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 9 Apr 2020 23:19:49 +0200 Subject: [PATCH 06/93] more WIP and breakage --- src/domain/BrawlViewModel.js | 117 +++++++----------- src/domain/ViewModel.js | 19 +++ src/domain/session/SessionLoadViewModel.js | 43 +++++++ src/domain/session/SessionViewModel.js | 7 ++ src/matrix/Reconnector.js | 14 +-- src/matrix/SessionContainer.js | 95 ++++++++++++++ src/observable/BaseObservableCollection.js | 9 ++ .../DOMClock.js => ui/web/dom/Clock.js} | 12 +- src/ui/web/dom/Online.js | 29 +++++ src/ui/web/general/SwitchView.js | 35 ++++++ src/utils/Disposables.js | 37 ++++++ 11 files changed, 326 insertions(+), 91 deletions(-) create mode 100644 src/domain/ViewModel.js create mode 100644 src/domain/session/SessionLoadViewModel.js create mode 100644 src/matrix/SessionContainer.js rename src/{utils/DOMClock.js => ui/web/dom/Clock.js} (82%) create mode 100644 src/ui/web/dom/Online.js create mode 100644 src/utils/Disposables.js diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 20bddd7f..3ed861fe 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -20,6 +20,7 @@ export default class BrawlViewModel extends EventEmitter { this._loading = false; this._error = null; this._sessionViewModel = null; + this._sessionSubscription = null; this._loginViewModel = null; this._sessionPickerViewModel = null; } @@ -33,45 +34,35 @@ export default class BrawlViewModel extends EventEmitter { } async _showPicker() { - this._clearSections(); - this._sessionPickerViewModel = new SessionPickerViewModel({ - sessionStore: this._sessionStore, - storageFactory: this._storageFactory, - sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) + this._setSection(() => { + this._sessionPickerViewModel = new SessionPickerViewModel({ + sessionStore: this._sessionStore, + storageFactory: this._storageFactory, + sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) + }); }); - this.emit("change", "activeSection"); try { await this._sessionPickerViewModel.load(); } catch (err) { - this._clearSections(); - this._error = err; - this.emit("change", "activeSection"); + this._setSection(() => this._error = err); } } _showLogin() { - this._clearSections(); - this._loginViewModel = new LoginViewModel({ - createHsApi: this._createHsApi, - defaultHomeServer: "https://matrix.org", - loginCallback: loginData => this._onLoginFinished(loginData) - }); - this.emit("change", "activeSection"); + this._setSection(() => { + this._loginViewModel = new LoginViewModel({ + createHsApi: this._createHsApi, + defaultHomeServer: "https://matrix.org", + loginCallback: loginData => this._onLoginFinished(loginData) + }); + }) } _showSession(session, sync) { - this._clearSections(); - this._sessionViewModel = new SessionViewModel({session, sync}); - this.emit("change", "activeSection"); - } - - _clearSections() { - this._error = null; - this._loading = false; - this._sessionViewModel = null; - this._loginViewModel = null; - this._sessionPickerViewModel = null; + this._setSection(() => { + this._sessionViewModel = new SessionViewModel({session, sync}); + }); } get activeSection() { @@ -88,6 +79,25 @@ export default class BrawlViewModel extends EventEmitter { } } + _setSection(setter) { + const oldSection = this.activeSection; + // clear all members the activeSection depends on + this._error = null; + this._loading = false; + this._sessionViewModel = null; + this._loginViewModel = null; + this._sessionPickerViewModel = null; + // now set it again + setter(); + const newSection = this.activeSection; + // remove session subscription when navigating away + if (oldSection === "session" && newSection !== oldSection) { + this._sessionSubscription(); + this._sessionSubscription = null; + } + this.emit("change", "activeSection"); + } + get loadingText() { return this._loadingText; } get sessionViewModel() { return this._sessionViewModel; } get loginViewModel() { return this._loginViewModel; } @@ -123,51 +133,12 @@ export default class BrawlViewModel extends EventEmitter { } async _loadSession(sessionInfo) { - try { - this._loading = true; - this._loadingText = "Loading your conversations…"; - const reconnector = new Reconnector( - new ExponentialRetryDelay(2000, this._clock.createTimeout), - this._clock.createMeasure - ); - const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, reconnector); - const storage = await this._storageFactory.create(sessionInfo.id); - // no need to pass access token to session - const filteredSessionInfo = { - deviceId: sessionInfo.deviceId, - userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, - }; - const session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); - // show spinner now, with title loading stored data? - this.emit("change", "activeSection"); - await session.load(); - const sync = new Sync({hsApi, storage, session}); - - reconnector.on("state", state => { - if (state === ConnectionState.Online) { - sync.start(); - session.notifyNetworkAvailable(reconnector.lastVersionsResponse); - } - }); - - const needsInitialSync = !session.syncToken; - if (!needsInitialSync) { - this._showSession(session, sync); - } - this._loadingText = "Getting your conversations from the server…"; - this.emit("change", "loadingText"); - // update spinner title to initial sync - await sync.start(); - if (needsInitialSync) { - this._showSession(session, sync); - } - // start sending pending messages - session.notifyNetworkAvailable(); - } catch (err) { - console.error(err); - this._error = err; - } - this.emit("change", "activeSection"); + this._setSection(() => { + // TODO this is pseudo code-ish + const container = this._createSessionContainer(); + this._sessionViewModel = new SessionViewModel({session, sync}); + this._sessionSubscription = this._activeSessionContainer.subscribe(this._updateSessionState); + this._activeSessionContainer.start(sessionInfo); + }); } } diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js new file mode 100644 index 00000000..2e26faae --- /dev/null +++ b/src/domain/ViewModel.js @@ -0,0 +1,19 @@ +export class ViewModel extends ObservableValue { + constructor(options) { + super(); + this.disposables = new Disposables(); + this._options = options; + } + + childOptions(explicitOptions) { + return Object.assign({}, this._options, explicitOptions); + } + + track(disposable) { + this.disposables.track(disposable); + } + + dispose() { + this.disposables.dispose(); + } +} diff --git a/src/domain/session/SessionLoadViewModel.js b/src/domain/session/SessionLoadViewModel.js new file mode 100644 index 00000000..3f3edba6 --- /dev/null +++ b/src/domain/session/SessionLoadViewModel.js @@ -0,0 +1,43 @@ +import EventEmitter from "../../EventEmitter.js"; +import RoomTileViewModel from "./roomlist/RoomTileViewModel.js"; +import RoomViewModel from "./room/RoomViewModel.js"; +import SyncStatusViewModel from "./SyncStatusViewModel.js"; + +export default class SessionLoadViewModel extends ViewModel { + constructor(options) { + super(options); + this._sessionContainer = options.sessionContainer; + this._updateState(); + } + + onSubscribeFirst() { + this.track(this._sessionContainer.subscribe(this._updateState)); + } + + _updateState(previousState) { + const state = this._sessionContainer.state; + if (previousState !== LoadState.Ready && state === LoadState.Ready) { + this._sessionViewModel = new SessionViewModel(this.childOptions({ + sessionContainer: this._sessionContainer + })); + this.track(this._sessionViewModel); + } else if (previousState === LoadState.Ready && state !== LoadState.Ready) { + this.disposables.disposeTracked(this._sessionViewModel); + this._sessionViewModel = null; + } + this.emit(); + } + + get isLoading() { + const state = this._sessionContainer.state; + return state === LoadState.Loading || state === LoadState.InitialSync; + } + + get loadingLabel() { + switch (this._sessionContainer.state) { + case LoadState.Loading: return "Loading your conversations…"; + case LoadState.InitialSync: return "Getting your conversations from the server…"; + default: return null; + } + } +} diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 28bc1277..74e1f00c 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -31,6 +31,13 @@ export default class SessionViewModel extends EventEmitter { return this._currentRoomViewModel; } + dispose() { + if (this._currentRoomViewModel) { + this._currentRoomViewModel.dispose(); + this._currentRoomViewModel = null; + } + } + _closeCurrentRoom() { if (this._currentRoomViewModel) { this._currentRoomViewModel.dispose(); diff --git a/src/matrix/Reconnector.js b/src/matrix/Reconnector.js index 2125380e..0cb85851 100644 --- a/src/matrix/Reconnector.js +++ b/src/matrix/Reconnector.js @@ -1,13 +1,3 @@ -// need to prevent memory leaks here! -export class DomOnlineDetected { - constructor(reconnector) { - // window.addEventListener('offline', () => appendOnlineStatus(false)); - // window.addEventListener('online', () => appendOnlineStatus(true)); - // appendOnlineStatus(navigator.onLine); - // on online, reconnector.tryNow() - } -} - export class ExponentialRetryDelay { constructor(start = 2000, createTimeout) { this._start = start; @@ -76,7 +66,7 @@ export const ConnectionState = createEnum( "Online" ); -export class Reconnector { +export class Reconnector extends ObservableValue { constructor({retryDelay, createTimeMeasure}) { this._online this._retryDelay = retryDelay; @@ -124,7 +114,7 @@ export class Reconnector { } else { this._stateSince = null; } - this.emit("change", state); + this.emit(state); } } diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js new file mode 100644 index 00000000..86be76d9 --- /dev/null +++ b/src/matrix/SessionContainer.js @@ -0,0 +1,95 @@ +const factory = { + Clock: () => new DOMClock(), + Request: () => fetchRequest, + Online: () => new DOMOnline(), + HomeServerApi: () +} + +export const LoadState = createEnum( + "Loading", + "InitialSync", + "Migrating", //not used atm, but would fit here + "Error", + "Ready", +); + +class SessionContainer extends ObservableValue { + constructor({clock, random, isOnline, request, storageFactory, factory}) { + this.disposables = new Disposables(); + } + + dispose() { + this.disposables.dispose(); + } + + get state() { + return this._state; + } + + _setState(state) { + if (state !== this._state) { + const previousState = this._state; + this._state = state; + this.emit(previousState); + } + } + + get sync() { + return this._sync; + } + + get session() { + return this._session; + } + + _createReconnector() { + const reconnector = new Reconnector( + new ExponentialRetryDelay(2000, this._clock.createTimeout), + this._clock.createMeasure + ); + // retry connection immediatly when online is detected + this.disposables.track(isOnline.subscribe(online => { + if(online) { + reconnector.tryNow(); + } + })); + return reconnector; + } + + async start(sessionInfo) { + try { + this._setState(LoadState.Loading); + this._reconnector = this._createReconnector(); + const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, this._reconnector); + const storage = await this._storageFactory.create(sessionInfo.id); + // no need to pass access token to session + const filteredSessionInfo = { + deviceId: sessionInfo.deviceId, + userId: sessionInfo.userId, + homeServer: sessionInfo.homeServer, + }; + this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); + await this._session.load(); + this._sync = new Sync({hsApi, storage, this._session}); + + // notify sync and session when back online + this.disposables.track(reconnector.subscribe(state => { + this._sync.start(); + session.notifyNetworkAvailable(reconnector.lastVersionsResponse); + })); + + const needsInitialSync = !this._session.syncToken; + if (!needsInitialSync) { + this._setState(LoadState.Ready); + } else { + this._setState(LoadState.InitialSync); + } + await this._sync.start(); + this._setState(LoadState.Ready); + this._session.notifyNetworkAvailable(); + } catch (err) { + this._error = err; + this._setState(LoadState.Error); + } + } +} diff --git a/src/observable/BaseObservableCollection.js b/src/observable/BaseObservableCollection.js index f9370f10..2a8a2336 100644 --- a/src/observable/BaseObservableCollection.js +++ b/src/observable/BaseObservableCollection.js @@ -31,6 +31,15 @@ export default class BaseObservableCollection { // Add iterator over handlers here } +// like an EventEmitter, but doesn't have an event type +export class ObservableValue extends BaseObservableCollection { + emit(argument) { + for (const h of this._handlers) { + h(argument); + } + } +} + export function tests() { class Collection extends BaseObservableCollection { constructor() { diff --git a/src/utils/DOMClock.js b/src/ui/web/dom/Clock.js similarity index 82% rename from src/utils/DOMClock.js rename to src/ui/web/dom/Clock.js index c943b109..a23d6c01 100644 --- a/src/utils/DOMClock.js +++ b/src/ui/web/dom/Clock.js @@ -1,6 +1,6 @@ -import {AbortError} from "./error.js"; +import {AbortError} from "../utils/error.js"; -class DOMTimeout { +class Timeout { constructor(ms) { this._reject = null; this._handle = null; @@ -27,7 +27,7 @@ class DOMTimeout { } } -class DOMTimeMeasure { +class TimeMeasure { constructor() { this._start = window.performance.now(); } @@ -37,13 +37,13 @@ class DOMTimeMeasure { } } -export class DOMClock { +export class Clock { createMeasure() { - return new DOMTimeMeasure(); + return new TimeMeasure(); } createTimeout(ms) { - return new DOMTimeout(ms); + return new Timeout(ms); } now() { diff --git a/src/ui/web/dom/Online.js b/src/ui/web/dom/Online.js new file mode 100644 index 00000000..0fa5ee7f --- /dev/null +++ b/src/ui/web/dom/Online.js @@ -0,0 +1,29 @@ +export class Online extends ObservableValue { + constructor() { + super(); + this._onOffline = this._onOffline.bind(this); + this._onOnline = this._onOnline.bind(this); + } + + _onOffline() { + this.emit(false); + } + + _onOnline() { + this.emit(true); + } + + get value() { + return navigator.onLine; + } + + onSubscribeFirst() { + window.addEventListener('offline', this._onOffline); + window.addEventListener('online', this._onOnline); + } + + onUnsubscribeLast() { + window.removeEventListener('offline', this._onOffline); + window.removeEventListener('online', this._onOnline); + } +} diff --git a/src/ui/web/general/SwitchView.js b/src/ui/web/general/SwitchView.js index 7789cb1e..6ab669e0 100644 --- a/src/ui/web/general/SwitchView.js +++ b/src/ui/web/general/SwitchView.js @@ -34,3 +34,38 @@ export default class SwitchView { return this._childView; } } + +// SessionLoadView +// should this be the new switch view? +// and the other one be the BasicSwitchView? +new BoundSwitchView(vm, vm => vm.isLoading, (loading, vm) => { + if (loading) { + return new InlineTemplateView(vm, t => { + return t.div({className: "loading"}, [ + t.span({className: "spinner"}), + t.span(vm => vm.loadingText) + ]); + }); + } else { + return new SessionView(vm.sessionViewModel); + } +}); + +class BoundSwitchView extends SwitchView { + constructor(value, mapper, viewCreator) { + super(viewCreator(mapper(value), value)); + this._mapper = mapper; + this._viewCreator = viewCreator; + this._mappedValue = mapper(value); + } + + update(value) { + const mappedValue = this._mapper(value); + if (mappedValue !== this._mappedValue) { + this._mappedValue = mappedValue; + this.switch(this._viewCreator(this._mappedValue, value)); + } else { + super.update(value); + } + } +} diff --git a/src/utils/Disposables.js b/src/utils/Disposables.js new file mode 100644 index 00000000..739fbe8b --- /dev/null +++ b/src/utils/Disposables.js @@ -0,0 +1,37 @@ +function disposeValue(value) { + if (typeof d === "function") { + value(); + } else { + value.dispose(); + } +} + +export class Disposables { + constructor() { + this._disposables = []; + } + + track(disposable) { + this._disposables.push(disposable); + } + + dispose() { + if (this._disposables) { + for (const d of this._disposables) { + disposeValue(d); + } + this._disposables = null; + } + } + + + disposeTracked(value) { + const idx = this._disposables.indexOf(value); + if (idx !== -1) { + const [foundValue] = this._disposables.splice(idx, 1); + disposeValue(foundValue); + return true; + } + return false; + } +} From 1f15ca64988d7d5915d0dc22116ab1360fb045ef Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sat, 18 Apr 2020 19:16:16 +0200 Subject: [PATCH 07/93] more WIP --- doc/- sync comes under session | 3 + doc/impl-thoughts/CATCHUP-BACKFILL.md | 7 + doc/impl-thoughts/RECONNECTING.md | 35 ++- doc/impl-thoughts/session-container.md | 18 ++ src/domain/ViewModel.js | 4 + src/main.js | 8 + src/matrix/Reconnector.js | 86 ++++-- src/matrix/SendScheduler.js | 4 + src/matrix/SessionContainer.js | 273 +++++++++++++----- src/matrix/error.js | 1 + src/matrix/hs-api.js | 1 - src/matrix/net/fetch.js | 2 +- src/matrix/session.js | 6 +- src/observable/BaseObservableCollection.js | 18 +- src/ui/web/dom/{Online.js => OnlineStatus.js} | 2 +- 15 files changed, 362 insertions(+), 106 deletions(-) create mode 100644 doc/- sync comes under session create mode 100644 doc/impl-thoughts/CATCHUP-BACKFILL.md create mode 100644 doc/impl-thoughts/session-container.md rename src/ui/web/dom/{Online.js => OnlineStatus.js} (92%) diff --git a/doc/- sync comes under session b/doc/- sync comes under session new file mode 100644 index 00000000..4db2baf1 --- /dev/null +++ b/doc/- sync comes under session @@ -0,0 +1,3 @@ + - sync comes under session + - sessioncontainer/client orchestrating reconnection + - \ No newline at end of file diff --git a/doc/impl-thoughts/CATCHUP-BACKFILL.md b/doc/impl-thoughts/CATCHUP-BACKFILL.md new file mode 100644 index 00000000..12c6ca3b --- /dev/null +++ b/doc/impl-thoughts/CATCHUP-BACKFILL.md @@ -0,0 +1,7 @@ +we should automatically fill gaps (capped at a certain (large) amount of events, 5000?) after a limited sync for a room + +## E2EE rooms + +during these fills (once supported), we should calculate push actions and trigger notifications, as we would otherwise have received this through sync. + +we could also trigger notifications when just backfilling on initial sync up to a certain amount of time in the past? diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index f648481f..18ea49d9 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -29,6 +29,39 @@ rooms should report how many messages they have queued up, and each time they sent one? `SendReporter` (passed from `Session` to `Room`, passed down to `SendQueue`), with: - - setPendingEventCount(roomId, count) + - setPendingEventCount(roomId, count). This should probably use the generic Room updating mechanism, e.g. a pendingMessageCount on Room that is updated. Then session listens for this in `_roomUpdateCallback`. `Session` listens to `Reconnector` to update it's status, but perhaps we wait to send messages until catchup sync is done + + +# TODO + + - finish (Base)ObservableValue + - put in own file + - add waitFor + - decide whether we want to inherit (no?) + - cleanup Reconnector with recent changes, move generic code, make imports work + - add SyncStatus as ObservableValue of enum in Sync + - show load progress in LoginView/SessionPickView and do away with loading screen + - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing + - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer + - rename SessionsStore to SessionInfoStorage + - make sure we've renamed all \*State enums and fields to \*Status + - add pendingMessageCount prop to SendQueue and Room, aggregate this in Session + - add completedFirstSync to Sync, so we can check if the catchup or initial sync is still in progress + - update SyncStatusViewModel to use reconnector.connectionStatus, sync.completedFirstSync, session.syncToken (is initial sync?) and session.pendingMessageCount to show these messages: + - disconnected, retrying in x seconds. [try now]. + - reconnecting... + - doing catchup sync + - syncing, sending x messages + - syncing + + perhaps we will want to put this as an ObservableValue on the SessionContainer ? + + NO: When connected, syncing and not sending anything, just hide the thing for now? although when you send messages it will just pop in and out all the time. + + - see if it makes sense for SendScheduler to use the same RetryDelay as Reconnector + - finally adjust all file names to their class names? e.g. camel case + - see if we want more dependency injection + - for classes from outside sdk + - for internal sdk classes? probably not yet diff --git a/doc/impl-thoughts/session-container.md b/doc/impl-thoughts/session-container.md new file mode 100644 index 00000000..d84d8ce9 --- /dev/null +++ b/doc/impl-thoughts/session-container.md @@ -0,0 +1,18 @@ +what should this new container be called? + - Client + - SessionContainer + + +it is what is returned from bootstrapping a ... thing +it allows you to replace classes within the client through IoC? +it wires up the different components +it unwires the components when you're done with the thing +it could hold all the dependencies for setting up a client, even before login + - online detection api + - clock + - homeserver + - requestFn + +we'll be explicitly making its parts public though, like session, sync, reconnector + +merge the connectionstate and diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index 2e26faae..cc4a6fff 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -1,3 +1,7 @@ +// ViewModel should just be an eventemitter, not an ObservableValue +// as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down) +// we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter + export class ViewModel extends ObservableValue { constructor(options) { super(); diff --git a/src/main.js b/src/main.js index b8e4ab33..64461f4c 100644 --- a/src/main.js +++ b/src/main.js @@ -20,6 +20,14 @@ export default async function main(container) { // window.getBrawlFetchLog = () => recorder.log(); // normal network: const request = fetchRequest; + const clock = new DOMClock(); + + const sessionContainer = new SessionContainer({ + clock, + request, + storageFactory: new StorageFactory(), + }); + const vm = new BrawlViewModel({ storageFactory: new StorageFactory(), createHsApi: (homeServer, accessToken, reconnector) => new HomeServerApi({homeServer, accessToken, request, reconnector}), diff --git a/src/matrix/Reconnector.js b/src/matrix/Reconnector.js index 0cb85851..92b47fb7 100644 --- a/src/matrix/Reconnector.js +++ b/src/matrix/Reconnector.js @@ -16,7 +16,7 @@ export class ExponentialRetryDelay { const powerOfTwo = (seconds * seconds) * 1000; this._current = Math.max(this._max, powerOfTwo); } catch(err) { - // swallow AbortError, means skipWaiting was called + // swallow AbortError, means abort was called if (!(err instanceof AbortError)) { throw err; } @@ -25,7 +25,7 @@ export class ExponentialRetryDelay { } } - skipWaiting() { + abort() { if (this._timeout) { this._timeout.abort(); } @@ -33,7 +33,7 @@ export class ExponentialRetryDelay { reset() { this._current = this._start; - this.skipWaiting(); + this.abort(); } get nextValue() { @@ -66,14 +66,13 @@ export const ConnectionState = createEnum( "Online" ); -export class Reconnector extends ObservableValue { - constructor({retryDelay, createTimeMeasure}) { - this._online +export class Reconnector { + constructor({retryDelay, createTimeMeasure, isOnline}) { + this._isOnline = isOnline; this._retryDelay = retryDelay; - this._currentDelay = null; this._createTimeMeasure = createTimeMeasure; // assume online, and do our thing when something fails - this._state = ConnectionState.Online; + this._state = new ObservableValue(ConnectionState.Online); this._isReconnecting = false; this._versionsResponse = null; } @@ -82,39 +81,53 @@ export class Reconnector extends ObservableValue { return this._versionsResponse; } - get state() { + get connectionState() { return this._state; } get retryIn() { - if (this._state === ConnectionState.Waiting) { + if (this._state.get() === ConnectionState.Waiting) { return this._retryDelay.nextValue - this._stateSince.measure(); } return 0; } - onRequestFailed(hsApi) { + async onRequestFailed(hsApi) { if (!this._isReconnecting) { this._setState(ConnectionState.Offline); - this._reconnectLoop(hsApi); + + const isOnlineSubscription = this._isOnline && this._isOnline.subscribe(online => { + if (online) { + this.tryNow(); + } + }); + + try { + await this._reconnectLoop(hsApi); + } finally { + if (isOnlineSubscription) { + // unsubscribe from this._isOnline + isOnlineSubscription(); + } + } } } tryNow() { if (this._retryDelay) { - this._retryDelay.skipWaiting(); + // this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop + this._retryDelay.abort(); } } _setState(state) { - if (state !== this._state) { - this._state = state; - if (this._state === ConnectionState.Waiting) { + if (state !== this._state.get()) { + if (state === ConnectionState.Waiting) { this._stateSince = this._createTimeMeasure(); } else { this._stateSince = null; } - this.emit(state); + this._state.set(state); } } @@ -123,19 +136,34 @@ export class Reconnector extends ObservableValue { this._versionsResponse = null; this._retryDelay.reset(); - while (!this._versionsResponse) { - try { - this._setState(ConnectionState.Reconnecting); - // use 10s timeout, because we don't want to be waiting for - // a stale connection when we just came online again - const versionsRequest = hsApi.versions({timeout: 10000}); - this._versionsResponse = await versionsRequest.response(); - this._setState(ConnectionState.Online); - } catch (err) { - // NetworkError or AbortError from timeout - this._setState(ConnectionState.Waiting); - await this._retryDelay.waitForRetry(); + try { + while (!this._versionsResponse) { + try { + this._setState(ConnectionState.Reconnecting); + // use 10s timeout, because we don't want to be waiting for + // a stale connection when we just came online again + const versionsRequest = hsApi.versions({timeout: 10000}); + this._versionsResponse = await versionsRequest.response(); + this._setState(ConnectionState.Online); + } catch (err) { + if (err instanceof NetworkError) { + this._setState(ConnectionState.Waiting); + try { + await this._retryDelay.waitForRetry(); + } catch (err) { + if (!(err instanceof AbortError)) { + throw err; + } + } + } else { + throw err; + } + } } + } catch (err) { + // nothing is catching the error above us, + // so just log here + console.err(err); } } } diff --git a/src/matrix/SendScheduler.js b/src/matrix/SendScheduler.js index 5340d093..3beb4fa1 100644 --- a/src/matrix/SendScheduler.js +++ b/src/matrix/SendScheduler.js @@ -62,6 +62,10 @@ export class SendScheduler { // this._enabled; } + stop() { + // TODO: abort current requests and set offline + } + // this should really be per roomId to avoid head-of-line blocking // // takes a callback instead of returning a promise with the slot diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 86be76d9..5cf616c3 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,95 +1,226 @@ -const factory = { - Clock: () => new DOMClock(), - Request: () => fetchRequest, - Online: () => new DOMOnline(), - HomeServerApi: () -} +import HomeServerApi from "./hs-api.js"; -export const LoadState = createEnum( +export const LoadStatus = createEnum( + "NotLoading", + "Login", + "LoginFailed", "Loading", - "InitialSync", "Migrating", //not used atm, but would fit here + "InitialSync", + "CatchupSync", "Error", "Ready", ); -class SessionContainer extends ObservableValue { - constructor({clock, random, isOnline, request, storageFactory, factory}) { - this.disposables = new Disposables(); +export const LoginFailure = createEnum( + "Network", + "Credentials", + "Unknown", +); + +export class SessionContainer { + constructor({clock, random, onlineStatus, request, storageFactory, sessionsStore}) { + this._random = random; + this._clock = clock; + this._onlineStatus = onlineStatus; + this._request = request; + this._storageFactory = storageFactory; + this._sessionsStore = sessionsStore; + + this._status = new ObservableValue(LoadStatus.NotLoading); + this._error = null; + this._loginFailure = null; + this._reconnector = null; + this._session = null; + this._sync = null; } - dispose() { - this.disposables.dispose(); + _createNewSessionId() { + return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString(); } - get state() { - return this._state; - } - - _setState(state) { - if (state !== this._state) { - const previousState = this._state; - this._state = state; - this.emit(previousState); + async startWithExistingSession(sessionId) { + if (this._status.get() !== LoadStatus.NotLoading) { + return; + } + this._status.set(LoadStatus.Loading); + try { + const sessionInfo = await this._sessionsStore.get(sessionId); + await this._loadSessionInfo(sessionInfo); + } catch (err) { + this._error = err; + this._status.set(LoadStatus.Error); } } + async startWithLogin(homeServer, username, password) { + if (this._status.get() !== LoadStatus.NotLoading) { + return; + } + this._status.set(LoadStatus.Login); + let sessionInfo; + try { + const hsApi = new HomeServerApi({homeServer, request: this._request}); + const loginData = await hsApi.passwordLogin(username, password).response(); + const sessionId = this._createNewSessionId(); + sessionInfo = { + id: sessionId, + deviceId: loginData.device_id, + userId: loginData.user_id, + homeServer: homeServer, + accessToken: loginData.access_token, + lastUsed: this._clock.now() + }; + await this._sessionsStore.add(sessionInfo); + } catch (err) { + this._error = err; + if (err instanceof HomeServerError) { + if (err.statusCode === 403) { + this._loginFailure = LoginFailure.Credentials; + } else { + this._loginFailure = LoginFailure.Unknown; + } + this._status.set(LoadStatus.LoginFailure); + } else if (err instanceof NetworkError) { + this._loginFailure = LoginFailure.Network; + this._status.set(LoadStatus.LoginFailure); + } else { + this._status.set(LoadStatus.Error); + } + return; + } + // loading the session can only lead to + // LoadStatus.Error in case of an error, + // so separate try/catch + try { + await this._loadSessionInfo(sessionInfo); + } catch (err) { + this._error = err; + this._status.set(LoadStatus.Error); + } + } + + async _loadSessionInfo(sessionInfo) { + this._status.set(LoadStatus.Loading); + this._reconnector = new Reconnector({ + onlineStatus: this._onlineStatus, + delay: new ExponentialRetryDelay(2000, this._clock.createTimeout), + createMeasure: this._clock.createMeasure + }); + const hsApi = new HomeServerApi({ + homeServer: sessionInfo.homeServer, + accessToken: sessionInfo.accessToken, + request: this._request, + reconnector: this._reconnector, + }); + const storage = await this._storageFactory.create(sessionInfo.id); + // no need to pass access token to session + const filteredSessionInfo = { + deviceId: sessionInfo.deviceId, + userId: sessionInfo.userId, + homeServer: sessionInfo.homeServer, + }; + this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); + await this._session.load(); + + const needsInitialSync = !this._session.syncToken; + if (!needsInitialSync) { + this._status.set(LoadStatus.CatchupSync); + } else { + this._status.set(LoadStatus.InitialSync); + } + + this._sync = new Sync({hsApi, storage, session: this._session}); + + // notify sync and session when back online + this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { + if (state === ConnectionStatus.Online) { + this._sync.start(); + this._session.start(this._reconnector.lastVersionsResponse); + } + }); + + try { + await this._sync.start(); + } catch (err) { + // swallow NetworkError here and continue, + // as the reconnector above will call + // sync.start again to retry in this case + if (!(err instanceof NetworkError)) { + throw err; + } + } + // only transition into Ready once the first sync has succeeded + await this._sync.status.waitFor(s => s === SyncStatus.Syncing); + this._status.set(LoadStatus.Ready); + + // if this fails, the reconnector will start polling versions to reconnect + const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); + this._session.start(lastVersionsResponse); + } + + + get loadStatus() { + return this._status; + } + + get loadError() { + return this._error; + } + + /** only set at loadStatus InitialSync, CatchupSync or Ready */ get sync() { return this._sync; } + /** only set at loadStatus InitialSync, CatchupSync or Ready */ get session() { return this._session; } - _createReconnector() { - const reconnector = new Reconnector( - new ExponentialRetryDelay(2000, this._clock.createTimeout), - this._clock.createMeasure - ); - // retry connection immediatly when online is detected - this.disposables.track(isOnline.subscribe(online => { - if(online) { - reconnector.tryNow(); - } - })); - return reconnector; - } - - async start(sessionInfo) { - try { - this._setState(LoadState.Loading); - this._reconnector = this._createReconnector(); - const hsApi = this._createHsApi(sessionInfo.homeServer, sessionInfo.accessToken, this._reconnector); - const storage = await this._storageFactory.create(sessionInfo.id); - // no need to pass access token to session - const filteredSessionInfo = { - deviceId: sessionInfo.deviceId, - userId: sessionInfo.userId, - homeServer: sessionInfo.homeServer, - }; - this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); - await this._session.load(); - this._sync = new Sync({hsApi, storage, this._session}); - - // notify sync and session when back online - this.disposables.track(reconnector.subscribe(state => { - this._sync.start(); - session.notifyNetworkAvailable(reconnector.lastVersionsResponse); - })); - - const needsInitialSync = !this._session.syncToken; - if (!needsInitialSync) { - this._setState(LoadState.Ready); - } else { - this._setState(LoadState.InitialSync); - } - await this._sync.start(); - this._setState(LoadState.Ready); - this._session.notifyNetworkAvailable(); - } catch (err) { - this._error = err; - this._setState(LoadState.Error); - } + stop() { + this._reconnectSubscription(); + this._reconnectSubscription = null; + this._sync.stop(); + this._session.stop(); } } + +/* +function main() { + // these are only required for external classes, + // SessionFactory has it's defaults for internal classes + const sessionFactory = new SessionFactory({ + Clock: DOMClock, + OnlineState: DOMOnlineState, + SessionsStore: LocalStorageSessionStore, // should be called SessionInfoStore? + StorageFactory: window.indexedDB ? IDBStorageFactory : MemoryStorageFactory, // should be called StorageManager? + // should be moved to StorageFactory as `KeyBounds`?: minStorageKey, middleStorageKey, maxStorageKey + // would need to pass it into EventKey though + request, + }); + + // lets not do this in a first cut + // internally in the matrix lib + const room = new creator.ctor("Room", Room)({}); + + // or short + const sessionFactory = new SessionFactory(WebFactory); + // sessionFactory.sessionInfoStore + + // registration + // const registration = sessionFactory.registerUser(); + // registration.stage + + + const container = sessionFactory.startWithRegistration(registration); + const container = sessionFactory.startWithLogin(server, username, password); + const container = sessionFactory.startWithExistingSession(sessionId); + // container.loadStatus is an ObservableValue + await container.loadStatus.waitFor(s => s === LoadStatus.Loaded || s === LoadStatus.CatchupSync); + + // loader isn't needed anymore from now on + const {session, sync, reconnector} = container; + container.stop(); +} +*/ diff --git a/src/matrix/error.js b/src/matrix/error.js index 42dcac73..2ba95037 100644 --- a/src/matrix/error.js +++ b/src/matrix/error.js @@ -3,6 +3,7 @@ export class HomeServerError extends Error { super(`${body ? body.error : status} on ${method} ${url}`); this.errcode = body ? body.errcode : null; this.retry_after_ms = body ? body.retry_after_ms : 0; + this.statusCode = status; } get isFatal() { diff --git a/src/matrix/hs-api.js b/src/matrix/hs-api.js index 314a9324..755d4825 100644 --- a/src/matrix/hs-api.js +++ b/src/matrix/hs-api.js @@ -81,7 +81,6 @@ export default class HomeServerApi { requestResult.response().then(() => timeout.abort()); } - const wrapper = new RequestWrapper(method, url, requestResult); if (this._reconnector) { diff --git a/src/matrix/net/fetch.js b/src/matrix/net/fetch.js index 36713475..76929c6b 100644 --- a/src/matrix/net/fetch.js +++ b/src/matrix/net/fetch.js @@ -57,7 +57,7 @@ export default function fetchRequest(url, options) { // this can either mean user is offline, server is offline, or a CORS error (server misconfiguration). // // One could check navigator.onLine to rule out the first - // but the 2 later ones are indistinguishable from javascript. + // but the 2 latter ones are indistinguishable from javascript. throw new NetworkError(`${options.method} ${url}: ${err.message}`); } throw err; diff --git a/src/matrix/session.js b/src/matrix/session.js index 837cb51a..d513d0b2 100644 --- a/src/matrix/session.js +++ b/src/matrix/session.js @@ -40,7 +40,11 @@ export default class Session { })); } - notifyNetworkAvailable(lastVersionResponse) { + stop() { + this._sendScheduler.stop(); + } + + start(lastVersionResponse) { for (const [, room] of this._rooms) { room.resumeSending(); } diff --git a/src/observable/BaseObservableCollection.js b/src/observable/BaseObservableCollection.js index 2a8a2336..a12269cf 100644 --- a/src/observable/BaseObservableCollection.js +++ b/src/observable/BaseObservableCollection.js @@ -32,7 +32,7 @@ export default class BaseObservableCollection { } // like an EventEmitter, but doesn't have an event type -export class ObservableValue extends BaseObservableCollection { +export class BaseObservableValue extends BaseObservableCollection { emit(argument) { for (const h of this._handlers) { h(argument); @@ -40,6 +40,22 @@ export class ObservableValue extends BaseObservableCollection { } } +export class ObservableValue extends BaseObservableValue { + constructor(initialValue) { + super(); + this._value = initialValue; + } + + get() { + return this._value; + } + + set(value) { + this._value = value; + this.emit(this._value); + } +} + export function tests() { class Collection extends BaseObservableCollection { constructor() { diff --git a/src/ui/web/dom/Online.js b/src/ui/web/dom/OnlineStatus.js similarity index 92% rename from src/ui/web/dom/Online.js rename to src/ui/web/dom/OnlineStatus.js index 0fa5ee7f..56276de0 100644 --- a/src/ui/web/dom/Online.js +++ b/src/ui/web/dom/OnlineStatus.js @@ -1,4 +1,4 @@ -export class Online extends ObservableValue { +export class OnlineStatus extends ObservableValue { constructor() { super(); this._onOffline = this._onOffline.bind(this); From 8c5411cb7db38b291fa2b7f9ae862669943e07a7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 19 Apr 2020 19:02:10 +0200 Subject: [PATCH 08/93] moar WIP --- doc/impl-thoughts/RECONNECTING.md | 4 +- package.json | 2 +- src/main.js | 12 +- src/matrix/Reconnector.js | 169 ----------------- src/matrix/SessionContainer.js | 35 +++- src/matrix/net/ExponentialRetryDelay.js | 107 +++++++++++ .../{hs-api.js => net/HomeServerApi.js} | 0 src/matrix/net/Reconnector.js | 176 ++++++++++++++++++ src/matrix/net/{ => request}/fetch.js | 0 src/matrix/net/{ => request}/replay.js | 0 src/mocks/Clock.js | 77 ++++++++ ...ervableCollection.js => BaseObservable.js} | 29 +-- src/observable/ObservableValue.js | 120 ++++++++++++ src/observable/list/BaseObservableList.js | 4 +- src/observable/map/BaseObservableMap.js | 4 +- src/ui/web/dom/Clock.js | 4 +- src/ui/web/dom/OnlineStatus.js | 4 +- src/ui/web/general/SwitchView.js | 6 +- src/utils/enum.js | 7 + yarn.lock | 8 +- 20 files changed, 538 insertions(+), 230 deletions(-) delete mode 100644 src/matrix/Reconnector.js create mode 100644 src/matrix/net/ExponentialRetryDelay.js rename src/matrix/{hs-api.js => net/HomeServerApi.js} (100%) create mode 100644 src/matrix/net/Reconnector.js rename src/matrix/net/{ => request}/fetch.js (100%) rename src/matrix/net/{ => request}/replay.js (100%) create mode 100644 src/mocks/Clock.js rename src/observable/{BaseObservableCollection.js => BaseObservable.js} (66%) create mode 100644 src/observable/ObservableValue.js create mode 100644 src/utils/enum.js diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 18ea49d9..7668096f 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -36,9 +36,9 @@ rooms should report how many messages they have queued up, and each time they se # TODO - - finish (Base)ObservableValue + - DONE: finish (Base)ObservableValue - put in own file - - add waitFor + - add waitFor (won't this leak if the promise never resolves?) - decide whether we want to inherit (no?) - cleanup Reconnector with recent changes, move generic code, make imports work - add SyncStatus as ObservableValue of enum in Sync diff --git a/package.json b/package.json index ebfc4819..53701dae 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "cheerio": "^1.0.0-rc.3", "finalhandler": "^1.1.1", - "impunity": "^0.0.10", + "impunity": "^0.0.11", "postcss": "^7.0.18", "postcss-import": "^12.0.1", "rollup": "^1.15.6", diff --git a/src/main.js b/src/main.js index 64461f4c..b2aa8831 100644 --- a/src/main.js +++ b/src/main.js @@ -1,11 +1,13 @@ -import HomeServerApi from "./matrix/hs-api.js"; +import HomeServerApi from "./matrix/net/HomeServerApi.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/replay.js"; import fetchRequest from "./matrix/net/fetch.js"; +import {Reconnector} from "./matrix/net/connection/Reconnector.js"; import StorageFactory from "./matrix/storage/idb/create.js"; import SessionsStore from "./matrix/sessions-store/localstorage/SessionsStore.js"; import BrawlViewModel from "./domain/BrawlViewModel.js"; import BrawlView from "./ui/web/BrawlView.js"; -import DOMClock from "./utils/DOMClock.js"; +import DOMClock from "./ui/web/dom/Clock.js"; +import OnlineStatus from "./ui/web/dom/OnlineStatus.js"; export default async function main(container) { try { @@ -22,12 +24,6 @@ export default async function main(container) { const request = fetchRequest; const clock = new DOMClock(); - const sessionContainer = new SessionContainer({ - clock, - request, - storageFactory: new StorageFactory(), - }); - const vm = new BrawlViewModel({ storageFactory: new StorageFactory(), createHsApi: (homeServer, accessToken, reconnector) => new HomeServerApi({homeServer, accessToken, request, reconnector}), diff --git a/src/matrix/Reconnector.js b/src/matrix/Reconnector.js deleted file mode 100644 index 92b47fb7..00000000 --- a/src/matrix/Reconnector.js +++ /dev/null @@ -1,169 +0,0 @@ -export class ExponentialRetryDelay { - constructor(start = 2000, createTimeout) { - this._start = start; - this._current = start; - this._createTimeout = createTimeout; - this._max = 60 * 5 * 1000; //5 min - this._timeout = null; - } - - async waitForRetry() { - this._timeout = this._createTimeout(this._current); - try { - await this._timeout.elapsed(); - // only increase delay if we didn't get interrupted - const seconds = this._current / 1000; - const powerOfTwo = (seconds * seconds) * 1000; - this._current = Math.max(this._max, powerOfTwo); - } catch(err) { - // swallow AbortError, means abort was called - if (!(err instanceof AbortError)) { - throw err; - } - } finally { - this._timeout = null; - } - } - - abort() { - if (this._timeout) { - this._timeout.abort(); - } - } - - reset() { - this._current = this._start; - this.abort(); - } - - get nextValue() { - return this._current; - } -} - -// we need a clock interface that gives us both timestamps and a timer that we can interrupt? - -// state -// - offline -// - waiting to reconnect -// - reconnecting -// - online -// -// - -function createEnum(...values) { - const obj = {}; - for (const value of values) { - obj[value] = value; - } - return Object.freeze(obj); -} - -export const ConnectionState = createEnum( - "Offline", - "Waiting", - "Reconnecting", - "Online" -); - -export class Reconnector { - constructor({retryDelay, createTimeMeasure, isOnline}) { - this._isOnline = isOnline; - this._retryDelay = retryDelay; - this._createTimeMeasure = createTimeMeasure; - // assume online, and do our thing when something fails - this._state = new ObservableValue(ConnectionState.Online); - this._isReconnecting = false; - this._versionsResponse = null; - } - - get lastVersionsResponse() { - return this._versionsResponse; - } - - get connectionState() { - return this._state; - } - - get retryIn() { - if (this._state.get() === ConnectionState.Waiting) { - return this._retryDelay.nextValue - this._stateSince.measure(); - } - return 0; - } - - async onRequestFailed(hsApi) { - if (!this._isReconnecting) { - this._setState(ConnectionState.Offline); - - const isOnlineSubscription = this._isOnline && this._isOnline.subscribe(online => { - if (online) { - this.tryNow(); - } - }); - - try { - await this._reconnectLoop(hsApi); - } finally { - if (isOnlineSubscription) { - // unsubscribe from this._isOnline - isOnlineSubscription(); - } - } - } - } - - tryNow() { - if (this._retryDelay) { - // this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop - this._retryDelay.abort(); - } - } - - _setState(state) { - if (state !== this._state.get()) { - if (state === ConnectionState.Waiting) { - this._stateSince = this._createTimeMeasure(); - } else { - this._stateSince = null; - } - this._state.set(state); - } - } - - async _reconnectLoop(hsApi) { - this._isReconnecting = true; - this._versionsResponse = null; - this._retryDelay.reset(); - - try { - while (!this._versionsResponse) { - try { - this._setState(ConnectionState.Reconnecting); - // use 10s timeout, because we don't want to be waiting for - // a stale connection when we just came online again - const versionsRequest = hsApi.versions({timeout: 10000}); - this._versionsResponse = await versionsRequest.response(); - this._setState(ConnectionState.Online); - } catch (err) { - if (err instanceof NetworkError) { - this._setState(ConnectionState.Waiting); - try { - await this._retryDelay.waitForRetry(); - } catch (err) { - if (!(err instanceof AbortError)) { - throw err; - } - } - } else { - throw err; - } - } - } - } catch (err) { - // nothing is catching the error above us, - // so just log here - console.err(err); - } - } -} diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 5cf616c3..78c63560 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,4 +1,4 @@ -import HomeServerApi from "./hs-api.js"; +import HomeServerApi from "./net/HomeServerApi.js"; export const LoadStatus = createEnum( "NotLoading", @@ -131,7 +131,6 @@ export class SessionContainer { } this._sync = new Sync({hsApi, storage, session: this._session}); - // notify sync and session when back online this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { if (state === ConnectionStatus.Online) { @@ -139,7 +138,15 @@ export class SessionContainer { this._session.start(this._reconnector.lastVersionsResponse); } }); - + await this._waitForFirstSync(); + this._status.set(LoadStatus.Ready); + + // if this fails, the reconnector will start polling versions to reconnect + const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); + this._session.start(lastVersionsResponse); + } + + async _waitForFirstSync() { try { await this._sync.start(); } catch (err) { @@ -151,12 +158,18 @@ export class SessionContainer { } } // only transition into Ready once the first sync has succeeded - await this._sync.status.waitFor(s => s === SyncStatus.Syncing); - this._status.set(LoadStatus.Ready); - - // if this fails, the reconnector will start polling versions to reconnect - const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); - this._session.start(lastVersionsResponse); + this._waitForFirstSyncHandle = this._sync.status.waitFor(s => s === SyncStatus.Syncing); + try { + await this._waitForFirstSyncHandle.promise; + } catch (err) { + // if dispose is called from stop, bail out + if (err instanceof AbortError) { + return; + } + throw err; + } finally { + this._waitForFirstSyncHandle = null; + } } @@ -183,6 +196,10 @@ export class SessionContainer { this._reconnectSubscription = null; this._sync.stop(); this._session.stop(); + if (this._waitForFirstSyncHandle) { + this._waitForFirstSyncHandle.dispose(); + this._waitForFirstSyncHandle = null; + } } } diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.js new file mode 100644 index 00000000..056f0a1b --- /dev/null +++ b/src/matrix/net/ExponentialRetryDelay.js @@ -0,0 +1,107 @@ +import {AbortError} from "../../utils/error.js"; + +export default class ExponentialRetryDelay { + constructor(createTimeout, start = 2000) { + this._start = start; + this._current = start; + this._createTimeout = createTimeout; + this._max = 60 * 5 * 1000; //5 min + this._timeout = null; + } + + async waitForRetry() { + this._timeout = this._createTimeout(this._current); + try { + await this._timeout.elapsed(); + // only increase delay if we didn't get interrupted + const next = 2 * this._current; + this._current = Math.min(this._max, next); + } catch(err) { + // swallow AbortError, means abort was called + if (!(err instanceof AbortError)) { + throw err; + } + } finally { + this._timeout = null; + } + } + + abort() { + if (this._timeout) { + this._timeout.abort(); + } + } + + reset() { + this._current = this._start; + this.abort(); + } + + get nextValue() { + return this._current; + } +} + + +import MockClock from "../../../mocks/Clock.js"; + +export function tests() { + return { + "test sequence": async assert => { + const clock = new MockClock(); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + let promise; + + assert.strictEqual(retryDelay.nextValue, 2000); + promise = retryDelay.waitForRetry(); + clock.elapse(2000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 4000); + promise = retryDelay.waitForRetry(); + clock.elapse(4000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 8000); + promise = retryDelay.waitForRetry(); + clock.elapse(8000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 16000); + promise = retryDelay.waitForRetry(); + clock.elapse(16000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 32000); + promise = retryDelay.waitForRetry(); + clock.elapse(32000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 64000); + promise = retryDelay.waitForRetry(); + clock.elapse(64000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 128000); + promise = retryDelay.waitForRetry(); + clock.elapse(128000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 256000); + promise = retryDelay.waitForRetry(); + clock.elapse(256000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 300000); + promise = retryDelay.waitForRetry(); + clock.elapse(300000); + await promise; + + assert.strictEqual(retryDelay.nextValue, 300000); + promise = retryDelay.waitForRetry(); + clock.elapse(300000); + await promise; + }, + } + +} diff --git a/src/matrix/hs-api.js b/src/matrix/net/HomeServerApi.js similarity index 100% rename from src/matrix/hs-api.js rename to src/matrix/net/HomeServerApi.js diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js new file mode 100644 index 00000000..ea2c86bf --- /dev/null +++ b/src/matrix/net/Reconnector.js @@ -0,0 +1,176 @@ +import createEnum from "../../utils/enum.js"; +import {AbortError} from "../../utils/error.js"; +import {NetworkError} from "../error.js" +import ObservableValue from "../../observable/ObservableValue.js"; + +export const ConnectionStatus = createEnum( + "Offline", + "Waiting", + "Reconnecting", + "Online" +); + +export class Reconnector { + constructor({retryDelay, createMeasure, onlineStatus}) { + this._onlineStatus = onlineStatus; + this._retryDelay = retryDelay; + this._createTimeMeasure = createMeasure; + // assume online, and do our thing when something fails + this._state = new ObservableValue(ConnectionStatus.Online); + this._isReconnecting = false; + this._versionsResponse = null; + } + + get lastVersionsResponse() { + return this._versionsResponse; + } + + get connectionStatus() { + return this._state; + } + + get retryIn() { + if (this._state.get() === ConnectionStatus.Waiting) { + return this._retryDelay.nextValue - this._stateSince.measure(); + } + return 0; + } + + async onRequestFailed(hsApi) { + if (!this._isReconnecting) { + this._setState(ConnectionStatus.Offline); + + const onlineStatusSubscription = this._onlineStatus && this._onlineStatus.subscribe(online => { + if (online) { + this.tryNow(); + } + }); + + try { + await this._reconnectLoop(hsApi); + } catch (err) { + // nothing is catching the error above us, + // so just log here + console.error(err); + } finally { + if (onlineStatusSubscription) { + // unsubscribe from this._onlineStatus + onlineStatusSubscription(); + } + } + } + } + + tryNow() { + if (this._retryDelay) { + // this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop + this._retryDelay.abort(); + } + } + + _setState(state) { + if (state !== this._state.get()) { + if (state === ConnectionStatus.Waiting) { + this._stateSince = this._createTimeMeasure(); + } else { + this._stateSince = null; + } + this._state.set(state); + } + } + + async _reconnectLoop(hsApi) { + this._isReconnecting = true; + this._versionsResponse = null; + this._retryDelay.reset(); + + while (!this._versionsResponse) { + try { + this._setState(ConnectionStatus.Reconnecting); + // use 10s timeout, because we don't want to be waiting for + // a stale connection when we just came online again + const versionsRequest = hsApi.versions({timeout: 10000}); + this._versionsResponse = await versionsRequest.response(); + this._setState(ConnectionStatus.Online); + } catch (err) { + if (err instanceof NetworkError) { + this._setState(ConnectionStatus.Waiting); + try { + await this._retryDelay.waitForRetry(); + } catch (err) { + if (!(err instanceof AbortError)) { + throw err; + } + } + } else { + throw err; + } + } + } + } +} + + +import MockClock from "../../../mocks/Clock.js"; +import ExponentialRetryDelay from "./ExponentialRetryDelay.js"; + +export function tests() { + function createHsApiMock(remainingFailures) { + return { + versions() { + return { + response() { + if (remainingFailures) { + remainingFailures -= 1; + return Promise.reject(new NetworkError()); + } else { + return Promise.resolve(42); + } + } + }; + } + } + } + + return { + "test reconnecting with 1 failure": async assert => { + const clock = new MockClock(); + const {createMeasure} = clock; + const onlineStatus = new ObservableValue(false); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); + const {connectionStatus} = reconnector; + const statuses = []; + const subscription = reconnector.connectionStatus.subscribe(s => { + statuses.push(s); + }); + reconnector.onRequestFailed(createHsApiMock(1)); + await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; + clock.elapse(2000); + await connectionStatus.waitFor(s => s === ConnectionStatus.Online).promise; + assert.deepEqual(statuses, [ + ConnectionStatus.Offline, + ConnectionStatus.Reconnecting, + ConnectionStatus.Waiting, + ConnectionStatus.Reconnecting, + ConnectionStatus.Online + ]); + assert.strictEqual(reconnector.lastVersionsResponse, 42); + subscription(); + }, + "test reconnecting with onlineStatus": async assert => { + const clock = new MockClock(); + const {createMeasure} = clock; + const onlineStatus = new ObservableValue(false); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); + const {connectionStatus} = reconnector; + reconnector.onRequestFailed(createHsApiMock(1)); + await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; + onlineStatus.set(true); //skip waiting + await connectionStatus.waitFor(s => s === ConnectionStatus.Online).promise; + assert.equal(connectionStatus.get(), ConnectionStatus.Online); + assert.strictEqual(reconnector.lastVersionsResponse, 42); + }, + } +} diff --git a/src/matrix/net/fetch.js b/src/matrix/net/request/fetch.js similarity index 100% rename from src/matrix/net/fetch.js rename to src/matrix/net/request/fetch.js diff --git a/src/matrix/net/replay.js b/src/matrix/net/request/replay.js similarity index 100% rename from src/matrix/net/replay.js rename to src/matrix/net/request/replay.js diff --git a/src/mocks/Clock.js b/src/mocks/Clock.js new file mode 100644 index 00000000..0a08bf58 --- /dev/null +++ b/src/mocks/Clock.js @@ -0,0 +1,77 @@ +import ObservableValue from "../observable/ObservableValue.js"; + +class Timeout { + constructor(elapsed, ms) { + this._reject = null; + this._handle = null; + const timeoutValue = elapsed.get() + ms; + this._waitHandle = elapsed.waitFor(t => t >= timeoutValue); + } + + elapsed() { + return this._waitHandle.promise; + } + + abort() { + // will reject with AbortError + this._waitHandle.dispose(); + } +} + +class TimeMeasure { + constructor(elapsed) { + this._elapsed = elapsed; + this._start = elapsed.get(); + } + + measure() { + return this._elapsed.get() - this._start; + } +} + +export default class Clock { + constructor(baseTimestamp = 0) { + this._baseTimestamp = baseTimestamp; + this._elapsed = new ObservableValue(0); + // should be callable as a function as well as a method + this.createMeasure = this.createMeasure.bind(this); + this.createTimeout = this.createTimeout.bind(this); + this.now = this.now.bind(this); + } + + createMeasure() { + return new TimeMeasure(this._elapsed); + } + + createTimeout(ms) { + return new Timeout(this._elapsed, ms); + } + + now() { + return this._baseTimestamp + this.elapsed; + } + + elapse(ms) { + this._elapsed.set(this._elapsed.get() + Math.max(0, ms)); + } + + get elapsed() { + return this._elapsed.get(); + } +} + +export function tests() { + return { + "test timeout": async assert => { + const clock = new Clock(); + Promise.resolve().then(() => { + clock.elapse(500); + clock.elapse(500); + }).catch(assert.fail); + const timeout = clock.createTimeout(1000); + const promise = timeout.elapsed(); + assert(promise instanceof Promise); + await promise; + } + } +} diff --git a/src/observable/BaseObservableCollection.js b/src/observable/BaseObservable.js similarity index 66% rename from src/observable/BaseObservableCollection.js rename to src/observable/BaseObservable.js index a12269cf..6ef46ab6 100644 --- a/src/observable/BaseObservableCollection.js +++ b/src/observable/BaseObservable.js @@ -1,4 +1,4 @@ -export default class BaseObservableCollection { +export default class BaseObservable { constructor() { this._handlers = new Set(); } @@ -31,33 +31,8 @@ export default class BaseObservableCollection { // Add iterator over handlers here } -// like an EventEmitter, but doesn't have an event type -export class BaseObservableValue extends BaseObservableCollection { - emit(argument) { - for (const h of this._handlers) { - h(argument); - } - } -} - -export class ObservableValue extends BaseObservableValue { - constructor(initialValue) { - super(); - this._value = initialValue; - } - - get() { - return this._value; - } - - set(value) { - this._value = value; - this.emit(this._value); - } -} - export function tests() { - class Collection extends BaseObservableCollection { + class Collection extends BaseObservable { constructor() { super(); this.firstSubscribeCalls = 0; diff --git a/src/observable/ObservableValue.js b/src/observable/ObservableValue.js new file mode 100644 index 00000000..94f2d188 --- /dev/null +++ b/src/observable/ObservableValue.js @@ -0,0 +1,120 @@ +import {AbortError} from "../utils/error.js"; +import BaseObservable from "./BaseObservable.js"; + +// like an EventEmitter, but doesn't have an event type +export class BaseObservableValue extends BaseObservable { + emit(argument) { + for (const h of this._handlers) { + h(argument); + } + } + +} + +class WaitForHandle { + constructor(observable, predicate) { + this._promise = new Promise((resolve, reject) => { + this._reject = reject; + this._subscription = observable.subscribe(v => { + if (predicate(v)) { + this._reject = null; + resolve(v); + this.dispose(); + } + }); + }); + } + + get promise() { + return this._promise; + } + + dispose() { + if (this._subscription) { + this._subscription(); + this._subscription = null; + } + if (this._reject) { + this._reject(new AbortError()); + this._reject = null; + } + } +} + +class ResolvedWaitForHandle { + constructor(promise) { + this.promise = promise; + } + + dispose() {} +} + +export default class ObservableValue extends BaseObservableValue { + constructor(initialValue) { + super(); + this._value = initialValue; + } + + get() { + return this._value; + } + + set(value) { + if (value !== this._value) { + this._value = value; + this.emit(this._value); + } + } + + waitFor(predicate) { + if (predicate(this.get())) { + return new ResolvedWaitForHandle(Promise.resolve(this.get())); + } else { + return new WaitForHandle(this, predicate); + } + } +} + +export function tests() { + return { + "set emits an update": assert => { + const a = new ObservableValue(); + let fired = false; + const subscription = a.subscribe(v => { + fired = true; + assert.strictEqual(v, 5); + }); + a.set(5); + assert(fired); + subscription(); + }, + "set doesn't emit if value hasn't changed": assert => { + const a = new ObservableValue(5); + let fired = false; + const subscription = a.subscribe(() => { + fired = true; + }); + a.set(5); + a.set(5); + assert(!fired); + subscription(); + }, + "waitFor promise resolves on matching update": async assert => { + const a = new ObservableValue(5); + const handle = a.waitFor(v => v === 6); + Promise.resolve().then(() => { + a.set(6); + }); + await handle.promise; + assert.strictEqual(a.get(), 6); + }, + "waitFor promise rejects when disposed": async assert => { + const a = new ObservableValue(); + const handle = a.waitFor(() => false); + Promise.resolve().then(() => { + handle.dispose(); + }); + await assert.rejects(handle.promise, AbortError); + }, + } +} diff --git a/src/observable/list/BaseObservableList.js b/src/observable/list/BaseObservableList.js index cdab32f3..4c7e3491 100644 --- a/src/observable/list/BaseObservableList.js +++ b/src/observable/list/BaseObservableList.js @@ -1,6 +1,6 @@ -import BaseObservableCollection from "../BaseObservableCollection.js"; +import BaseObservable from "../BaseObservable.js"; -export default class BaseObservableList extends BaseObservableCollection { +export default class BaseObservableList extends BaseObservable { emitReset() { for(let h of this._handlers) { h.onReset(this); diff --git a/src/observable/map/BaseObservableMap.js b/src/observable/map/BaseObservableMap.js index fb8a6a3f..c2f1b974 100644 --- a/src/observable/map/BaseObservableMap.js +++ b/src/observable/map/BaseObservableMap.js @@ -1,6 +1,6 @@ -import BaseObservableCollection from "../BaseObservableCollection.js"; +import BaseObservable from "../BaseObservable.js"; -export default class BaseObservableMap extends BaseObservableCollection { +export default class BaseObservableMap extends BaseObservable { emitReset() { for(let h of this._handlers) { h.onReset(); diff --git a/src/ui/web/dom/Clock.js b/src/ui/web/dom/Clock.js index a23d6c01..cc36c813 100644 --- a/src/ui/web/dom/Clock.js +++ b/src/ui/web/dom/Clock.js @@ -1,4 +1,4 @@ -import {AbortError} from "../utils/error.js"; +import {AbortError} from "../../../utils/error.js"; class Timeout { constructor(ms) { @@ -37,7 +37,7 @@ class TimeMeasure { } } -export class Clock { +export default class Clock { createMeasure() { return new TimeMeasure(); } diff --git a/src/ui/web/dom/OnlineStatus.js b/src/ui/web/dom/OnlineStatus.js index 56276de0..dc17a2ba 100644 --- a/src/ui/web/dom/OnlineStatus.js +++ b/src/ui/web/dom/OnlineStatus.js @@ -1,4 +1,6 @@ -export class OnlineStatus extends ObservableValue { +import {BaseObservableValue} from "../../../observable/ObservableValue.js"; + +export default class OnlineStatus extends BaseObservableValue { constructor() { super(); this._onOffline = this._onOffline.bind(this); diff --git a/src/ui/web/general/SwitchView.js b/src/ui/web/general/SwitchView.js index 6ab669e0..3b40c2bb 100644 --- a/src/ui/web/general/SwitchView.js +++ b/src/ui/web/general/SwitchView.js @@ -34,7 +34,7 @@ export default class SwitchView { return this._childView; } } - +/* // SessionLoadView // should this be the new switch view? // and the other one be the BasicSwitchView? @@ -50,8 +50,8 @@ new BoundSwitchView(vm, vm => vm.isLoading, (loading, vm) => { return new SessionView(vm.sessionViewModel); } }); - -class BoundSwitchView extends SwitchView { +*/ +export class BoundSwitchView extends SwitchView { constructor(value, mapper, viewCreator) { super(viewCreator(mapper(value), value)); this._mapper = mapper; diff --git a/src/utils/enum.js b/src/utils/enum.js new file mode 100644 index 00000000..56b14a77 --- /dev/null +++ b/src/utils/enum.js @@ -0,0 +1,7 @@ +export default function createEnum(...values) { + const obj = {}; + for (const value of values) { + obj[value] = value; + } + return Object.freeze(obj); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bc0ca49c..bac88dfc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -234,10 +234,10 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -impunity@^0.0.10: - version "0.0.10" - resolved "https://registry.yarnpkg.com/impunity/-/impunity-0.0.10.tgz#b4e47c85db53279ca7fcf2e07f7ffb111b050e49" - integrity sha512-orL7IaDV//74U6GDyw7j7wcLwxhhLpXStyZ+Pz4O1UEYx1zlCojfpBNuq26Mzbaw0HMEwrMMi4JnLQ9lz3HVFg== +impunity@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/impunity/-/impunity-0.0.11.tgz#216da6860ad17dd360fdaa2b15d7006579b5dd8a" + integrity sha512-EZUlc/Qx7oaRXZY+PtewrPby63sWZQsEtjGFB05XfbL/20SBkR8ksFnBahkeOD2/ErNkO3vh8AV0oDbdSSS8jQ== dependencies: colors "^1.3.3" commander "^2.19.0" From 8c56ac3e4fe7c5d74de66670080384ba524ba8f2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 19 Apr 2020 19:05:12 +0200 Subject: [PATCH 09/93] rename NetworkError to ConnectionError --- src/matrix/SendScheduler.js | 4 ++-- src/matrix/SessionContainer.js | 6 +++--- src/matrix/error.js | 2 +- src/matrix/net/HomeServerApi.js | 4 ++-- src/matrix/net/Reconnector.js | 6 +++--- src/matrix/net/request/fetch.js | 4 ++-- src/matrix/net/request/replay.js | 6 +++--- src/matrix/room/sending/SendQueue.js | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/matrix/SendScheduler.js b/src/matrix/SendScheduler.js index 3beb4fa1..ac6e557f 100644 --- a/src/matrix/SendScheduler.js +++ b/src/matrix/SendScheduler.js @@ -1,5 +1,5 @@ import Platform from "../Platform.js"; -import {HomeServerError, NetworkError} from "./error.js"; +import {HomeServerError, ConnectionError} from "./error.js"; export class RateLimitingBackoff { constructor() { @@ -88,7 +88,7 @@ export class SendScheduler { // this can throw! result = await this._doSend(request.sendCallback); } catch (err) { - if (err instanceof NetworkError) { + if (err instanceof ConnectionError) { // we're offline, everybody will have // to re-request slots when we come back online this._offline = true; diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 78c63560..b71fd213 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -81,7 +81,7 @@ export class SessionContainer { this._loginFailure = LoginFailure.Unknown; } this._status.set(LoadStatus.LoginFailure); - } else if (err instanceof NetworkError) { + } else if (err instanceof ConnectionError) { this._loginFailure = LoginFailure.Network; this._status.set(LoadStatus.LoginFailure); } else { @@ -150,10 +150,10 @@ export class SessionContainer { try { await this._sync.start(); } catch (err) { - // swallow NetworkError here and continue, + // swallow ConnectionError here and continue, // as the reconnector above will call // sync.start again to retry in this case - if (!(err instanceof NetworkError)) { + if (!(err instanceof ConnectionError)) { throw err; } } diff --git a/src/matrix/error.js b/src/matrix/error.js index 2ba95037..a4979952 100644 --- a/src/matrix/error.js +++ b/src/matrix/error.js @@ -15,5 +15,5 @@ export class HomeServerError extends Error { export {AbortError} from "../utils/error.js"; -export class NetworkError extends Error { +export class ConnectionError extends Error { } diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 755d4825..cba5e591 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -1,6 +1,6 @@ import { HomeServerError, - NetworkError, + ConnectionError, } from "./error.js"; class RequestWrapper { @@ -85,7 +85,7 @@ export default class HomeServerApi { if (this._reconnector) { wrapper.response().catch(err => { - if (err instanceof NetworkError) { + if (err instanceof ConnectionError) { this._reconnector.onRequestFailed(this); } }); diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index ea2c86bf..fb83704e 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -1,6 +1,6 @@ import createEnum from "../../utils/enum.js"; import {AbortError} from "../../utils/error.js"; -import {NetworkError} from "../error.js" +import {ConnectionError} from "../error.js" import ObservableValue from "../../observable/ObservableValue.js"; export const ConnectionStatus = createEnum( @@ -93,7 +93,7 @@ export class Reconnector { this._versionsResponse = await versionsRequest.response(); this._setState(ConnectionStatus.Online); } catch (err) { - if (err instanceof NetworkError) { + if (err instanceof ConnectionError) { this._setState(ConnectionStatus.Waiting); try { await this._retryDelay.waitForRetry(); @@ -122,7 +122,7 @@ export function tests() { response() { if (remainingFailures) { remainingFailures -= 1; - return Promise.reject(new NetworkError()); + return Promise.reject(new ConnectionError()); } else { return Promise.resolve(42); } diff --git a/src/matrix/net/request/fetch.js b/src/matrix/net/request/fetch.js index 76929c6b..dff1c527 100644 --- a/src/matrix/net/request/fetch.js +++ b/src/matrix/net/request/fetch.js @@ -1,6 +1,6 @@ import { AbortError, - NetworkError + ConnectionError } from "../error.js"; class RequestResult { @@ -58,7 +58,7 @@ export default function fetchRequest(url, options) { // // One could check navigator.onLine to rule out the first // but the 2 latter ones are indistinguishable from javascript. - throw new NetworkError(`${options.method} ${url}: ${err.message}`); + throw new ConnectionError(`${options.method} ${url}: ${err.message}`); } throw err; }); diff --git a/src/matrix/net/request/replay.js b/src/matrix/net/request/replay.js index f47e6d47..916de9d6 100644 --- a/src/matrix/net/request/replay.js +++ b/src/matrix/net/request/replay.js @@ -1,6 +1,6 @@ import { AbortError, - NetworkError + ConnectionError } from "../error.js"; class RequestLogItem { @@ -24,7 +24,7 @@ class RequestLogItem { this.end = performance.now(); this.error = { aborted: err instanceof AbortError, - network: err instanceof NetworkError, + network: err instanceof ConnectionError, message: err.message, }; } @@ -98,7 +98,7 @@ class ReplayRequestResult { if (error.aborted || this._aborted) { throw new AbortError(error.message); } else if (error.network) { - throw new NetworkError(error.message); + throw new ConnectionError(error.message); } else { throw new Error(error.message); } diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 3c9f90c3..caea41fa 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -1,5 +1,5 @@ import SortedArray from "../../../observable/list/SortedArray.js"; -import {NetworkError} from "../../error.js"; +import {ConnectionError} from "../../error.js"; import PendingEvent from "./PendingEvent.js"; function makeTxnId() { @@ -52,7 +52,7 @@ export default class SendQueue { console.log("keep sending?", this._amountSent, "<", this._pendingEvents.length); } } catch(err) { - if (err instanceof NetworkError) { + if (err instanceof ConnectionError) { this._offline = true; } } finally { From 80f7caadbeda91773991f032dacc5afbe53f283f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 19 Apr 2020 19:13:38 +0200 Subject: [PATCH 10/93] rename SessionsStore to SessionInfoStorage --- doc/impl-thoughts/RECONNECTING.md | 2 +- src/domain/BrawlViewModel.js | 16 ++++++++-------- src/domain/SessionPickerViewModel.js | 12 ++++++------ src/main.js | 4 ++-- src/matrix/{session.js => Session.js} | 0 src/matrix/{sync.js => Sync.js} | 0 .../localstorage/SessionInfoStorage.js} | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) rename src/matrix/{session.js => Session.js} (100%) rename src/matrix/{sync.js => Sync.js} (100%) rename src/matrix/{sessions-store/localstorage/SessionsStore.js => sessioninfo/localstorage/SessionInfoStorage.js} (97%) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 7668096f..862c2f4e 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -40,7 +40,7 @@ rooms should report how many messages they have queued up, and each time they se - put in own file - add waitFor (won't this leak if the promise never resolves?) - decide whether we want to inherit (no?) - - cleanup Reconnector with recent changes, move generic code, make imports work + - DONE: cleanup Reconnector with recent changes, move generic code, make imports work - add SyncStatus as ObservableValue of enum in Sync - show load progress in LoginView/SessionPickView and do away with loading screen - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 3ed861fe..c9d1b775 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,5 +1,5 @@ -import Session from "../matrix/session.js"; -import Sync from "../matrix/sync.js"; +import Session from "../matrix/Session.js"; +import Sync from "../matrix/Sync.js"; import SessionViewModel from "./session/SessionViewModel.js"; import LoginViewModel from "./LoginViewModel.js"; import SessionPickerViewModel from "./SessionPickerViewModel.js"; @@ -10,10 +10,10 @@ export function createNewSessionId() { } export default class BrawlViewModel extends EventEmitter { - constructor({storageFactory, sessionStore, createHsApi, clock}) { + constructor({storageFactory, sessionInfoStorage, createHsApi, clock}) { super(); this._storageFactory = storageFactory; - this._sessionStore = sessionStore; + this._sessionInfoStorage = sessionInfoStorage; this._createHsApi = createHsApi; this._clock = clock; @@ -26,7 +26,7 @@ export default class BrawlViewModel extends EventEmitter { } async load() { - if (await this._sessionStore.hasAnySession()) { + if (await this._sessionInfoStorage.hasAnySession()) { this._showPicker(); } else { this._showLogin(); @@ -36,7 +36,7 @@ export default class BrawlViewModel extends EventEmitter { async _showPicker() { this._setSection(() => { this._sessionPickerViewModel = new SessionPickerViewModel({ - sessionStore: this._sessionStore, + sessionInfoStorage: this._sessionInfoStorage, storageFactory: this._storageFactory, sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) }); @@ -116,7 +116,7 @@ export default class BrawlViewModel extends EventEmitter { accessToken: loginData.access_token, lastUsed: this._clock.now() }; - await this._sessionStore.add(sessionInfo); + await this._sessionInfoStorage.add(sessionInfo); this._loadSession(sessionInfo); } else { this._showPicker(); @@ -126,7 +126,7 @@ export default class BrawlViewModel extends EventEmitter { _onSessionPicked(sessionInfo) { if (sessionInfo) { this._loadSession(sessionInfo); - this._sessionStore.updateLastUsed(sessionInfo.id, this._clock.now()); + this._sessionInfoStorage.updateLastUsed(sessionInfo.id, this._clock.now()); } else { this._showLogin(); } diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 97efdcf3..a191b317 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -99,15 +99,15 @@ class SessionItemViewModel extends EventEmitter { } export default class SessionPickerViewModel { - constructor({storageFactory, sessionStore, sessionCallback}) { + constructor({storageFactory, sessionInfoStorage, sessionCallback}) { this._storageFactory = storageFactory; - this._sessionStore = sessionStore; + this._sessionInfoStorage = sessionInfoStorage; this._sessionCallback = sessionCallback; this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id)); } async load() { - const sessions = await this._sessionStore.getAll(); + const sessions = await this._sessionInfoStorage.getAll(); this._sessions.setManyUnsorted(sessions.map(s => new SessionItemViewModel(s, this))); } @@ -119,7 +119,7 @@ export default class SessionPickerViewModel { } async _exportData(id) { - const sessionInfo = await this._sessionStore.get(id); + const sessionInfo = await this._sessionInfoStorage.get(id); const stores = await this._storageFactory.export(id); const data = {sessionInfo, stores}; return data; @@ -131,13 +131,13 @@ export default class SessionPickerViewModel { sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`; sessionInfo.id = createNewSessionId(); await this._storageFactory.import(sessionInfo.id, data.stores); - await this._sessionStore.add(sessionInfo); + await this._sessionInfoStorage.add(sessionInfo); this._sessions.set(new SessionItemViewModel(sessionInfo, this)); } async delete(id) { const idx = this._sessions.array.findIndex(s => s.id === id); - await this._sessionStore.delete(id); + await this._sessionInfoStorage.delete(id); await this._storageFactory.delete(id); this._sessions.remove(idx); } diff --git a/src/main.js b/src/main.js index b2aa8831..ed208f31 100644 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,7 @@ import HomeServerApi from "./matrix/net/HomeServerApi.js"; import fetchRequest from "./matrix/net/fetch.js"; import {Reconnector} from "./matrix/net/connection/Reconnector.js"; import StorageFactory from "./matrix/storage/idb/create.js"; -import SessionsStore from "./matrix/sessions-store/localstorage/SessionsStore.js"; +import SessionInfoStorage from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import BrawlViewModel from "./domain/BrawlViewModel.js"; import BrawlView from "./ui/web/BrawlView.js"; import DOMClock from "./ui/web/dom/Clock.js"; @@ -27,7 +27,7 @@ export default async function main(container) { const vm = new BrawlViewModel({ storageFactory: new StorageFactory(), createHsApi: (homeServer, accessToken, reconnector) => new HomeServerApi({homeServer, accessToken, request, reconnector}), - sessionStore: new SessionsStore("brawl_sessions_v1"), + sessionInfoStorage: new SessionInfoStorage("brawl_sessions_v1"), clock: new DOMClock(), }); await vm.load(); diff --git a/src/matrix/session.js b/src/matrix/Session.js similarity index 100% rename from src/matrix/session.js rename to src/matrix/Session.js diff --git a/src/matrix/sync.js b/src/matrix/Sync.js similarity index 100% rename from src/matrix/sync.js rename to src/matrix/Sync.js diff --git a/src/matrix/sessions-store/localstorage/SessionsStore.js b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js similarity index 97% rename from src/matrix/sessions-store/localstorage/SessionsStore.js rename to src/matrix/sessioninfo/localstorage/SessionInfoStorage.js index dbe6fda5..abd9ffb2 100644 --- a/src/matrix/sessions-store/localstorage/SessionsStore.js +++ b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js @@ -1,4 +1,4 @@ -export default class SessionsStore { +export default class SessionInfoStorage { constructor(name) { this._name = name; } From 72b0eefccb0faaf2e82be6aa93a560524352d645 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 19 Apr 2020 19:52:26 +0200 Subject: [PATCH 11/93] replace isSyncing and emit with an Observable SyncStatus --- doc/impl-thoughts/RECONNECTING.md | 7 ++-- src/matrix/SessionContainer.js | 7 ++-- src/matrix/Sync.js | 59 ++++++++++++++++++------------- 3 files changed, 41 insertions(+), 32 deletions(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 862c2f4e..78293fba 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -41,11 +41,12 @@ rooms should report how many messages they have queued up, and each time they se - add waitFor (won't this leak if the promise never resolves?) - decide whether we want to inherit (no?) - DONE: cleanup Reconnector with recent changes, move generic code, make imports work - - add SyncStatus as ObservableValue of enum in Sync - - show load progress in LoginView/SessionPickView and do away with loading screen + - DONE: add SyncStatus as ObservableValue of enum in Sync + - cleanup SessionContainer - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing + - show load progress in LoginView/SessionPickView and do away with loading screen - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer - - rename SessionsStore to SessionInfoStorage + - DONE: rename SessionsStore to SessionInfoStorage - make sure we've renamed all \*State enums and fields to \*Status - add pendingMessageCount prop to SendQueue and Room, aggregate this in Session - add completedFirstSync to Sync, so we can check if the catchup or initial sync is still in progress diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index b71fd213..777ddf9f 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -6,8 +6,7 @@ export const LoadStatus = createEnum( "LoginFailed", "Loading", "Migrating", //not used atm, but would fit here - "InitialSync", - "CatchupSync", + "FirstSync", "Error", "Ready", ); @@ -127,7 +126,6 @@ export class SessionContainer { if (!needsInitialSync) { this._status.set(LoadStatus.CatchupSync); } else { - this._status.set(LoadStatus.InitialSync); } this._sync = new Sync({hsApi, storage, session: this._session}); @@ -148,7 +146,8 @@ export class SessionContainer { async _waitForFirstSync() { try { - await this._sync.start(); + this._sync.start(); + this._status.set(LoadStatus.FirstSync); } catch (err) { // swallow ConnectionError here and continue, // as the reconnector above will call diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 56208198..ae2dd264 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -1,9 +1,17 @@ import {AbortError} from "./error.js"; -import EventEmitter from "../EventEmitter.js"; +import ObservableValue from "../observable/ObservableValue.js"; +import {createEnum} from "../utils/enum.js"; const INCREMENTAL_TIMEOUT = 30000; const SYNC_EVENT_LIMIT = 10; +export const SyncStatus = createEnum( + "InitialSync", + "CatchupSync", + "Syncing", + "Stopped" +); + function parseRooms(roomsSection, roomCallback) { if (roomsSection) { const allMemberships = ["join", "invite", "leave"]; @@ -19,53 +27,54 @@ function parseRooms(roomsSection, roomCallback) { return []; } -export default class Sync extends EventEmitter { +export default class Sync { constructor({hsApi, session, storage}) { - super(); this._hsApi = hsApi; this._session = session; this._storage = storage; - this._isSyncing = false; this._currentRequest = null; + this._status = new ObservableValue(SyncStatus.Stopped); + this._error = null; } - get isSyncing() { - return this._isSyncing; + get status() { + return this._status; } - // this should not throw? - // returns when initial sync is done - async start() { - if (this._isSyncing) { + /** the error that made the sync stop */ + get error() { + return this._error; + } + + start() { + // not already syncing? + if (this._status.get() !== SyncStatus.Stopped) { return; } - this._isSyncing = true; - this.emit("status", "started"); let syncToken = this._session.syncToken; - // do initial sync if needed - if (!syncToken) { - // need to create limit filter here - syncToken = await this._syncRequest(); + if (syncToken) { + this._status.set(SyncStatus.CatchupSync); + } else { + this._status.set(SyncStatus.InitialSync); } this._syncLoop(syncToken); } async _syncLoop(syncToken) { // if syncToken is falsy, it will first do an initial sync ... - while(this._isSyncing) { + while(this._status.get() !== SyncStatus.Stopped) { try { console.log(`starting sync request with since ${syncToken} ...`); - syncToken = await this._syncRequest(syncToken, INCREMENTAL_TIMEOUT); + const timeout = syncToken ? INCREMENTAL_TIMEOUT : undefined; + syncToken = await this._syncRequest(syncToken, timeout); + this._status.set(SyncStatus.Syncing); } catch (err) { - this._isSyncing = false; if (!(err instanceof AbortError)) { - console.error("stopping sync because of error"); - console.error(err); - this.emit("status", "error", err); + this._error = err; + this._status.set(SyncStatus.Stopped); } } } - this.emit("status", "stopped"); } async _syncRequest(syncToken, timeout) { @@ -128,10 +137,10 @@ export default class Sync extends EventEmitter { } stop() { - if (!this._isSyncing) { + if (this._status.get() === SyncStatus.Stopped) { return; } - this._isSyncing = false; + this._status.set(SyncStatus.Stopped); if (this._currentRequest) { this._currentRequest.abort(); this._currentRequest = null; From 87b23d062c54f3c9f138447be27faa9e39047ab7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 19:47:45 +0200 Subject: [PATCH 12/93] fix imports after reshuffleling --- src/domain/BrawlViewModel.js | 2 +- src/main.js | 6 +++--- src/matrix/Sync.js | 4 ++-- src/matrix/net/ExponentialRetryDelay.js | 2 +- src/matrix/net/HomeServerApi.js | 2 +- src/matrix/net/Reconnector.js | 2 +- src/matrix/net/request/fetch.js | 2 +- src/matrix/net/request/replay.js | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index c9d1b775..e4ccd0ac 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,5 +1,5 @@ import Session from "../matrix/Session.js"; -import Sync from "../matrix/Sync.js"; +import {Sync} from "../matrix/Sync.js"; import SessionViewModel from "./session/SessionViewModel.js"; import LoginViewModel from "./LoginViewModel.js"; import SessionPickerViewModel from "./SessionPickerViewModel.js"; diff --git a/src/main.js b/src/main.js index ed208f31..ef1b8c9c 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ import HomeServerApi from "./matrix/net/HomeServerApi.js"; -// import {RecordRequester, ReplayRequester} from "./matrix/net/replay.js"; -import fetchRequest from "./matrix/net/fetch.js"; -import {Reconnector} from "./matrix/net/connection/Reconnector.js"; +// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; +import fetchRequest from "./matrix/net/request/fetch.js"; +import {Reconnector} from "./matrix/net/Reconnector.js"; import StorageFactory from "./matrix/storage/idb/create.js"; import SessionInfoStorage from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import BrawlViewModel from "./domain/BrawlViewModel.js"; diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index ae2dd264..8b79c6da 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -1,6 +1,6 @@ import {AbortError} from "./error.js"; import ObservableValue from "../observable/ObservableValue.js"; -import {createEnum} from "../utils/enum.js"; +import createEnum from "../utils/enum.js"; const INCREMENTAL_TIMEOUT = 30000; const SYNC_EVENT_LIMIT = 10; @@ -27,7 +27,7 @@ function parseRooms(roomsSection, roomCallback) { return []; } -export default class Sync { +export class Sync { constructor({hsApi, session, storage}) { this._hsApi = hsApi; this._session = session; diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.js index 056f0a1b..42ef364e 100644 --- a/src/matrix/net/ExponentialRetryDelay.js +++ b/src/matrix/net/ExponentialRetryDelay.js @@ -43,7 +43,7 @@ export default class ExponentialRetryDelay { } -import MockClock from "../../../mocks/Clock.js"; +import MockClock from "../../mocks/Clock.js"; export function tests() { return { diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index cba5e591..53c4cca4 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -1,7 +1,7 @@ import { HomeServerError, ConnectionError, -} from "./error.js"; +} from "../error.js"; class RequestWrapper { constructor(method, url, requestResult) { diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index fb83704e..48af075e 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -111,7 +111,7 @@ export class Reconnector { } -import MockClock from "../../../mocks/Clock.js"; +import MockClock from "../../mocks/Clock.js"; import ExponentialRetryDelay from "./ExponentialRetryDelay.js"; export function tests() { diff --git a/src/matrix/net/request/fetch.js b/src/matrix/net/request/fetch.js index dff1c527..1947d165 100644 --- a/src/matrix/net/request/fetch.js +++ b/src/matrix/net/request/fetch.js @@ -1,7 +1,7 @@ import { AbortError, ConnectionError -} from "../error.js"; +} from "../../error.js"; class RequestResult { constructor(promise, controller) { diff --git a/src/matrix/net/request/replay.js b/src/matrix/net/request/replay.js index 916de9d6..c6f12269 100644 --- a/src/matrix/net/request/replay.js +++ b/src/matrix/net/request/replay.js @@ -1,7 +1,7 @@ import { AbortError, ConnectionError -} from "../error.js"; +} from "../../error.js"; class RequestLogItem { constructor(url, options) { From 164d9d594f844726bb5dabbf507380fb0198677f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 19:48:21 +0200 Subject: [PATCH 13/93] finish implemenation of SessionContainer --- doc/impl-thoughts/RECONNECTING.md | 3 ++- src/matrix/SendScheduler.js | 14 +++++++--- src/matrix/Session.js | 19 +++++++++++++- src/matrix/SessionContainer.js | 43 ++++++++++++++++++------------- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 78293fba..5e507834 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -42,7 +42,8 @@ rooms should report how many messages they have queued up, and each time they se - decide whether we want to inherit (no?) - DONE: cleanup Reconnector with recent changes, move generic code, make imports work - DONE: add SyncStatus as ObservableValue of enum in Sync - - cleanup SessionContainer + - DONE: cleanup SessionContainer + - move all imports to non-default - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing - show load progress in LoginView/SessionPickView and do away with loading screen - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer diff --git a/src/matrix/SendScheduler.js b/src/matrix/SendScheduler.js index ac6e557f..1280ca6a 100644 --- a/src/matrix/SendScheduler.js +++ b/src/matrix/SendScheduler.js @@ -48,7 +48,7 @@ export class SendScheduler { this._hsApi = hsApi; this._sendRequests = []; this._sendScheduled = false; - this._offline = false; + this._stopped = false; this._waitTime = 0; this._backoff = backoff; /* @@ -66,6 +66,14 @@ export class SendScheduler { // TODO: abort current requests and set offline } + start() { + this._stopped = false; + } + + get isStarted() { + return !this._stopped; + } + // this should really be per roomId to avoid head-of-line blocking // // takes a callback instead of returning a promise with the slot @@ -74,7 +82,7 @@ export class SendScheduler { let request; const promise = new Promise((resolve, reject) => request = {resolve, reject, sendCallback}); this._sendRequests.push(request); - if (!this._sendScheduled && !this._offline) { + if (!this._sendScheduled && !this._stopped) { this._sendLoop(); } return promise; @@ -91,7 +99,7 @@ export class SendScheduler { if (err instanceof ConnectionError) { // we're offline, everybody will have // to re-request slots when we come back online - this._offline = true; + this._stopped = true; for (const r of this._sendRequests) { r.reject(err); } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index d513d0b2..7c4771e4 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -40,11 +40,28 @@ export default class Session { })); } + get isStarted() { + return this._sendScheduler.isStarted; + } + stop() { this._sendScheduler.stop(); } - start(lastVersionResponse) { + async start(lastVersionResponse) { + if (lastVersionResponse) { + // store /versions response + const txn = await this._storage.readWriteTxn([ + this._storage.storeNames.session + ]); + const newSessionData = Object.assign({}, this._session, {serverVersions: lastVersionResponse}); + txn.session.set(newSessionData); + // TODO: what can we do if this throws? + await txn.complete(); + this._session = newSessionData; + } + + this._sendScheduler.start(); for (const [, room] of this._rooms) { room.resumeSending(); } diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 777ddf9f..aad58959 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,4 +1,11 @@ +import createEnum from "../utils/enum.js"; +import ObservableValue from "../observable/ObservableValue.js"; import HomeServerApi from "./net/HomeServerApi.js"; +import {Reconnector, ConnectionStatus} from "./net/Reconnector.js"; +import ExponentialRetryDelay from "./net/ExponentialRetryDelay.js"; +import {HomeServerError, ConnectionError, AbortError} from "./error.js"; +import {Sync, SyncStatus} from "./Sync.js"; +import Session from "./Session.js"; export const LoadStatus = createEnum( "NotLoading", @@ -12,19 +19,19 @@ export const LoadStatus = createEnum( ); export const LoginFailure = createEnum( - "Network", + "Connection", "Credentials", "Unknown", ); export class SessionContainer { - constructor({clock, random, onlineStatus, request, storageFactory, sessionsStore}) { + constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage}) { this._random = random; this._clock = clock; this._onlineStatus = onlineStatus; this._request = request; this._storageFactory = storageFactory; - this._sessionsStore = sessionsStore; + this._sessionInfoStorage = sessionInfoStorage; this._status = new ObservableValue(LoadStatus.NotLoading); this._error = null; @@ -44,7 +51,7 @@ export class SessionContainer { } this._status.set(LoadStatus.Loading); try { - const sessionInfo = await this._sessionsStore.get(sessionId); + const sessionInfo = await this._sessionInfoStorage.get(sessionId); await this._loadSessionInfo(sessionInfo); } catch (err) { this._error = err; @@ -70,7 +77,7 @@ export class SessionContainer { accessToken: loginData.access_token, lastUsed: this._clock.now() }; - await this._sessionsStore.add(sessionInfo); + await this._sessionInfoStorage.add(sessionInfo); } catch (err) { this._error = err; if (err instanceof HomeServerError) { @@ -81,7 +88,7 @@ export class SessionContainer { } this._status.set(LoadStatus.LoginFailure); } else if (err instanceof ConnectionError) { - this._loginFailure = LoginFailure.Network; + this._loginFailure = LoginFailure.Connection; this._status.set(LoadStatus.LoginFailure); } else { this._status.set(LoadStatus.Error); @@ -122,12 +129,6 @@ export class SessionContainer { this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); await this._session.load(); - const needsInitialSync = !this._session.syncToken; - if (!needsInitialSync) { - this._status.set(LoadStatus.CatchupSync); - } else { - } - this._sync = new Sync({hsApi, storage, session: this._session}); // notify sync and session when back online this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { @@ -137,17 +138,23 @@ export class SessionContainer { } }); await this._waitForFirstSync(); + this._status.set(LoadStatus.Ready); - // if this fails, the reconnector will start polling versions to reconnect - const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); - this._session.start(lastVersionsResponse); + // if the sync failed, and then the reconnector + // restored the connection, it would have already + // started to session, so check first + // to prevent an extra /versions request + if (!this._session.isStarted) { + const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); + this._session.start(lastVersionsResponse); + } } async _waitForFirstSync() { try { - this._sync.start(); this._status.set(LoadStatus.FirstSync); + this._sync.start(); } catch (err) { // swallow ConnectionError here and continue, // as the reconnector above will call @@ -209,7 +216,7 @@ function main() { const sessionFactory = new SessionFactory({ Clock: DOMClock, OnlineState: DOMOnlineState, - SessionsStore: LocalStorageSessionStore, // should be called SessionInfoStore? + SessionInfoStorage: LocalStorageSessionStore, // should be called SessionInfoStore? StorageFactory: window.indexedDB ? IDBStorageFactory : MemoryStorageFactory, // should be called StorageManager? // should be moved to StorageFactory as `KeyBounds`?: minStorageKey, middleStorageKey, maxStorageKey // would need to pass it into EventKey though @@ -233,7 +240,7 @@ function main() { const container = sessionFactory.startWithLogin(server, username, password); const container = sessionFactory.startWithExistingSession(sessionId); // container.loadStatus is an ObservableValue - await container.loadStatus.waitFor(s => s === LoadStatus.Loaded || s === LoadStatus.CatchupSync); + await container.loadStatus.waitFor(s => s === LoadStatus.FirstSync && container.sync.status === SyncStatus.CatchupSync || s === LoadStatus.Ready); // loader isn't needed anymore from now on const {session, sync, reconnector} = container; From 0de5e899eae91e19ecae3c2cddb2d8317a5043c0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:25:17 +0200 Subject: [PATCH 14/93] remove dead code --- src/ui/web/general/ListView.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js index fb5be255..4bd14f7c 100644 --- a/src/ui/web/general/ListView.js +++ b/src/ui/web/general/ListView.js @@ -1,13 +1,5 @@ import {tag} from "./html.js"; -class UIView { - mount() {} - unmount() {} - update(_value) {} - // can only be called between a call to mount and unmount - root() {} -} - function insertAt(parentNode, idx, childNode) { const isLast = idx === parentNode.childElementCount; if (isLast) { From 001dbefbcfa559aae43af91cb6a677e605b5b7a8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:26:39 +0200 Subject: [PATCH 15/93] stop using default exports because it becomes hard to remember where you used them and where not --- src/EventEmitter.js | 2 +- src/Platform.js | 2 +- src/domain/BrawlViewModel.js | 12 ++++++------ src/domain/LoginViewModel.js | 4 ++-- src/domain/SessionPickerViewModel.js | 4 ++-- src/domain/session/SessionLoadViewModel.js | 10 +++++----- src/domain/session/SessionViewModel.js | 10 +++++----- src/domain/session/SyncStatusViewModel.js | 4 ++-- src/domain/session/room/RoomViewModel.js | 6 +++--- .../session/room/timeline/TilesCollection.js | 10 +++++----- .../session/room/timeline/TimelineViewModel.js | 6 +++--- .../session/room/timeline/UpdateAction.js | 2 +- .../session/room/timeline/tiles/GapTile.js | 6 +++--- .../session/room/timeline/tiles/ImageTile.js | 4 ++-- .../room/timeline/tiles/LocationTile.js | 4 ++-- .../session/room/timeline/tiles/MessageTile.js | 4 ++-- .../room/timeline/tiles/RoomMemberTile.js | 4 ++-- .../room/timeline/tiles/RoomNameTile.js | 4 ++-- .../session/room/timeline/tiles/SimpleTile.js | 4 ++-- .../session/room/timeline/tiles/TextTile.js | 4 ++-- .../session/room/timeline/tilesCreator.js | 12 ++++++------ .../session/roomlist/RoomTileViewModel.js | 2 +- src/main.js | 16 ++++++++-------- src/matrix/SendScheduler.js | 2 +- src/matrix/Session.js | 6 +++--- src/matrix/SessionContainer.js | 12 ++++++------ src/matrix/Sync.js | 4 ++-- src/matrix/User.js | 2 +- src/matrix/net/ExponentialRetryDelay.js | 4 ++-- src/matrix/net/HomeServerApi.js | 2 +- src/matrix/net/Reconnector.js | 8 ++++---- src/matrix/net/request/fetch.js | 2 +- src/matrix/room/room.js | 16 ++++++++-------- src/matrix/room/sending/PendingEvent.js | 2 +- src/matrix/room/sending/SendQueue.js | 6 +++--- src/matrix/room/summary.js | 2 +- src/matrix/room/timeline/Direction.js | 4 +--- src/matrix/room/timeline/EventKey.js | 4 ++-- src/matrix/room/timeline/FragmentIdComparer.js | 2 +- src/matrix/room/timeline/Timeline.js | 10 +++++----- src/matrix/room/timeline/entries/BaseEntry.js | 4 ++-- src/matrix/room/timeline/entries/EventEntry.js | 4 ++-- .../timeline/entries/FragmentBoundaryEntry.js | 8 ++++---- .../room/timeline/entries/PendingEventEntry.js | 4 ++-- .../room/timeline/persistence/GapWriter.js | 6 +++--- .../room/timeline/persistence/SyncWriter.js | 8 ++++---- .../timeline/persistence/TimelineReader.js | 8 ++++---- .../localstorage/SessionInfoStorage.js | 2 +- src/matrix/storage/idb/create.js | 4 ++-- src/matrix/storage/idb/query-target.js | 2 +- src/matrix/storage/idb/storage.js | 4 ++-- src/matrix/storage/idb/store.js | 4 ++-- .../storage/idb/stores/PendingEventStore.js | 4 ++-- .../storage/idb/stores/RoomStateStore.js | 2 +- .../storage/idb/stores/RoomSummaryStore.js | 2 +- src/matrix/storage/idb/stores/SessionStore.js | 2 +- .../storage/idb/stores/TimelineEventStore.js | 6 +++--- .../idb/stores/TimelineFragmentStore.js | 4 ++-- src/matrix/storage/idb/transaction.js | 16 ++++++++-------- src/matrix/storage/memory/Storage.js | 4 ++-- src/matrix/storage/memory/Transaction.js | 4 ++-- .../storage/memory/stores/RoomTimelineStore.js | 8 ++++---- src/matrix/storage/memory/stores/Store.js | 2 +- src/mocks/Clock.js | 4 ++-- src/observable/BaseObservable.js | 2 +- src/observable/ObservableValue.js | 4 ++-- src/observable/index.js | 18 +++++++++--------- src/observable/list/BaseObservableList.js | 4 ++-- src/observable/list/ConcatList.js | 6 +++--- src/observable/list/MappedList.js | 4 ++-- src/observable/list/ObservableArray.js | 4 ++-- src/observable/list/SortedArray.js | 6 +++--- src/observable/list/SortedMapList.js | 8 ++++---- src/observable/map/BaseObservableMap.js | 4 ++-- src/observable/map/FilteredMap.js | 4 ++-- src/observable/map/MappedMap.js | 4 ++-- src/observable/map/ObservableMap.js | 4 ++-- src/ui/web/BrawlView.js | 12 ++++++------ src/ui/web/WebPlatform.js | 2 +- src/ui/web/dom/Clock.js | 2 +- src/ui/web/dom/OnlineStatus.js | 2 +- src/ui/web/general/ListView.js | 2 +- src/ui/web/general/SwitchView.js | 2 +- src/ui/web/general/Template.js | 2 +- src/ui/web/general/TemplateView.js | 4 ++-- src/ui/web/login/LoginView.js | 4 ++-- src/ui/web/login/SessionPickerView.js | 6 +++--- src/ui/web/session/RoomPlaceholderView.js | 2 +- src/ui/web/session/RoomTile.js | 4 ++-- src/ui/web/session/SessionView.js | 14 +++++++------- src/ui/web/session/SyncStatusBar.js | 4 ++-- src/ui/web/session/room/MessageComposer.js | 4 ++-- src/ui/web/session/room/RoomView.js | 8 ++++---- src/ui/web/session/room/TimelineList.js | 10 +++++----- .../session/room/timeline/AnnouncementView.js | 4 ++-- src/ui/web/session/room/timeline/GapView.js | 4 ++-- .../session/room/timeline/TextMessageView.js | 4 ++-- .../web/session/room/timeline/TimelineTile.js | 2 +- src/utils/enum.js | 4 ++-- src/utils/sortedIndex.js | 2 +- 100 files changed, 257 insertions(+), 259 deletions(-) diff --git a/src/EventEmitter.js b/src/EventEmitter.js index fd155a7e..24d03ec3 100644 --- a/src/EventEmitter.js +++ b/src/EventEmitter.js @@ -1,4 +1,4 @@ -export default class EventEmitter { +export class EventEmitter { constructor() { this._handlersByName = {}; } diff --git a/src/Platform.js b/src/Platform.js index b68d821d..bfdfbd96 100644 --- a/src/Platform.js +++ b/src/Platform.js @@ -1,5 +1,5 @@ //#ifdef PLATFORM_GNOME //##export {default} from "./ui/gnome/GnomePlatform.js"; //#else -export {default} from "./ui/web/WebPlatform.js"; +export {WebPlatform as Platform} from "./ui/web/WebPlatform.js"; //#endif diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index e4ccd0ac..45943da6 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,15 +1,15 @@ -import Session from "../matrix/Session.js"; +import {Session} from "../matrix/Session.js"; import {Sync} from "../matrix/Sync.js"; -import SessionViewModel from "./session/SessionViewModel.js"; -import LoginViewModel from "./LoginViewModel.js"; -import SessionPickerViewModel from "./SessionPickerViewModel.js"; -import EventEmitter from "../EventEmitter.js"; +import {SessionViewModel} from "./session/SessionViewModel.js"; +import {LoginViewModel} from "./LoginViewModel.js"; +import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; +import {EventEmitter} from "../EventEmitter.js"; export function createNewSessionId() { return (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); } -export default class BrawlViewModel extends EventEmitter { +export class BrawlViewModel extends EventEmitter { constructor({storageFactory, sessionInfoStorage, createHsApi, clock}) { super(); this._storageFactory = storageFactory; diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 5d0a9ef2..f26c5be0 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,6 +1,6 @@ -import EventEmitter from "../EventEmitter.js"; +import {EventEmitter} from "../EventEmitter.js"; -export default class LoginViewModel extends EventEmitter { +export class LoginViewModel extends EventEmitter { constructor({loginCallback, defaultHomeServer, createHsApi}) { super(); this._loginCallback = loginCallback; diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index a191b317..bc719a79 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,5 +1,5 @@ import {SortedArray} from "../observable/index.js"; -import EventEmitter from "../EventEmitter.js"; +import {EventEmitter} from "../EventEmitter.js"; import {createNewSessionId} from "./BrawlViewModel.js" class SessionItemViewModel extends EventEmitter { @@ -98,7 +98,7 @@ class SessionItemViewModel extends EventEmitter { } } -export default class SessionPickerViewModel { +export class SessionPickerViewModel { constructor({storageFactory, sessionInfoStorage, sessionCallback}) { this._storageFactory = storageFactory; this._sessionInfoStorage = sessionInfoStorage; diff --git a/src/domain/session/SessionLoadViewModel.js b/src/domain/session/SessionLoadViewModel.js index 3f3edba6..9193e840 100644 --- a/src/domain/session/SessionLoadViewModel.js +++ b/src/domain/session/SessionLoadViewModel.js @@ -1,9 +1,9 @@ -import EventEmitter from "../../EventEmitter.js"; -import RoomTileViewModel from "./roomlist/RoomTileViewModel.js"; -import RoomViewModel from "./room/RoomViewModel.js"; -import SyncStatusViewModel from "./SyncStatusViewModel.js"; +import {EventEmitter} from "../../EventEmitter.js"; +import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; +import {RoomViewModel} from "./room/RoomViewModel.js"; +import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; -export default class SessionLoadViewModel extends ViewModel { +export class SessionLoadViewModel extends ViewModel { constructor(options) { super(options); this._sessionContainer = options.sessionContainer; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 74e1f00c..02a48834 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,9 +1,9 @@ -import EventEmitter from "../../EventEmitter.js"; -import RoomTileViewModel from "./roomlist/RoomTileViewModel.js"; -import RoomViewModel from "./room/RoomViewModel.js"; -import SyncStatusViewModel from "./SyncStatusViewModel.js"; +import {EventEmitter} from "../../EventEmitter.js"; +import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; +import {RoomViewModel} from "./room/RoomViewModel.js"; +import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; -export default class SessionViewModel extends EventEmitter { +export class SessionViewModel extends EventEmitter { constructor({session, sync}) { super(); this._session = session; diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js index ef227873..17fb243c 100644 --- a/src/domain/session/SyncStatusViewModel.js +++ b/src/domain/session/SyncStatusViewModel.js @@ -1,6 +1,6 @@ -import EventEmitter from "../../EventEmitter.js"; +import {EventEmitter} from "../../EventEmitter.js"; -export default class SyncStatusViewModel extends EventEmitter { +export class SyncStatusViewModel extends EventEmitter { constructor(sync) { super(); this._sync = sync; diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 19a58944..8f117cbe 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -1,8 +1,8 @@ -import EventEmitter from "../../../EventEmitter.js"; -import TimelineViewModel from "./timeline/TimelineViewModel.js"; +import {EventEmitter} from "../../../EventEmitter.js"; +import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {avatarInitials} from "../avatar.js"; -export default class RoomViewModel extends EventEmitter { +export class RoomViewModel extends EventEmitter { constructor({room, ownUserId, closeCallback}) { super(); this._room = room; diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index cb2621d6..ce96f00e 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -1,12 +1,12 @@ -import BaseObservableList from "../../../../observable/list/BaseObservableList.js"; -import sortedIndex from "../../../../utils/sortedIndex.js"; +import {BaseObservableList} from "../../../../observable/list/BaseObservableList.js"; +import {sortedIndex} from "../../../../utils/sortedIndex.js"; // maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary // for now, tileCreator should be stable in whether it returns a tile or not. // e.g. the decision to create a tile or not should be based on properties // not updated later on (e.g. event type) // also see big comment in onUpdate -export default class TilesCollection extends BaseObservableList { +export class TilesCollection extends BaseObservableList { constructor(entries, tileCreator) { super(); this._entries = entries; @@ -187,8 +187,8 @@ export default class TilesCollection extends BaseObservableList { } } -import ObservableArray from "../../../../observable/list/ObservableArray.js"; -import UpdateAction from "./UpdateAction.js"; +import {ObservableArray} from "../../../../observable/list/ObservableArray.js"; +import {UpdateAction} from "./UpdateAction.js"; export function tests() { class TestTile { diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 2e952a76..1c19c15f 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -14,10 +14,10 @@ the timeline (counted in tiles), which results to a range in sortKeys we want on to the room timeline, which unload entries from memory. when loading, it just reads events from a sortkey backwards or forwards... */ -import TilesCollection from "./TilesCollection.js"; -import tilesCreator from "./tilesCreator.js"; +import {TilesCollection} from "./TilesCollection.js"; +import {tilesCreator} from "./tilesCreator.js"; -export default class TimelineViewModel { +export class TimelineViewModel { constructor(room, timeline, ownUserId) { this._timeline = timeline; // once we support sending messages we could do diff --git a/src/domain/session/room/timeline/UpdateAction.js b/src/domain/session/room/timeline/UpdateAction.js index 1421cbd6..ed09560f 100644 --- a/src/domain/session/room/timeline/UpdateAction.js +++ b/src/domain/session/room/timeline/UpdateAction.js @@ -1,4 +1,4 @@ -export default class UpdateAction { +export class UpdateAction { constructor(remove, update, updateParams) { this._remove = remove; this._update = update; diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index 8e4fa0f4..b0aaca95 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -1,7 +1,7 @@ -import SimpleTile from "./SimpleTile.js"; -import UpdateAction from "../UpdateAction.js"; +import {SimpleTile} from "./SimpleTile.js"; +import {UpdateAction} from "../UpdateAction.js"; -export default class GapTile extends SimpleTile { +export class GapTile extends SimpleTile { constructor(options, timeline) { super(options); this._timeline = timeline; diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js index ebe8d022..8aee454b 100644 --- a/src/domain/session/room/timeline/tiles/ImageTile.js +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -1,6 +1,6 @@ -import MessageTile from "./MessageTile.js"; +import {MessageTile} from "./MessageTile.js"; -export default class ImageTile extends MessageTile { +export class ImageTile extends MessageTile { constructor(options) { super(options); diff --git a/src/domain/session/room/timeline/tiles/LocationTile.js b/src/domain/session/room/timeline/tiles/LocationTile.js index 69dbc629..4a176233 100644 --- a/src/domain/session/room/timeline/tiles/LocationTile.js +++ b/src/domain/session/room/timeline/tiles/LocationTile.js @@ -1,4 +1,4 @@ -import MessageTile from "./MessageTile.js"; +import {MessageTile} from "./MessageTile.js"; /* map urls: @@ -7,7 +7,7 @@ android: https://developers.google.com/maps/documentation/urls/guide wp: maps:49.275267 -122.988617 https://www.habaneroconsulting.com/stories/insights/2011/opening-native-map-apps-from-the-mobile-browser */ -export default class LocationTile extends MessageTile { +export class LocationTile extends MessageTile { get mapsLink() { const geoUri = this._getContent().geo_uri; const [lat, long] = geoUri.split(":")[1].split(","); diff --git a/src/domain/session/room/timeline/tiles/MessageTile.js b/src/domain/session/room/timeline/tiles/MessageTile.js index bb862047..0ca74128 100644 --- a/src/domain/session/room/timeline/tiles/MessageTile.js +++ b/src/domain/session/room/timeline/tiles/MessageTile.js @@ -1,6 +1,6 @@ -import SimpleTile from "./SimpleTile.js"; +import {SimpleTile} from "./SimpleTile.js"; -export default class MessageTile extends SimpleTile { +export class MessageTile extends SimpleTile { constructor(options) { super(options); diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js index f841a2ed..536cbeb5 100644 --- a/src/domain/session/room/timeline/tiles/RoomMemberTile.js +++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js @@ -1,6 +1,6 @@ -import SimpleTile from "./SimpleTile.js"; +import {SimpleTile} from "./SimpleTile.js"; -export default class RoomNameTile extends SimpleTile { +export class RoomMemberTile extends SimpleTile { get shape() { return "announcement"; diff --git a/src/domain/session/room/timeline/tiles/RoomNameTile.js b/src/domain/session/room/timeline/tiles/RoomNameTile.js index 36ad7934..d37255ae 100644 --- a/src/domain/session/room/timeline/tiles/RoomNameTile.js +++ b/src/domain/session/room/timeline/tiles/RoomNameTile.js @@ -1,6 +1,6 @@ -import SimpleTile from "./SimpleTile.js"; +import {SimpleTile} from "./SimpleTile.js"; -export default class RoomNameTile extends SimpleTile { +export class RoomNameTile extends SimpleTile { get shape() { return "announcement"; diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 7b1ed91f..da5ba575 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -1,6 +1,6 @@ -import UpdateAction from "../UpdateAction.js"; +import {UpdateAction} from "../UpdateAction.js"; -export default class SimpleTile { +export class SimpleTile { constructor({entry}) { this._entry = entry; this._emitUpdate = null; diff --git a/src/domain/session/room/timeline/tiles/TextTile.js b/src/domain/session/room/timeline/tiles/TextTile.js index 47680ef0..a6144b1b 100644 --- a/src/domain/session/room/timeline/tiles/TextTile.js +++ b/src/domain/session/room/timeline/tiles/TextTile.js @@ -1,6 +1,6 @@ -import MessageTile from "./MessageTile.js"; +import {MessageTile} from "./MessageTile.js"; -export default class TextTile extends MessageTile { +export class TextTile extends MessageTile { get text() { const content = this._getContent(); const body = content && content.body; diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js index 87d70238..9f53a378 100644 --- a/src/domain/session/room/timeline/tilesCreator.js +++ b/src/domain/session/room/timeline/tilesCreator.js @@ -1,10 +1,10 @@ -import GapTile from "./tiles/GapTile.js"; -import TextTile from "./tiles/TextTile.js"; -import LocationTile from "./tiles/LocationTile.js"; -import RoomNameTile from "./tiles/RoomNameTile.js"; -import RoomMemberTile from "./tiles/RoomMemberTile.js"; +import {GapTile} from "./tiles/GapTile.js"; +import {TextTile} from "./tiles/TextTile.js"; +import {LocationTile} from "./tiles/LocationTile.js"; +import {RoomNameTile} from "./tiles/RoomNameTile.js"; +import {RoomMemberTile} from "./tiles/RoomMemberTile.js"; -export default function ({room, ownUserId}) { +export function tilesCreator({room, ownUserId}) { return function tilesCreator(entry, emitUpdate) { const options = {entry, emitUpdate, ownUserId}; if (entry.isGap) { diff --git a/src/domain/session/roomlist/RoomTileViewModel.js b/src/domain/session/roomlist/RoomTileViewModel.js index 110b88b2..cd12242f 100644 --- a/src/domain/session/roomlist/RoomTileViewModel.js +++ b/src/domain/session/roomlist/RoomTileViewModel.js @@ -1,6 +1,6 @@ import {avatarInitials} from "../avatar.js"; -export default class RoomTileViewModel { +export class RoomTileViewModel { // we use callbacks to parent VM instead of emit because // it would be annoying to keep track of subscriptions in // parent for all RoomTileViewModels diff --git a/src/main.js b/src/main.js index ef1b8c9c..f85e2844 100644 --- a/src/main.js +++ b/src/main.js @@ -1,13 +1,13 @@ -import HomeServerApi from "./matrix/net/HomeServerApi.js"; +import {HomeServerApi} from "./matrix/net/HomeServerApi.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; -import fetchRequest from "./matrix/net/request/fetch.js"; +import {fetchRequest} from "./matrix/net/request/fetch.js"; import {Reconnector} from "./matrix/net/Reconnector.js"; -import StorageFactory from "./matrix/storage/idb/create.js"; -import SessionInfoStorage from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; -import BrawlViewModel from "./domain/BrawlViewModel.js"; -import BrawlView from "./ui/web/BrawlView.js"; -import DOMClock from "./ui/web/dom/Clock.js"; -import OnlineStatus from "./ui/web/dom/OnlineStatus.js"; +import {StorageFactory} from "./matrix/storage/idb/create.js"; +import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; +import {BrawlViewModel} from "./domain/BrawlViewModel.js"; +import {BrawlView} from "./ui/web/BrawlView.js"; +import {Clock as DOMClock} from "./ui/web/dom/Clock.js"; +import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js"; export default async function main(container) { try { diff --git a/src/matrix/SendScheduler.js b/src/matrix/SendScheduler.js index 1280ca6a..1b572cc2 100644 --- a/src/matrix/SendScheduler.js +++ b/src/matrix/SendScheduler.js @@ -1,4 +1,4 @@ -import Platform from "../Platform.js"; +import {Platform} from "../Platform.js"; import {HomeServerError, ConnectionError} from "./error.js"; export class RateLimitingBackoff { diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 7c4771e4..b0a21133 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1,9 +1,9 @@ -import Room from "./room/room.js"; +import {Room} from "./room/room.js"; import { ObservableMap } from "../observable/index.js"; import { SendScheduler, RateLimitingBackoff } from "./SendScheduler.js"; -import User from "./User.js"; +import {User} from "./User.js"; -export default class Session { +export class Session { // sessionInfo contains deviceId, userId and homeServer constructor({storage, hsApi, sessionInfo}) { this._storage = storage; diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index aad58959..b907cc7e 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -1,11 +1,11 @@ -import createEnum from "../utils/enum.js"; -import ObservableValue from "../observable/ObservableValue.js"; -import HomeServerApi from "./net/HomeServerApi.js"; +import {createEnum} from "../utils/enum.js"; +import {ObservableValue} from "../observable/ObservableValue.js"; +import {HomeServerApi} from "./net/HomeServerApi.js"; import {Reconnector, ConnectionStatus} from "./net/Reconnector.js"; -import ExponentialRetryDelay from "./net/ExponentialRetryDelay.js"; +import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay.js"; import {HomeServerError, ConnectionError, AbortError} from "./error.js"; import {Sync, SyncStatus} from "./Sync.js"; -import Session from "./Session.js"; +import {Session} from "./Session.js"; export const LoadStatus = createEnum( "NotLoading", @@ -138,7 +138,7 @@ export class SessionContainer { } }); await this._waitForFirstSync(); - + this._status.set(LoadStatus.Ready); // if the sync failed, and then the reconnector diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 8b79c6da..eeea0b66 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -1,6 +1,6 @@ import {AbortError} from "./error.js"; -import ObservableValue from "../observable/ObservableValue.js"; -import createEnum from "../utils/enum.js"; +import {ObservableValue} from "../observable/ObservableValue.js"; +import {createEnum} from "../utils/enum.js"; const INCREMENTAL_TIMEOUT = 30000; const SYNC_EVENT_LIMIT = 10; diff --git a/src/matrix/User.js b/src/matrix/User.js index 5c0aa37f..6db27e78 100644 --- a/src/matrix/User.js +++ b/src/matrix/User.js @@ -1,4 +1,4 @@ -export default class User { +export class User { constructor(userId) { this._userId = userId; } diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.js index 42ef364e..a1317619 100644 --- a/src/matrix/net/ExponentialRetryDelay.js +++ b/src/matrix/net/ExponentialRetryDelay.js @@ -1,6 +1,6 @@ import {AbortError} from "../../utils/error.js"; -export default class ExponentialRetryDelay { +export class ExponentialRetryDelay { constructor(createTimeout, start = 2000) { this._start = start; this._current = start; @@ -43,7 +43,7 @@ export default class ExponentialRetryDelay { } -import MockClock from "../../mocks/Clock.js"; +import {Clock as MockClock} from "../../mocks/Clock.js"; export function tests() { return { diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 53c4cca4..918987c5 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -28,7 +28,7 @@ class RequestWrapper { } } -export default class HomeServerApi { +export class HomeServerApi { constructor({homeServer, accessToken, request, createTimeout, reconnector}) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index 48af075e..aa1f80da 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -1,7 +1,7 @@ -import createEnum from "../../utils/enum.js"; +import {createEnum} from "../../utils/enum.js"; import {AbortError} from "../../utils/error.js"; import {ConnectionError} from "../error.js" -import ObservableValue from "../../observable/ObservableValue.js"; +import {ObservableValue} from "../../observable/ObservableValue.js"; export const ConnectionStatus = createEnum( "Offline", @@ -111,8 +111,8 @@ export class Reconnector { } -import MockClock from "../../mocks/Clock.js"; -import ExponentialRetryDelay from "./ExponentialRetryDelay.js"; +import {Clock as MockClock} from "../../mocks/Clock.js"; +import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js"; export function tests() { function createHsApiMock(remainingFailures) { diff --git a/src/matrix/net/request/fetch.js b/src/matrix/net/request/fetch.js index 1947d165..bc9ea8b7 100644 --- a/src/matrix/net/request/fetch.js +++ b/src/matrix/net/request/fetch.js @@ -31,7 +31,7 @@ class RequestResult { } } -export default function fetchRequest(url, options) { +export function fetchRequest(url, options) { const controller = typeof AbortController === "function" ? new AbortController() : null; if (controller) { options = Object.assign(options, { diff --git a/src/matrix/room/room.js b/src/matrix/room/room.js index 721eefe9..71cb88bd 100644 --- a/src/matrix/room/room.js +++ b/src/matrix/room/room.js @@ -1,12 +1,12 @@ -import EventEmitter from "../../EventEmitter.js"; -import RoomSummary from "./summary.js"; -import SyncWriter from "./timeline/persistence/SyncWriter.js"; -import GapWriter from "./timeline/persistence/GapWriter.js"; -import Timeline from "./timeline/Timeline.js"; -import FragmentIdComparer from "./timeline/FragmentIdComparer.js"; -import SendQueue from "./sending/SendQueue.js"; +import {EventEmitter} from "../../EventEmitter.js"; +import {RoomSummary} from "./summary.js"; +import {SyncWriter} from "./timeline/persistence/SyncWriter.js"; +import {GapWriter} from "./timeline/persistence/GapWriter.js"; +import {Timeline} from "./timeline/Timeline.js"; +import {FragmentIdComparer} from "./timeline/FragmentIdComparer.js"; +import {SendQueue} from "./sending/SendQueue.js"; -export default class Room extends EventEmitter { +export class Room extends EventEmitter { constructor({roomId, storage, hsApi, emitCollectionChange, sendScheduler, pendingEvents, user}) { super(); this._roomId = roomId; diff --git a/src/matrix/room/sending/PendingEvent.js b/src/matrix/room/sending/PendingEvent.js index 105da49b..a87efc98 100644 --- a/src/matrix/room/sending/PendingEvent.js +++ b/src/matrix/room/sending/PendingEvent.js @@ -1,4 +1,4 @@ -export default class PendingEvent { +export class PendingEvent { constructor(data) { this._data = data; } diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index caea41fa..958901e4 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -1,6 +1,6 @@ -import SortedArray from "../../../observable/list/SortedArray.js"; +import {SortedArray} from "../../../observable/list/SortedArray.js"; import {ConnectionError} from "../../error.js"; -import PendingEvent from "./PendingEvent.js"; +import {PendingEvent} from "./PendingEvent.js"; function makeTxnId() { const n = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); @@ -8,7 +8,7 @@ function makeTxnId() { return "t" + "0".repeat(14 - str.length) + str; } -export default class SendQueue { +export class SendQueue { constructor({roomId, storage, sendScheduler, pendingEvents}) { pendingEvents = pendingEvents || []; this._roomId = roomId; diff --git a/src/matrix/room/summary.js b/src/matrix/room/summary.js index b29ee9bd..177c1bc4 100644 --- a/src/matrix/room/summary.js +++ b/src/matrix/room/summary.js @@ -99,7 +99,7 @@ class SummaryData { } } -export default class RoomSummary { +export class RoomSummary { constructor(roomId) { this._data = new SummaryData(null, roomId); } diff --git a/src/matrix/room/timeline/Direction.js b/src/matrix/room/timeline/Direction.js index 9c6fa8cd..05fcc0dc 100644 --- a/src/matrix/room/timeline/Direction.js +++ b/src/matrix/room/timeline/Direction.js @@ -1,6 +1,4 @@ - - -export default class Direction { +export class Direction { constructor(isForward) { this._isForward = isForward; } diff --git a/src/matrix/room/timeline/EventKey.js b/src/matrix/room/timeline/EventKey.js index e7837cbd..5f34a66d 100644 --- a/src/matrix/room/timeline/EventKey.js +++ b/src/matrix/room/timeline/EventKey.js @@ -1,7 +1,7 @@ -import Platform from "../../../Platform.js"; +import {Platform} from "../../../Platform.js"; // key for events in the timelineEvents store -export default class EventKey { +export class EventKey { constructor(fragmentId, eventIndex) { this.fragmentId = fragmentId; this.eventIndex = eventIndex; diff --git a/src/matrix/room/timeline/FragmentIdComparer.js b/src/matrix/room/timeline/FragmentIdComparer.js index d1a11564..24073dbe 100644 --- a/src/matrix/room/timeline/FragmentIdComparer.js +++ b/src/matrix/room/timeline/FragmentIdComparer.js @@ -114,7 +114,7 @@ class Island { /* index for fast lookup of how two fragments can be sorted */ -export default class FragmentIdComparer { +export class FragmentIdComparer { constructor(fragments) { this._fragmentsById = fragments.reduce((map, f) => {map.set(f.id, f); return map;}, new Map()); this.rebuild(fragments); diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index a13fa304..c6321876 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -1,9 +1,9 @@ -import { SortedArray, MappedList, ConcatList } from "../../../observable/index.js"; -import Direction from "./Direction.js"; -import TimelineReader from "./persistence/TimelineReader.js"; -import PendingEventEntry from "./entries/PendingEventEntry.js"; +import {SortedArray, MappedList, ConcatList} from "../../../observable/index.js"; +import {Direction} from "./Direction.js"; +import {TimelineReader} from "./persistence/TimelineReader.js"; +import {PendingEventEntry} from "./entries/PendingEventEntry.js"; -export default class Timeline { +export class Timeline { constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, user}) { this._roomId = roomId; this._storage = storage; diff --git a/src/matrix/room/timeline/entries/BaseEntry.js b/src/matrix/room/timeline/entries/BaseEntry.js index 6c55788c..bd129cf2 100644 --- a/src/matrix/room/timeline/entries/BaseEntry.js +++ b/src/matrix/room/timeline/entries/BaseEntry.js @@ -1,8 +1,8 @@ //entries can be sorted, first by fragment, then by entry index. -import EventKey from "../EventKey.js"; +import {EventKey} from "../EventKey.js"; export const PENDING_FRAGMENT_ID = Number.MAX_SAFE_INTEGER; -export default class BaseEntry { +export class BaseEntry { constructor(fragmentIdComparer) { this._fragmentIdComparer = fragmentIdComparer; } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 9156d466..041c392c 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -1,6 +1,6 @@ -import BaseEntry from "./BaseEntry.js"; +import {BaseEntry} from "./BaseEntry.js"; -export default class EventEntry extends BaseEntry { +export class EventEntry extends BaseEntry { constructor(eventEntry, fragmentIdComparer) { super(fragmentIdComparer); this._eventEntry = eventEntry; diff --git a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js index 4dc0ab61..c84ddede 100644 --- a/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js +++ b/src/matrix/room/timeline/entries/FragmentBoundaryEntry.js @@ -1,9 +1,9 @@ -import BaseEntry from "./BaseEntry.js"; -import Direction from "../Direction.js"; +import {BaseEntry} from "./BaseEntry.js"; +import {Direction} from "../Direction.js"; import {isValidFragmentId} from "../common.js"; -import Platform from "../../../../Platform.js"; +import {Platform} from "../../../../Platform.js"; -export default class FragmentBoundaryEntry extends BaseEntry { +export class FragmentBoundaryEntry extends BaseEntry { constructor(fragment, isFragmentStart, fragmentIdComparer) { super(fragmentIdComparer); this._fragment = fragment; diff --git a/src/matrix/room/timeline/entries/PendingEventEntry.js b/src/matrix/room/timeline/entries/PendingEventEntry.js index 63e8ba84..0d2c9ae1 100644 --- a/src/matrix/room/timeline/entries/PendingEventEntry.js +++ b/src/matrix/room/timeline/entries/PendingEventEntry.js @@ -1,6 +1,6 @@ -import BaseEntry, {PENDING_FRAGMENT_ID} from "./BaseEntry.js"; +import {BaseEntry, PENDING_FRAGMENT_ID} from "./BaseEntry.js"; -export default class PendingEventEntry extends BaseEntry { +export class PendingEventEntry extends BaseEntry { constructor({pendingEvent, user}) { super(null); this._pendingEvent = pendingEvent; diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index 36080270..f6939fae 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -1,8 +1,8 @@ -import EventKey from "../EventKey.js"; -import EventEntry from "../entries/EventEntry.js"; +import {EventKey} from "../EventKey.js"; +import {EventEntry} from "../entries/EventEntry.js"; import {createEventEntry, directionalAppend} from "./common.js"; -export default class GapWriter { +export class GapWriter { constructor({roomId, storage, fragmentIdComparer}) { this._roomId = roomId; this._storage = storage; diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 8143ced0..103bb31f 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -1,6 +1,6 @@ -import EventKey from "../EventKey.js"; -import EventEntry from "../entries/EventEntry.js"; -import FragmentBoundaryEntry from "../entries/FragmentBoundaryEntry.js"; +import {EventKey} from "../EventKey.js"; +import {EventEntry} from "../entries/EventEntry.js"; +import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; import {createEventEntry} from "./common.js"; // Synapse bug? where the m.room.create event appears twice in sync response @@ -17,7 +17,7 @@ function deduplicateEvents(events) { }); } -export default class SyncWriter { +export class SyncWriter { constructor({roomId, fragmentIdComparer}) { this._roomId = roomId; this._fragmentIdComparer = fragmentIdComparer; diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index 01c9f693..3262550a 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -1,9 +1,9 @@ import {directionalConcat, directionalAppend} from "./common.js"; -import Direction from "../Direction.js"; -import EventEntry from "../entries/EventEntry.js"; -import FragmentBoundaryEntry from "../entries/FragmentBoundaryEntry.js"; +import {Direction} from "../Direction.js"; +import {EventEntry} from "../entries/EventEntry.js"; +import {FragmentBoundaryEntry} from "../entries/FragmentBoundaryEntry.js"; -export default class TimelineReader { +export class TimelineReader { constructor({roomId, storage, fragmentIdComparer}) { this._roomId = roomId; this._storage = storage; diff --git a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js index abd9ffb2..29c4be94 100644 --- a/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js +++ b/src/matrix/sessioninfo/localstorage/SessionInfoStorage.js @@ -1,4 +1,4 @@ -export default class SessionInfoStorage { +export class SessionInfoStorage { constructor(name) { this._name = name; } diff --git a/src/matrix/storage/idb/create.js b/src/matrix/storage/idb/create.js index 6056409f..dc56dfcd 100644 --- a/src/matrix/storage/idb/create.js +++ b/src/matrix/storage/idb/create.js @@ -1,11 +1,11 @@ -import Storage from "./storage.js"; +import {Storage} from "./storage.js"; import { openDatabase, reqAsPromise } from "./utils.js"; import { exportSession, importSession } from "./export.js"; const sessionName = sessionId => `brawl_session_${sessionId}`; const openDatabaseWithSessionId = sessionId => openDatabase(sessionName(sessionId), createStores, 1); -export default class StorageFactory { +export class StorageFactory { async create(sessionId) { const db = await openDatabaseWithSessionId(sessionId); return new Storage(db); diff --git a/src/matrix/storage/idb/query-target.js b/src/matrix/storage/idb/query-target.js index 7e848218..1948b685 100644 --- a/src/matrix/storage/idb/query-target.js +++ b/src/matrix/storage/idb/query-target.js @@ -1,6 +1,6 @@ import {iterateCursor, reqAsPromise} from "./utils.js"; -export default class QueryTarget { +export class QueryTarget { constructor(target) { this._target = target; } diff --git a/src/matrix/storage/idb/storage.js b/src/matrix/storage/idb/storage.js index 5466ab76..ddf0f73a 100644 --- a/src/matrix/storage/idb/storage.js +++ b/src/matrix/storage/idb/storage.js @@ -1,7 +1,7 @@ -import Transaction from "./transaction.js"; +import {Transaction} from "./transaction.js"; import { STORE_NAMES, StorageError } from "../common.js"; -export default class Storage { +export class Storage { constructor(idbDatabase) { this._db = idbDatabase; const nameMap = STORE_NAMES.reduce((nameMap, name) => { diff --git a/src/matrix/storage/idb/store.js b/src/matrix/storage/idb/store.js index f2169fe3..a3eed7ef 100644 --- a/src/matrix/storage/idb/store.js +++ b/src/matrix/storage/idb/store.js @@ -1,4 +1,4 @@ -import QueryTarget from "./query-target.js"; +import {QueryTarget} from "./query-target.js"; import { reqAsPromise } from "./utils.js"; import { StorageError } from "../common.js"; @@ -80,7 +80,7 @@ class QueryTargetWrapper { } } -export default class Store extends QueryTarget { +export class Store extends QueryTarget { constructor(idbStore) { super(new QueryTargetWrapper(idbStore)); } diff --git a/src/matrix/storage/idb/stores/PendingEventStore.js b/src/matrix/storage/idb/stores/PendingEventStore.js index d413ec63..7aa5408a 100644 --- a/src/matrix/storage/idb/stores/PendingEventStore.js +++ b/src/matrix/storage/idb/stores/PendingEventStore.js @@ -1,5 +1,5 @@ import { encodeUint32, decodeUint32 } from "../utils.js"; -import Platform from "../../../../Platform.js"; +import {Platform} from "../../../../Platform.js"; function encodeKey(roomId, queueIndex) { return `${roomId}|${encodeUint32(queueIndex)}`; @@ -11,7 +11,7 @@ function decodeKey(key) { return {roomId, queueIndex}; } -export default class PendingEventStore { +export class PendingEventStore { constructor(eventStore) { this._eventStore = eventStore; } diff --git a/src/matrix/storage/idb/stores/RoomStateStore.js b/src/matrix/storage/idb/stores/RoomStateStore.js index 09f3cd6d..21c17c5a 100644 --- a/src/matrix/storage/idb/stores/RoomStateStore.js +++ b/src/matrix/storage/idb/stores/RoomStateStore.js @@ -1,4 +1,4 @@ -export default class RoomStateStore { +export class RoomStateStore { constructor(idbStore) { this._roomStateStore = idbStore; } diff --git a/src/matrix/storage/idb/stores/RoomSummaryStore.js b/src/matrix/storage/idb/stores/RoomSummaryStore.js index 5d4c99ec..45cf8468 100644 --- a/src/matrix/storage/idb/stores/RoomSummaryStore.js +++ b/src/matrix/storage/idb/stores/RoomSummaryStore.js @@ -11,7 +11,7 @@ store contains: inviteCount joinCount */ -export default class RoomSummaryStore { +export class RoomSummaryStore { constructor(summaryStore) { this._summaryStore = summaryStore; } diff --git a/src/matrix/storage/idb/stores/SessionStore.js b/src/matrix/storage/idb/stores/SessionStore.js index 405f9794..72af611d 100644 --- a/src/matrix/storage/idb/stores/SessionStore.js +++ b/src/matrix/storage/idb/stores/SessionStore.js @@ -14,7 +14,7 @@ store contains: avatarUrl lastSynced */ -export default class SessionStore { +export class SessionStore { constructor(sessionStore) { this._sessionStore = sessionStore; } diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.js b/src/matrix/storage/idb/stores/TimelineEventStore.js index f54fc758..7ccb0883 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.js +++ b/src/matrix/storage/idb/stores/TimelineEventStore.js @@ -1,7 +1,7 @@ -import EventKey from "../../../room/timeline/EventKey.js"; +import {EventKey} from "../../../room/timeline/EventKey.js"; import { StorageError } from "../../common.js"; import { encodeUint32 } from "../utils.js"; -import Platform from "../../../../Platform.js"; +import {Platform} from "../../../../Platform.js"; function encodeKey(roomId, fragmentId, eventIndex) { return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`; @@ -81,7 +81,7 @@ class Range { * @property {?Event} event if an event entry, the event * @property {?Gap} gap if a gap entry, the gap */ -export default class TimelineEventStore { +export class TimelineEventStore { constructor(timelineStore) { this._timelineStore = timelineStore; } diff --git a/src/matrix/storage/idb/stores/TimelineFragmentStore.js b/src/matrix/storage/idb/stores/TimelineFragmentStore.js index 064daed7..4dc33c13 100644 --- a/src/matrix/storage/idb/stores/TimelineFragmentStore.js +++ b/src/matrix/storage/idb/stores/TimelineFragmentStore.js @@ -1,12 +1,12 @@ import { StorageError } from "../../common.js"; -import Platform from "../../../../Platform.js"; +import {Platform} from "../../../../Platform.js"; import { encodeUint32 } from "../utils.js"; function encodeKey(roomId, fragmentId) { return `${roomId}|${encodeUint32(fragmentId)}`; } -export default class RoomFragmentStore { +export class TimelineFragmentStore { constructor(store) { this._store = store; } diff --git a/src/matrix/storage/idb/transaction.js b/src/matrix/storage/idb/transaction.js index 1c2c8286..916f9bc5 100644 --- a/src/matrix/storage/idb/transaction.js +++ b/src/matrix/storage/idb/transaction.js @@ -1,14 +1,14 @@ import {txnAsPromise} from "./utils.js"; import {StorageError} from "../common.js"; -import Store from "./store.js"; -import SessionStore from "./stores/SessionStore.js"; -import RoomSummaryStore from "./stores/RoomSummaryStore.js"; -import TimelineEventStore from "./stores/TimelineEventStore.js"; -import RoomStateStore from "./stores/RoomStateStore.js"; -import TimelineFragmentStore from "./stores/TimelineFragmentStore.js"; -import PendingEventStore from "./stores/PendingEventStore.js"; +import {Store} from "./store.js"; +import {SessionStore} from "./stores/SessionStore.js"; +import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; +import {TimelineEventStore} from "./stores/TimelineEventStore.js"; +import {RoomStateStore} from "./stores/RoomStateStore.js"; +import {TimelineFragmentStore} from "./stores/TimelineFragmentStore.js"; +import {PendingEventStore} from "./stores/PendingEventStore.js"; -export default class Transaction { +export class Transaction { constructor(txn, allowedStoreNames) { this._txn = txn; this._allowedStoreNames = allowedStoreNames; diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js index fb178b01..3c5294a9 100644 --- a/src/matrix/storage/memory/Storage.js +++ b/src/matrix/storage/memory/Storage.js @@ -1,7 +1,7 @@ -import Transaction from "./transaction.js"; +import {Transaction} from "./transaction.js"; import { STORE_MAP, STORE_NAMES } from "../common.js"; -export default class Storage { +export class Storage { constructor(initialStoreValues = {}) { this._validateStoreNames(Object.keys(initialStoreValues)); this.storeNames = STORE_MAP; diff --git a/src/matrix/storage/memory/Transaction.js b/src/matrix/storage/memory/Transaction.js index 437962da..b37a53fc 100644 --- a/src/matrix/storage/memory/Transaction.js +++ b/src/matrix/storage/memory/Transaction.js @@ -1,6 +1,6 @@ -import RoomTimelineStore from "./stores/RoomTimelineStore.js"; +import {RoomTimelineStore} from "./stores/RoomTimelineStore.js"; -export default class Transaction { +export class Transaction { constructor(storeValues, writable) { this._storeValues = storeValues; this._txnStoreValues = {}; diff --git a/src/matrix/storage/memory/stores/RoomTimelineStore.js b/src/matrix/storage/memory/stores/RoomTimelineStore.js index 3c17c045..6152daa7 100644 --- a/src/matrix/storage/memory/stores/RoomTimelineStore.js +++ b/src/matrix/storage/memory/stores/RoomTimelineStore.js @@ -1,6 +1,6 @@ -import SortKey from "../../room/timeline/SortKey.js"; -import sortedIndex from "../../../utils/sortedIndex.js"; -import Store from "./Store.js"; +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) { @@ -65,7 +65,7 @@ class Range { } } -export default class RoomTimelineStore extends Store { +export class RoomTimelineStore extends Store { constructor(timeline, writable) { super(timeline || [], writable); } diff --git a/src/matrix/storage/memory/stores/Store.js b/src/matrix/storage/memory/stores/Store.js index 8028a783..c04ac258 100644 --- a/src/matrix/storage/memory/stores/Store.js +++ b/src/matrix/storage/memory/stores/Store.js @@ -1,4 +1,4 @@ -export default class Store { +export class Store { constructor(storeValue, writable) { this._storeValue = storeValue; this._writable = writable; diff --git a/src/mocks/Clock.js b/src/mocks/Clock.js index 0a08bf58..fd49b313 100644 --- a/src/mocks/Clock.js +++ b/src/mocks/Clock.js @@ -1,4 +1,4 @@ -import ObservableValue from "../observable/ObservableValue.js"; +import {ObservableValue} from "../observable/ObservableValue.js"; class Timeout { constructor(elapsed, ms) { @@ -29,7 +29,7 @@ class TimeMeasure { } } -export default class Clock { +export class Clock { constructor(baseTimestamp = 0) { this._baseTimestamp = baseTimestamp; this._elapsed = new ObservableValue(0); diff --git a/src/observable/BaseObservable.js b/src/observable/BaseObservable.js index 6ef46ab6..75d8023f 100644 --- a/src/observable/BaseObservable.js +++ b/src/observable/BaseObservable.js @@ -1,4 +1,4 @@ -export default class BaseObservable { +export class BaseObservable { constructor() { this._handlers = new Set(); } diff --git a/src/observable/ObservableValue.js b/src/observable/ObservableValue.js index 94f2d188..b9fa4c4d 100644 --- a/src/observable/ObservableValue.js +++ b/src/observable/ObservableValue.js @@ -1,5 +1,5 @@ import {AbortError} from "../utils/error.js"; -import BaseObservable from "./BaseObservable.js"; +import {BaseObservable} from "./BaseObservable.js"; // like an EventEmitter, but doesn't have an event type export class BaseObservableValue extends BaseObservable { @@ -49,7 +49,7 @@ class ResolvedWaitForHandle { dispose() {} } -export default class ObservableValue extends BaseObservableValue { +export class ObservableValue extends BaseObservableValue { constructor(initialValue) { super(); this._value = initialValue; diff --git a/src/observable/index.js b/src/observable/index.js index 5444e27e..2497a7db 100644 --- a/src/observable/index.js +++ b/src/observable/index.js @@ -1,13 +1,13 @@ -import SortedMapList from "./list/SortedMapList.js"; -import FilteredMap from "./map/FilteredMap.js"; -import MappedMap from "./map/MappedMap.js"; -import BaseObservableMap from "./map/BaseObservableMap.js"; +import {SortedMapList} from "./list/SortedMapList.js"; +import {FilteredMap} from "./map/FilteredMap.js"; +import {MappedMap} from "./map/MappedMap.js"; +import {BaseObservableMap} from "./map/BaseObservableMap.js"; // re-export "root" (of chain) collections -export { default as ObservableArray } from "./list/ObservableArray.js"; -export { default as SortedArray } from "./list/SortedArray.js"; -export { default as MappedList } from "./list/MappedList.js"; -export { default as ConcatList } from "./list/ConcatList.js"; -export { default as ObservableMap } from "./map/ObservableMap.js"; +export { ObservableArray } from "./list/ObservableArray.js"; +export { SortedArray } from "./list/SortedArray.js"; +export { MappedList } from "./list/MappedList.js"; +export { ConcatList } from "./list/ConcatList.js"; +export { ObservableMap } from "./map/ObservableMap.js"; // avoid circular dependency between these classes // and BaseObservableMap (as they extend it) diff --git a/src/observable/list/BaseObservableList.js b/src/observable/list/BaseObservableList.js index 4c7e3491..4f15d02a 100644 --- a/src/observable/list/BaseObservableList.js +++ b/src/observable/list/BaseObservableList.js @@ -1,6 +1,6 @@ -import BaseObservable from "../BaseObservable.js"; +import {BaseObservable} from "../BaseObservable.js"; -export default class BaseObservableList extends BaseObservable { +export class BaseObservableList extends BaseObservable { emitReset() { for(let h of this._handlers) { h.onReset(this); diff --git a/src/observable/list/ConcatList.js b/src/observable/list/ConcatList.js index 6177f6f3..987dbb80 100644 --- a/src/observable/list/ConcatList.js +++ b/src/observable/list/ConcatList.js @@ -1,6 +1,6 @@ -import BaseObservableList from "./BaseObservableList.js"; +import {BaseObservableList} from "./BaseObservableList.js"; -export default class ConcatList extends BaseObservableList { +export class ConcatList extends BaseObservableList { constructor(...sourceLists) { super(); this._sourceLists = sourceLists; @@ -86,7 +86,7 @@ export default class ConcatList extends BaseObservableList { } } -import ObservableArray from "./ObservableArray.js"; +import {ObservableArray} from "./ObservableArray.js"; export async function tests() { return { test_length(assert) { diff --git a/src/observable/list/MappedList.js b/src/observable/list/MappedList.js index a2adcdbd..55b8bd30 100644 --- a/src/observable/list/MappedList.js +++ b/src/observable/list/MappedList.js @@ -1,6 +1,6 @@ -import BaseObservableList from "./BaseObservableList.js"; +import {BaseObservableList} from "./BaseObservableList.js"; -export default class MappedList extends BaseObservableList { +export class MappedList extends BaseObservableList { constructor(sourceList, mapper, updater) { super(); this._sourceList = sourceList; diff --git a/src/observable/list/ObservableArray.js b/src/observable/list/ObservableArray.js index 47b0e24a..afbc0144 100644 --- a/src/observable/list/ObservableArray.js +++ b/src/observable/list/ObservableArray.js @@ -1,6 +1,6 @@ -import BaseObservableList from "./BaseObservableList.js"; +import {BaseObservableList} from "./BaseObservableList.js"; -export default class ObservableArray extends BaseObservableList { +export class ObservableArray extends BaseObservableList { constructor(initialValues = []) { super(); this._items = initialValues; diff --git a/src/observable/list/SortedArray.js b/src/observable/list/SortedArray.js index 6b34afdf..5bb89297 100644 --- a/src/observable/list/SortedArray.js +++ b/src/observable/list/SortedArray.js @@ -1,7 +1,7 @@ -import BaseObservableList from "./BaseObservableList.js"; -import sortedIndex from "../../utils/sortedIndex.js"; +import {BaseObservableList} from "./BaseObservableList.js"; +import {sortedIndex} from "../../utils/sortedIndex.js"; -export default class SortedArray extends BaseObservableList { +export class SortedArray extends BaseObservableList { constructor(comparator) { super(); this._comparator = comparator; diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index ebd7e86a..539bb65f 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -1,5 +1,5 @@ -import BaseObservableList from "./BaseObservableList.js"; -import sortedIndex from "../../utils/sortedIndex.js"; +import {BaseObservableList} from "./BaseObservableList.js"; +import {sortedIndex} from "../../utils/sortedIndex.js"; /* @@ -29,7 +29,7 @@ with a node containing {value, leftCount, rightCount, leftNode, rightNode, paren // types modified outside of the collection (and affecting sort order) or not // no duplicates allowed for now -export default class SortedMapList extends BaseObservableList { +export class SortedMapList extends BaseObservableList { constructor(sourceMap, comparator) { super(); this._sourceMap = sourceMap; @@ -114,7 +114,7 @@ export default class SortedMapList extends BaseObservableList { } //#ifdef TESTS -import ObservableMap from "../map/ObservableMap.js"; +import {ObservableMap} from "../map/ObservableMap.js"; export function tests() { return { diff --git a/src/observable/map/BaseObservableMap.js b/src/observable/map/BaseObservableMap.js index c2f1b974..c825af8e 100644 --- a/src/observable/map/BaseObservableMap.js +++ b/src/observable/map/BaseObservableMap.js @@ -1,6 +1,6 @@ -import BaseObservable from "../BaseObservable.js"; +import {BaseObservable} from "../BaseObservable.js"; -export default class BaseObservableMap extends BaseObservable { +export class BaseObservableMap extends BaseObservable { emitReset() { for(let h of this._handlers) { h.onReset(); diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.js index b48008b8..17500ecc 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.js @@ -1,6 +1,6 @@ -import BaseObservableMap from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap.js"; -export default class FilteredMap extends BaseObservableMap { +export class FilteredMap extends BaseObservableMap { constructor(source, mapper, updater) { super(); this._source = source; diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js index 4b16373b..14f46c44 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.js @@ -1,9 +1,9 @@ -import BaseObservableMap from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap.js"; /* so a mapped value can emit updates on it's own with this._updater that is passed in the mapping function how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ -export default class MappedMap extends BaseObservableMap { +export class MappedMap extends BaseObservableMap { constructor(source, mapper) { super(); this._source = source; diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index cd864ef4..cbdd7b3c 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -1,6 +1,6 @@ -import BaseObservableMap from "./BaseObservableMap.js"; +import {BaseObservableMap} from "./BaseObservableMap.js"; -export default class ObservableMap extends BaseObservableMap { +export class ObservableMap extends BaseObservableMap { constructor(initialValues) { super(); this._values = new Map(initialValues); diff --git a/src/ui/web/BrawlView.js b/src/ui/web/BrawlView.js index c5642e15..f4325a77 100644 --- a/src/ui/web/BrawlView.js +++ b/src/ui/web/BrawlView.js @@ -1,10 +1,10 @@ -import SessionView from "./session/SessionView.js"; -import LoginView from "./login/LoginView.js"; -import SessionPickerView from "./login/SessionPickerView.js"; -import TemplateView from "./general/TemplateView.js"; -import SwitchView from "./general/SwitchView.js"; +import {SessionView} from "./session/SessionView.js"; +import {LoginView} from "./login/LoginView.js"; +import {SessionPickerView} from "./login/SessionPickerView.js"; +import {TemplateView} from "./general/TemplateView.js"; +import {SwitchView} from "./general/SwitchView.js"; -export default class BrawlView { +export class BrawlView { constructor(vm) { this._vm = vm; this._switcher = null; diff --git a/src/ui/web/WebPlatform.js b/src/ui/web/WebPlatform.js index 4f3d9e06..453e36e6 100644 --- a/src/ui/web/WebPlatform.js +++ b/src/ui/web/WebPlatform.js @@ -1,4 +1,4 @@ -export default { +export const WebPlatform = { get minStorageKey() { // for indexeddb, we use unsigned 32 bit integers as keys return 0; diff --git a/src/ui/web/dom/Clock.js b/src/ui/web/dom/Clock.js index cc36c813..8a10499a 100644 --- a/src/ui/web/dom/Clock.js +++ b/src/ui/web/dom/Clock.js @@ -37,7 +37,7 @@ class TimeMeasure { } } -export default class Clock { +export class Clock { createMeasure() { return new TimeMeasure(); } diff --git a/src/ui/web/dom/OnlineStatus.js b/src/ui/web/dom/OnlineStatus.js index dc17a2ba..1cd3a9b5 100644 --- a/src/ui/web/dom/OnlineStatus.js +++ b/src/ui/web/dom/OnlineStatus.js @@ -1,6 +1,6 @@ import {BaseObservableValue} from "../../../observable/ObservableValue.js"; -export default class OnlineStatus extends BaseObservableValue { +export class OnlineStatus extends BaseObservableValue { constructor() { super(); this._onOffline = this._onOffline.bind(this); diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js index 4bd14f7c..cc01191b 100644 --- a/src/ui/web/general/ListView.js +++ b/src/ui/web/general/ListView.js @@ -10,7 +10,7 @@ function insertAt(parentNode, idx, childNode) { } } -export default class ListView { +export class ListView { constructor({list, onItemClick, className}, childCreator) { this._onItemClick = onItemClick; this._list = list; diff --git a/src/ui/web/general/SwitchView.js b/src/ui/web/general/SwitchView.js index 3b40c2bb..80ee7198 100644 --- a/src/ui/web/general/SwitchView.js +++ b/src/ui/web/general/SwitchView.js @@ -1,4 +1,4 @@ -export default class SwitchView { +export class SwitchView { constructor(defaultView) { this._childView = defaultView; } diff --git a/src/ui/web/general/Template.js b/src/ui/web/general/Template.js index b8c566c2..43891951 100644 --- a/src/ui/web/general/Template.js +++ b/src/ui/web/general/Template.js @@ -22,7 +22,7 @@ function objHasFns(obj) { missing: - create views */ -export default class Template { +export class Template { constructor(value, render) { this._value = value; this._eventListeners = null; diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index f6be9292..9afa1efe 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -1,6 +1,6 @@ -import Template from "./Template.js"; +import {Template} from "./Template.js"; -export default class TemplateView { +export class TemplateView { constructor(vm, bindToChangeEvent) { this.viewModel = vm; this._changeEventHandler = bindToChangeEvent ? this.update.bind(this, this.viewModel) : null; diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index e5b97da6..9eccc062 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -1,7 +1,7 @@ -import TemplateView from "../general/TemplateView.js"; +import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; -export default class LoginView extends TemplateView { +export class LoginView extends TemplateView { constructor(vm) { super(vm, true); } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 1dca2624..ed335033 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -1,5 +1,5 @@ -import ListView from "../general/ListView.js"; -import TemplateView from "../general/TemplateView.js"; +import {ListView} from "../general/ListView.js"; +import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; function selectFileAsText(mimeType) { @@ -71,7 +71,7 @@ class SessionPickerItemView extends TemplateView { } } -export default class SessionPickerView extends TemplateView { +export class SessionPickerView extends TemplateView { mount() { this._sessionList = new ListView({ list: this.viewModel.sessions, diff --git a/src/ui/web/session/RoomPlaceholderView.js b/src/ui/web/session/RoomPlaceholderView.js index d4cb7e0e..3a8f3e27 100644 --- a/src/ui/web/session/RoomPlaceholderView.js +++ b/src/ui/web/session/RoomPlaceholderView.js @@ -1,6 +1,6 @@ import {tag} from "../general/html.js"; -export default class RoomPlaceholderView { +export class RoomPlaceholderView { constructor() { this._root = null; } diff --git a/src/ui/web/session/RoomTile.js b/src/ui/web/session/RoomTile.js index 59c76cd8..38210003 100644 --- a/src/ui/web/session/RoomTile.js +++ b/src/ui/web/session/RoomTile.js @@ -1,6 +1,6 @@ -import TemplateView from "../general/TemplateView.js"; +import {TemplateView} from "../general/TemplateView.js"; -export default class RoomTile extends TemplateView { +export class RoomTile extends TemplateView { render(t) { return t.li([ t.div({className: "avatar medium"}, vm => vm.avatarInitials), diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js index 9b97ac19..19456fd0 100644 --- a/src/ui/web/session/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -1,12 +1,12 @@ -import ListView from "../general/ListView.js"; -import RoomTile from "./RoomTile.js"; -import RoomView from "./room/RoomView.js"; -import SwitchView from "../general/SwitchView.js"; -import RoomPlaceholderView from "./RoomPlaceholderView.js"; -import SyncStatusBar from "./SyncStatusBar.js"; +import {ListView} from "../general/ListView.js"; +import {RoomTile} from "./RoomTile.js"; +import {RoomView} from "./room/RoomView.js"; +import {SwitchView} from "../general/SwitchView.js"; +import {RoomPlaceholderView} from "./RoomPlaceholderView.js"; +import {SyncStatusBar} from "./SyncStatusBar.js"; import {tag} from "../general/html.js"; -export default class SessionView { +export class SessionView { constructor(viewModel) { this._viewModel = viewModel; this._middleSwitcher = null; diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js index 792aaa24..e8d4e95c 100644 --- a/src/ui/web/session/SyncStatusBar.js +++ b/src/ui/web/session/SyncStatusBar.js @@ -1,6 +1,6 @@ -import TemplateView from "../general/TemplateView.js"; +import {TemplateView} from "../general/TemplateView.js"; -export default class SyncStatusBar extends TemplateView { +export class SyncStatusBar extends TemplateView { constructor(vm) { super(vm, true); } diff --git a/src/ui/web/session/room/MessageComposer.js b/src/ui/web/session/room/MessageComposer.js index 79e2fd5e..664b246c 100644 --- a/src/ui/web/session/room/MessageComposer.js +++ b/src/ui/web/session/room/MessageComposer.js @@ -1,6 +1,6 @@ -import TemplateView from "../../general/TemplateView.js"; +import {TemplateView} from "../../general/TemplateView.js"; -export default class MessageComposer extends TemplateView { +export class MessageComposer extends TemplateView { constructor(viewModel) { super(viewModel); this._input = null; diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js index f431c16c..d3d0d849 100644 --- a/src/ui/web/session/room/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -1,8 +1,8 @@ -import TemplateView from "../../general/TemplateView.js"; -import TimelineList from "./TimelineList.js"; -import MessageComposer from "./MessageComposer.js"; +import {TemplateView} from "../../general/TemplateView.js"; +import {TimelineList} from "./TimelineList.js"; +import {MessageComposer} from "./MessageComposer.js"; -export default class RoomView extends TemplateView { +export class RoomView extends TemplateView { constructor(viewModel) { super(viewModel, true); this._timelineList = null; diff --git a/src/ui/web/session/room/TimelineList.js b/src/ui/web/session/room/TimelineList.js index ab5eee0d..8d042212 100644 --- a/src/ui/web/session/room/TimelineList.js +++ b/src/ui/web/session/room/TimelineList.js @@ -1,9 +1,9 @@ -import ListView from "../../general/ListView.js"; -import GapView from "./timeline/GapView.js"; -import TextMessageView from "./timeline/TextMessageView.js"; -import AnnouncementView from "./timeline/AnnouncementView.js"; +import {ListView} from "../../general/ListView.js"; +import {GapView} from "./timeline/GapView.js"; +import {TextMessageView} from "./timeline/TextMessageView.js"; +import {AnnouncementView} from "./timeline/AnnouncementView.js"; -export default class TimelineList extends ListView { +export class TimelineList extends ListView { constructor(options = {}) { options.className = "Timeline"; super(options, entry => { diff --git a/src/ui/web/session/room/timeline/AnnouncementView.js b/src/ui/web/session/room/timeline/AnnouncementView.js index fff0d081..b377a1c0 100644 --- a/src/ui/web/session/room/timeline/AnnouncementView.js +++ b/src/ui/web/session/room/timeline/AnnouncementView.js @@ -1,6 +1,6 @@ -import TemplateView from "../../../general/TemplateView.js"; +import {TemplateView} from "../../../general/TemplateView.js"; -export default class AnnouncementView extends TemplateView { +export class AnnouncementView extends TemplateView { render(t) { return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement)); } diff --git a/src/ui/web/session/room/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js index 62cde6a6..db79f161 100644 --- a/src/ui/web/session/room/timeline/GapView.js +++ b/src/ui/web/session/room/timeline/GapView.js @@ -1,6 +1,6 @@ -import TemplateView from "../../../general/TemplateView.js"; +import {TemplateView} from "../../../general/TemplateView.js"; -export default class GapView extends TemplateView { +export class GapView extends TemplateView { render(t, vm) { const className = { GapView: true, diff --git a/src/ui/web/session/room/timeline/TextMessageView.js b/src/ui/web/session/room/timeline/TextMessageView.js index a4d698eb..7cf39cce 100644 --- a/src/ui/web/session/room/timeline/TextMessageView.js +++ b/src/ui/web/session/room/timeline/TextMessageView.js @@ -1,6 +1,6 @@ -import TemplateView from "../../../general/TemplateView.js"; +import {TemplateView} from "../../../general/TemplateView.js"; -export default class TextMessageView extends TemplateView { +export class TextMessageView extends TemplateView { render(t, vm) { // no bindings ... should this be a template view? return t.li( diff --git a/src/ui/web/session/room/timeline/TimelineTile.js b/src/ui/web/session/room/timeline/TimelineTile.js index 4e87f182..003f3191 100644 --- a/src/ui/web/session/room/timeline/TimelineTile.js +++ b/src/ui/web/session/room/timeline/TimelineTile.js @@ -1,6 +1,6 @@ import {tag} from "../../../general/html.js"; -export default class TimelineTile { +export class TimelineTile { constructor(tileVM) { this._tileVM = tileVM; this._root = null; diff --git a/src/utils/enum.js b/src/utils/enum.js index 56b14a77..3500ef01 100644 --- a/src/utils/enum.js +++ b/src/utils/enum.js @@ -1,7 +1,7 @@ -export default function createEnum(...values) { +export function createEnum(...values) { const obj = {}; for (const value of values) { obj[value] = value; } return Object.freeze(obj); -} \ No newline at end of file +} diff --git a/src/utils/sortedIndex.js b/src/utils/sortedIndex.js index 70eaa9b8..ac002d81 100644 --- a/src/utils/sortedIndex.js +++ b/src/utils/sortedIndex.js @@ -6,7 +6,7 @@ * Based on Underscore.js 1.8.3 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors */ -export default function sortedIndex(array, value, comparator) { +export function sortedIndex(array, value, comparator) { let low = 0; let high = array.length; From 0f29fdb24eb46761b7df3620d7e7a0da1b13c547 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:27:07 +0200 Subject: [PATCH 16/93] some notes --- doc/impl-thoughts/RECONNECTING.md | 3 +++ src/matrix/Sync.js | 1 + 2 files changed, 4 insertions(+) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 5e507834..5e09772e 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -44,6 +44,9 @@ rooms should report how many messages they have queued up, and each time they se - DONE: add SyncStatus as ObservableValue of enum in Sync - DONE: cleanup SessionContainer - move all imports to non-default + - remove #ifdef + - move EventEmitter to utils + - move all lower-cased files - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing - show load progress in LoginView/SessionPickView and do away with loading screen - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index eeea0b66..673bed82 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -80,6 +80,7 @@ export class Sync { async _syncRequest(syncToken, timeout) { let {syncFilterId} = this._session; if (typeof syncFilterId !== "string") { + // TODO: this should be interruptable by stop, we can reuse _currentRequest syncFilterId = (await this._hsApi.createFilter(this._session.user.id, {room: {state: {lazy_load_members: true}}}).response()).filter_id; } this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout); From c379caf4c0ee2c52ae601a2a2f3bc1fed69bbc8b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:31:27 +0200 Subject: [PATCH 17/93] remove obsolete ifdef comments --- src/EventEmitter.js | 3 +-- src/Platform.js | 4 ---- src/matrix/room/timeline/EventKey.js | 2 -- src/matrix/room/timeline/FragmentIdComparer.js | 2 -- src/matrix/room/timeline/persistence/GapWriter.js | 2 -- src/matrix/room/timeline/persistence/SyncWriter.js | 2 -- src/observable/list/SortedMapList.js | 2 -- src/observable/map/ObservableMap.js | 2 -- 8 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/EventEmitter.js b/src/EventEmitter.js index 24d03ec3..94da0cde 100644 --- a/src/EventEmitter.js +++ b/src/EventEmitter.js @@ -36,7 +36,7 @@ export class EventEmitter { onLastSubscriptionRemoved(name) {} } -//#ifdef TESTS + export function tests() { return { test_on_off(assert) { @@ -72,4 +72,3 @@ export function tests() { } }; } -//#endif diff --git a/src/Platform.js b/src/Platform.js index bfdfbd96..d8b4a7a9 100644 --- a/src/Platform.js +++ b/src/Platform.js @@ -1,5 +1 @@ -//#ifdef PLATFORM_GNOME -//##export {default} from "./ui/gnome/GnomePlatform.js"; -//#else export {WebPlatform as Platform} from "./ui/web/WebPlatform.js"; -//#endif diff --git a/src/matrix/room/timeline/EventKey.js b/src/matrix/room/timeline/EventKey.js index 5f34a66d..885efba0 100644 --- a/src/matrix/room/timeline/EventKey.js +++ b/src/matrix/room/timeline/EventKey.js @@ -49,7 +49,6 @@ export class EventKey { } } -//#ifdef TESTS export function xtests() { const fragmentIdComparer = {compare: (a, b) => a - b}; @@ -156,4 +155,3 @@ export function xtests() { } }; } -//#endif diff --git a/src/matrix/room/timeline/FragmentIdComparer.js b/src/matrix/room/timeline/FragmentIdComparer.js index 24073dbe..da3b2243 100644 --- a/src/matrix/room/timeline/FragmentIdComparer.js +++ b/src/matrix/room/timeline/FragmentIdComparer.js @@ -180,7 +180,6 @@ export class FragmentIdComparer { } } -//#ifdef TESTS export function tests() { return { test_1_island_3_fragments(assert) { @@ -297,4 +296,3 @@ export function tests() { } } } -//#endif diff --git a/src/matrix/room/timeline/persistence/GapWriter.js b/src/matrix/room/timeline/persistence/GapWriter.js index f6939fae..36bb1256 100644 --- a/src/matrix/room/timeline/persistence/GapWriter.js +++ b/src/matrix/room/timeline/persistence/GapWriter.js @@ -178,7 +178,6 @@ export class GapWriter { } } -//#ifdef TESTS //import MemoryStorage from "../storage/memory/MemoryStorage.js"; export function xtests() { @@ -277,4 +276,3 @@ export function xtests() { }, } } -//#endif diff --git a/src/matrix/room/timeline/persistence/SyncWriter.js b/src/matrix/room/timeline/persistence/SyncWriter.js index 103bb31f..92907b5a 100644 --- a/src/matrix/room/timeline/persistence/SyncWriter.js +++ b/src/matrix/room/timeline/persistence/SyncWriter.js @@ -134,7 +134,6 @@ export class SyncWriter { } } -//#ifdef TESTS //import MemoryStorage from "../storage/memory/MemoryStorage.js"; export function xtests() { @@ -233,4 +232,3 @@ export function xtests() { }, } } -//#endif diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index 539bb65f..154febc3 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -113,7 +113,6 @@ export class SortedMapList extends BaseObservableList { } } -//#ifdef TESTS import {ObservableMap} from "../map/ObservableMap.js"; export function tests() { @@ -250,4 +249,3 @@ export function tests() { }, } } -//#endif diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index cbdd7b3c..cfc366ab 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -56,7 +56,6 @@ export class ObservableMap extends BaseObservableMap { } } -//#ifdef TESTS export function tests() { return { test_initial_values(assert) { @@ -152,4 +151,3 @@ export function tests() { }, } } -//#endif From 31f3886eba1e7138bdfd34a12d8e82091c3b722a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:35:53 +0200 Subject: [PATCH 18/93] move EventEmitter to utils --- src/domain/BrawlViewModel.js | 2 +- src/domain/LoginViewModel.js | 2 +- src/domain/SessionPickerViewModel.js | 2 +- src/domain/session/SessionLoadViewModel.js | 2 +- src/domain/session/SessionViewModel.js | 2 +- src/domain/session/SyncStatusViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 2 +- src/matrix/room/room.js | 2 +- src/{ => utils}/EventEmitter.js | 0 9 files changed, 8 insertions(+), 8 deletions(-) rename src/{ => utils}/EventEmitter.js (100%) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 45943da6..219bae10 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -3,7 +3,7 @@ import {Sync} from "../matrix/Sync.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; -import {EventEmitter} from "../EventEmitter.js"; +import {EventEmitter} from "../utils/EventEmitter.js"; export function createNewSessionId() { return (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index f26c5be0..c6030962 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../EventEmitter.js"; +import {EventEmitter} from "../utils/EventEmitter.js"; export class LoginViewModel extends EventEmitter { constructor({loginCallback, defaultHomeServer, createHsApi}) { diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index bc719a79..66807e58 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,5 +1,5 @@ import {SortedArray} from "../observable/index.js"; -import {EventEmitter} from "../EventEmitter.js"; +import {EventEmitter} from "../utils/EventEmitter.js"; import {createNewSessionId} from "./BrawlViewModel.js" class SessionItemViewModel extends EventEmitter { diff --git a/src/domain/session/SessionLoadViewModel.js b/src/domain/session/SessionLoadViewModel.js index 9193e840..9bbd8863 100644 --- a/src/domain/session/SessionLoadViewModel.js +++ b/src/domain/session/SessionLoadViewModel.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../../EventEmitter.js"; +import {EventEmitter} from "../../utils/EventEmitter.js"; import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 02a48834..cef60944 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../../EventEmitter.js"; +import {EventEmitter} from "../../utils/EventEmitter.js"; import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js index 17fb243c..738aefa6 100644 --- a/src/domain/session/SyncStatusViewModel.js +++ b/src/domain/session/SyncStatusViewModel.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../../EventEmitter.js"; +import {EventEmitter} from "../../utils/EventEmitter.js"; export class SyncStatusViewModel extends EventEmitter { constructor(sync) { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 8f117cbe..074070bc 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../../../EventEmitter.js"; +import {EventEmitter} from "../../../utils/EventEmitter.js"; import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {avatarInitials} from "../avatar.js"; diff --git a/src/matrix/room/room.js b/src/matrix/room/room.js index 71cb88bd..06bc3a6e 100644 --- a/src/matrix/room/room.js +++ b/src/matrix/room/room.js @@ -1,4 +1,4 @@ -import {EventEmitter} from "../../EventEmitter.js"; +import {EventEmitter} from "../../utils/EventEmitter.js"; import {RoomSummary} from "./summary.js"; import {SyncWriter} from "./timeline/persistence/SyncWriter.js"; import {GapWriter} from "./timeline/persistence/GapWriter.js"; diff --git a/src/EventEmitter.js b/src/utils/EventEmitter.js similarity index 100% rename from src/EventEmitter.js rename to src/utils/EventEmitter.js From a097929dbd9aee3ff5595399a8cfb14208bf095a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:41:10 +0200 Subject: [PATCH 19/93] rename lowercase filenames of classes to camelcase like class --- src/main.js | 2 +- src/matrix/Session.js | 2 +- src/matrix/room/{room.js => Room.js} | 2 +- src/matrix/room/{summary.js => RoomSummary.js} | 0 src/matrix/storage/idb/{query-target.js => QueryTarget.js} | 0 src/matrix/storage/idb/{storage.js => Storage.js} | 2 +- src/matrix/storage/idb/{create.js => StorageFactory.js} | 2 +- src/matrix/storage/idb/{store.js => Store.js} | 2 +- src/matrix/storage/idb/{transaction.js => Transaction.js} | 2 +- src/matrix/storage/idb/stores/{member.js => MemberStore.js} | 0 src/matrix/storage/memory/Storage.js | 2 +- 11 files changed, 8 insertions(+), 8 deletions(-) rename src/matrix/room/{room.js => Room.js} (99%) rename src/matrix/room/{summary.js => RoomSummary.js} (100%) rename src/matrix/storage/idb/{query-target.js => QueryTarget.js} (100%) rename src/matrix/storage/idb/{storage.js => Storage.js} (96%) rename src/matrix/storage/idb/{create.js => StorageFactory.js} (98%) rename src/matrix/storage/idb/{store.js => Store.js} (98%) rename src/matrix/storage/idb/{transaction.js => Transaction.js} (98%) rename src/matrix/storage/idb/stores/{member.js => MemberStore.js} (100%) diff --git a/src/main.js b/src/main.js index f85e2844..111b4a31 100644 --- a/src/main.js +++ b/src/main.js @@ -2,7 +2,7 @@ import {HomeServerApi} from "./matrix/net/HomeServerApi.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; import {fetchRequest} from "./matrix/net/request/fetch.js"; import {Reconnector} from "./matrix/net/Reconnector.js"; -import {StorageFactory} from "./matrix/storage/idb/create.js"; +import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js"; import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import {BrawlViewModel} from "./domain/BrawlViewModel.js"; import {BrawlView} from "./ui/web/BrawlView.js"; diff --git a/src/matrix/Session.js b/src/matrix/Session.js index b0a21133..4186d5f3 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1,4 +1,4 @@ -import {Room} from "./room/room.js"; +import {Room} from "./room/Room.js"; import { ObservableMap } from "../observable/index.js"; import { SendScheduler, RateLimitingBackoff } from "./SendScheduler.js"; import {User} from "./User.js"; diff --git a/src/matrix/room/room.js b/src/matrix/room/Room.js similarity index 99% rename from src/matrix/room/room.js rename to src/matrix/room/Room.js index 06bc3a6e..e533d932 100644 --- a/src/matrix/room/room.js +++ b/src/matrix/room/Room.js @@ -1,5 +1,5 @@ import {EventEmitter} from "../../utils/EventEmitter.js"; -import {RoomSummary} from "./summary.js"; +import {RoomSummary} from "./RoomSummary.js"; import {SyncWriter} from "./timeline/persistence/SyncWriter.js"; import {GapWriter} from "./timeline/persistence/GapWriter.js"; import {Timeline} from "./timeline/Timeline.js"; diff --git a/src/matrix/room/summary.js b/src/matrix/room/RoomSummary.js similarity index 100% rename from src/matrix/room/summary.js rename to src/matrix/room/RoomSummary.js diff --git a/src/matrix/storage/idb/query-target.js b/src/matrix/storage/idb/QueryTarget.js similarity index 100% rename from src/matrix/storage/idb/query-target.js rename to src/matrix/storage/idb/QueryTarget.js diff --git a/src/matrix/storage/idb/storage.js b/src/matrix/storage/idb/Storage.js similarity index 96% rename from src/matrix/storage/idb/storage.js rename to src/matrix/storage/idb/Storage.js index ddf0f73a..6db4b1c1 100644 --- a/src/matrix/storage/idb/storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -1,4 +1,4 @@ -import {Transaction} from "./transaction.js"; +import {Transaction} from "./Transaction.js"; import { STORE_NAMES, StorageError } from "../common.js"; export class Storage { diff --git a/src/matrix/storage/idb/create.js b/src/matrix/storage/idb/StorageFactory.js similarity index 98% rename from src/matrix/storage/idb/create.js rename to src/matrix/storage/idb/StorageFactory.js index dc56dfcd..8f5babcd 100644 --- a/src/matrix/storage/idb/create.js +++ b/src/matrix/storage/idb/StorageFactory.js @@ -1,4 +1,4 @@ -import {Storage} from "./storage.js"; +import {Storage} from "./Storage.js"; import { openDatabase, reqAsPromise } from "./utils.js"; import { exportSession, importSession } from "./export.js"; diff --git a/src/matrix/storage/idb/store.js b/src/matrix/storage/idb/Store.js similarity index 98% rename from src/matrix/storage/idb/store.js rename to src/matrix/storage/idb/Store.js index a3eed7ef..7acf9411 100644 --- a/src/matrix/storage/idb/store.js +++ b/src/matrix/storage/idb/Store.js @@ -1,4 +1,4 @@ -import {QueryTarget} from "./query-target.js"; +import {QueryTarget} from "./QueryTarget.js"; import { reqAsPromise } from "./utils.js"; import { StorageError } from "../common.js"; diff --git a/src/matrix/storage/idb/transaction.js b/src/matrix/storage/idb/Transaction.js similarity index 98% rename from src/matrix/storage/idb/transaction.js rename to src/matrix/storage/idb/Transaction.js index 916f9bc5..dc18321b 100644 --- a/src/matrix/storage/idb/transaction.js +++ b/src/matrix/storage/idb/Transaction.js @@ -1,6 +1,6 @@ import {txnAsPromise} from "./utils.js"; import {StorageError} from "../common.js"; -import {Store} from "./store.js"; +import {Store} from "./Store.js"; import {SessionStore} from "./stores/SessionStore.js"; import {RoomSummaryStore} from "./stores/RoomSummaryStore.js"; import {TimelineEventStore} from "./stores/TimelineEventStore.js"; diff --git a/src/matrix/storage/idb/stores/member.js b/src/matrix/storage/idb/stores/MemberStore.js similarity index 100% rename from src/matrix/storage/idb/stores/member.js rename to src/matrix/storage/idb/stores/MemberStore.js diff --git a/src/matrix/storage/memory/Storage.js b/src/matrix/storage/memory/Storage.js index 3c5294a9..206c95ca 100644 --- a/src/matrix/storage/memory/Storage.js +++ b/src/matrix/storage/memory/Storage.js @@ -1,4 +1,4 @@ -import {Transaction} from "./transaction.js"; +import {Transaction} from "./Transaction.js"; import { STORE_MAP, STORE_NAMES } from "../common.js"; export class Storage { From ad7c564d5d835529d92d9f45310cde408148e002 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:43:02 +0200 Subject: [PATCH 20/93] notes --- doc/impl-thoughts/RECONNECTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 5e09772e..933d66a8 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -43,10 +43,10 @@ rooms should report how many messages they have queued up, and each time they se - DONE: cleanup Reconnector with recent changes, move generic code, make imports work - DONE: add SyncStatus as ObservableValue of enum in Sync - DONE: cleanup SessionContainer - - move all imports to non-default - - remove #ifdef - - move EventEmitter to utils - - move all lower-cased files + - DONE: move all imports to non-default + - DONE: remove #ifdef + - DONE: move EventEmitter to utils + - DONE: move all lower-cased files - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing - show load progress in LoginView/SessionPickView and do away with loading screen - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer From de7dcf6a401b5d4cf3963dbdee65f000285a3a00 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 21:56:10 +0200 Subject: [PATCH 21/93] adjust main.js to use SessionContainer --- src/main.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/main.js b/src/main.js index 111b4a31..7e0643bb 100644 --- a/src/main.js +++ b/src/main.js @@ -1,12 +1,11 @@ -import {HomeServerApi} from "./matrix/net/HomeServerApi.js"; // import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; import {fetchRequest} from "./matrix/net/request/fetch.js"; -import {Reconnector} from "./matrix/net/Reconnector.js"; +import {SessionContainer} from "./matrix/SessionContainer.js"; import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js"; import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js"; import {BrawlViewModel} from "./domain/BrawlViewModel.js"; import {BrawlView} from "./ui/web/BrawlView.js"; -import {Clock as DOMClock} from "./ui/web/dom/Clock.js"; +import {Clock} from "./ui/web/dom/Clock.js"; import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js"; export default async function main(container) { @@ -22,13 +21,22 @@ export default async function main(container) { // window.getBrawlFetchLog = () => recorder.log(); // normal network: const request = fetchRequest; - const clock = new DOMClock(); + const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1"); + const clock = new Clock(); const vm = new BrawlViewModel({ - storageFactory: new StorageFactory(), - createHsApi: (homeServer, accessToken, reconnector) => new HomeServerApi({homeServer, accessToken, request, reconnector}), - sessionInfoStorage: new SessionInfoStorage("brawl_sessions_v1"), - clock: new DOMClock(), + createSessionContainer: () => { + return new SessionContainer({ + random: Math.random, + onlineStatus: new OnlineStatus(), + storageFactory: new StorageFactory(), + sessionInfoStorage, + request, + clock, + }); + }, + sessionInfoStorage, + clock, }); await vm.load(); const view = new BrawlView(vm); From bb7fca059242e209414c470a1d6352c0a357fdc9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 22:26:04 +0200 Subject: [PATCH 22/93] support deleting the session from the container --- src/matrix/SessionContainer.js | 24 +++++++++++++++++++++--- src/matrix/storage/idb/Storage.js | 4 ++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index b907cc7e..504d1034 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -39,6 +39,8 @@ export class SessionContainer { this._reconnector = null; this._session = null; this._sync = null; + this._sessionId = null; + this._storage = null; } _createNewSessionId() { @@ -119,17 +121,18 @@ export class SessionContainer { request: this._request, reconnector: this._reconnector, }); - const storage = await this._storageFactory.create(sessionInfo.id); + this._sessionId = sessionInfo.id; + this._storage = await this._storageFactory.create(sessionInfo.id); // no need to pass access token to session const filteredSessionInfo = { deviceId: sessionInfo.deviceId, userId: sessionInfo.userId, homeServer: sessionInfo.homeServer, }; - this._session = new Session({storage, sessionInfo: filteredSessionInfo, hsApi}); + this._session = new Session({storage: this._storage, sessionInfo: filteredSessionInfo, hsApi}); await this._session.load(); - this._sync = new Sync({hsApi, storage, session: this._session}); + this._sync = new Sync({hsApi, storage: this._storage, session: this._session}); // notify sync and session when back online this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { if (state === ConnectionStatus.Online) { @@ -206,6 +209,21 @@ export class SessionContainer { this._waitForFirstSyncHandle.dispose(); this._waitForFirstSyncHandle = null; } + if (this._storage) { + this._storage.close(); + } + } + + async deleteSession() { + if (this._sessionId) { + // if one fails, don't block the other from trying + // also, run in parallel + await Promise.all([ + this._storageFactory.delete(this._sessionId), + this._sessionInfoStorage.delete(this._sessionId), + ]); + this._sessionId = null; + } } } diff --git a/src/matrix/storage/idb/Storage.js b/src/matrix/storage/idb/Storage.js index 6db4b1c1..812e1cc8 100644 --- a/src/matrix/storage/idb/Storage.js +++ b/src/matrix/storage/idb/Storage.js @@ -37,4 +37,8 @@ export class Storage { throw new StorageError("readWriteTxn failed", err); } } + + close() { + this._db.close(); + } } From a5965ad3789acb5cd73eacceeb15f2dafcc6121a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 22:29:34 +0200 Subject: [PATCH 23/93] port LoginViewModel over to SessionContainer --- src/domain/LoginViewModel.js | 64 +++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index c6030962..68e1f7c6 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,11 +1,15 @@ import {EventEmitter} from "../utils/EventEmitter.js"; +import {LoadStatus} from "../matrix/SessionContainer.js"; +import {AbortError} from "../utils/error.js"; export class LoginViewModel extends EventEmitter { - constructor({loginCallback, defaultHomeServer, createHsApi}) { + constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { super(); - this._loginCallback = loginCallback; + this._createSessionContainer = createSessionContainer; + this._sessionCallback = sessionCallback; this._defaultHomeServer = defaultHomeServer; - this._createHsApi = createHsApi; + this._sessionContainer = null; + this._loadWaitHandle = null; this._loading = false; this._error = null; } @@ -18,13 +22,25 @@ export class LoginViewModel extends EventEmitter { get loading() { return this._loading; } async login(username, password, homeserver) { - const hsApi = this._createHsApi(homeserver); try { this._loading = true; this.emit("change", "loading"); - const loginData = await hsApi.passwordLogin(username, password).response(); - loginData.homeServerUrl = homeserver; - this._loginCallback(loginData); + this._sessionContainer = this._createSessionContainer(); + this._sessionContainer.startWithLogin(homeserver, username, password); + this._loadWaitHandle = this._sessionContainer.loadStatus.waitFor(s => { + this.emit("change", "loadStatus"); + return s === LoadStatus.Ready; + }); + try { + await this._loadWaitHandle.promise; + } catch (err) { + if (err instanceof AbortError) { + // login was cancelled + return; + } + } + this._loadWaitHandle = null; + this._sessionCallback(this._sessionContainer); // wait for parent view model to switch away here } catch (err) { this._error = err; @@ -33,7 +49,39 @@ export class LoginViewModel extends EventEmitter { } } + get loadStatus() { + return this._sessionContainer && this._sessionContainer.loadStatus; + } + + get loadError() { + if (this._sessionContainer) { + const error = this._sessionContainer.loadError; + if (error) { + return error.message; + } + } + return null; + } + + async cancelLogin() { + if (!this._loading) { + return; + } + this._loading = false; + this.emit("change", "loading"); + if (this._sessionContainer) { + this._sessionContainer.stop(); + await this._sessionContainer.deleteSession(); + this._sessionContainer = null; + } + if (this._loadWaitHandle) { + // rejects with AbortError + this._loadWaitHandle.dispose(); + this._loadWaitHandle = null; + } + } + cancel() { - this._loginCallback(); + this._sessionCallback(); } } From f4983b5ba625d12e72d5433f72ef3242cb70b94a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 22:49:14 +0200 Subject: [PATCH 24/93] port SessionPickerViewModel to SessionContainer --- src/domain/SessionPickerViewModel.js | 47 +++++++++++++++++++++++++--- src/main.js | 4 ++- src/matrix/SessionContainer.js | 47 ++++------------------------ 3 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 66807e58..98fe89f4 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,6 +1,7 @@ import {SortedArray} from "../observable/index.js"; import {EventEmitter} from "../utils/EventEmitter.js"; -import {createNewSessionId} from "./BrawlViewModel.js" +import {LoadStatus} from "../matrix/SessionContainer.js"; +import {SyncStatus} from "../matrix/Sync.js"; class SessionItemViewModel extends EventEmitter { constructor(sessionInfo, pickerVM) { @@ -99,22 +100,58 @@ class SessionItemViewModel extends EventEmitter { } export class SessionPickerViewModel { - constructor({storageFactory, sessionInfoStorage, sessionCallback}) { + constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { this._storageFactory = storageFactory; this._sessionInfoStorage = sessionInfoStorage; this._sessionCallback = sessionCallback; + this._createSessionContainer = createSessionContainer; this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id)); + this._loading = false; } + // this loads all the sessions async load() { const sessions = await this._sessionInfoStorage.getAll(); this._sessions.setManyUnsorted(sessions.map(s => new SessionItemViewModel(s, this))); } - pick(id) { + // this is the loading of a single picked session + get loading() { + return this._loading; + } + + get loadStatus() { + return this._sessionContainer && this._sessionContainer.loadStatus; + } + + get loadError() { + if (this._sessionContainer) { + const error = this._sessionContainer.loadError; + if (error) { + return error.message; + } + } + return null; + } + + async pick(id) { const sessionVM = this._sessions.array.find(s => s.id === id); if (sessionVM) { - this._sessionCallback(sessionVM.sessionInfo); + this._loading = true; + this.emit("change", "loading"); + this._sessionContainer = this._createSessionContainer(); + this._sessionContainer.startWithExistingSession(sessionVM.sessionInfo.id); + // TODO: allow to cancel here + const waitHandle = this._sessionContainer.loadStatus.waitFor(s => { + this.emit("change", "loadStatus"); + // wait for initial sync, but not catchup sync + return ( + s === LoadStatus.FirstSync && + this._sessionContainer.sync.status === SyncStatus.CatchupSync + ) || s === LoadStatus.Ready; + }); + await waitHandle.promise; + this._sessionCallback(this._sessionContainer); } } @@ -129,7 +166,7 @@ export class SessionPickerViewModel { const data = JSON.parse(json); const {sessionInfo} = data; sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`; - sessionInfo.id = createNewSessionId(); + sessionInfo.id = this._createSessionContainer().createNewSessionId(); await this._storageFactory.import(sessionInfo.id, data.stores); await this._sessionInfoStorage.add(sessionInfo); this._sessions.set(new SessionItemViewModel(sessionInfo, this)); diff --git a/src/main.js b/src/main.js index 7e0643bb..c9440927 100644 --- a/src/main.js +++ b/src/main.js @@ -23,19 +23,21 @@ export default async function main(container) { const request = fetchRequest; const sessionInfoStorage = new SessionInfoStorage("brawl_sessions_v1"); const clock = new Clock(); + const storageFactory = new StorageFactory(); const vm = new BrawlViewModel({ createSessionContainer: () => { return new SessionContainer({ random: Math.random, onlineStatus: new OnlineStatus(), - storageFactory: new StorageFactory(), + storageFactory, sessionInfoStorage, request, clock, }); }, sessionInfoStorage, + storageFactory, clock, }); await vm.load(); diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 504d1034..f0445163 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -43,7 +43,7 @@ export class SessionContainer { this._storage = null; } - _createNewSessionId() { + createNewSessionId() { return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString(); } @@ -54,6 +54,9 @@ export class SessionContainer { this._status.set(LoadStatus.Loading); try { const sessionInfo = await this._sessionInfoStorage.get(sessionId); + if (!sessionInfo) { + throw new Error("Invalid session id: " + sessionId); + } await this._loadSessionInfo(sessionInfo); } catch (err) { this._error = err; @@ -70,7 +73,7 @@ export class SessionContainer { try { const hsApi = new HomeServerApi({homeServer, request: this._request}); const loginData = await hsApi.passwordLogin(username, password).response(); - const sessionId = this._createNewSessionId(); + const sessionId = this.createNewSessionId(); sessionInfo = { id: sessionId, deviceId: loginData.device_id, @@ -211,6 +214,7 @@ export class SessionContainer { } if (this._storage) { this._storage.close(); + this._storage = null; } } @@ -226,42 +230,3 @@ export class SessionContainer { } } } - -/* -function main() { - // these are only required for external classes, - // SessionFactory has it's defaults for internal classes - const sessionFactory = new SessionFactory({ - Clock: DOMClock, - OnlineState: DOMOnlineState, - SessionInfoStorage: LocalStorageSessionStore, // should be called SessionInfoStore? - StorageFactory: window.indexedDB ? IDBStorageFactory : MemoryStorageFactory, // should be called StorageManager? - // should be moved to StorageFactory as `KeyBounds`?: minStorageKey, middleStorageKey, maxStorageKey - // would need to pass it into EventKey though - request, - }); - - // lets not do this in a first cut - // internally in the matrix lib - const room = new creator.ctor("Room", Room)({}); - - // or short - const sessionFactory = new SessionFactory(WebFactory); - // sessionFactory.sessionInfoStore - - // registration - // const registration = sessionFactory.registerUser(); - // registration.stage - - - const container = sessionFactory.startWithRegistration(registration); - const container = sessionFactory.startWithLogin(server, username, password); - const container = sessionFactory.startWithExistingSession(sessionId); - // container.loadStatus is an ObservableValue - await container.loadStatus.waitFor(s => s === LoadStatus.FirstSync && container.sync.status === SyncStatus.CatchupSync || s === LoadStatus.Ready); - - // loader isn't needed anymore from now on - const {session, sync, reconnector} = container; - container.stop(); -} -*/ From b32f5711bfe12a0b79ef93bcf120806d2ff0952b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 23:10:13 +0200 Subject: [PATCH 25/93] port BrawlViewModel to SessionContainer --- src/domain/BrawlViewModel.js | 99 ++++++++++++------------------------ 1 file changed, 33 insertions(+), 66 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 219bae10..6e308861 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,28 +1,24 @@ -import {Session} from "../matrix/Session.js"; -import {Sync} from "../matrix/Sync.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {EventEmitter} from "../utils/EventEmitter.js"; -export function createNewSessionId() { - return (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(); -} - export class BrawlViewModel extends EventEmitter { - constructor({storageFactory, sessionInfoStorage, createHsApi, clock}) { + constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) { super(); - this._storageFactory = storageFactory; + this._createSessionContainer = createSessionContainer; this._sessionInfoStorage = sessionInfoStorage; - this._createHsApi = createHsApi; + this._storageFactory = storageFactory; this._clock = clock; this._loading = false; this._error = null; this._sessionViewModel = null; - this._sessionSubscription = null; this._loginViewModel = null; this._sessionPickerViewModel = null; + + this._sessionContainer = null; + this._sessionCallback = this._sessionCallback.bind(this); } async load() { @@ -33,12 +29,29 @@ export class BrawlViewModel extends EventEmitter { } } + _sessionCallback(sessionContainer) { + if (sessionContainer) { + this._setSection(() => { + this._sessionContainer = sessionContainer; + this._sessionViewModel = new SessionViewModel(sessionContainer); + }); + } else { + // switch between picker and login + if (this.activeSection === "login") { + this._showPicker(); + } else { + this._showLogin(); + } + } + } + async _showPicker() { this._setSection(() => { this._sessionPickerViewModel = new SessionPickerViewModel({ sessionInfoStorage: this._sessionInfoStorage, storageFactory: this._storageFactory, - sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo) + createSessionContainer: this._createSessionContainer, + sessionCallback: this._sessionCallback, }); }); try { @@ -51,20 +64,14 @@ export class BrawlViewModel extends EventEmitter { _showLogin() { this._setSection(() => { this._loginViewModel = new LoginViewModel({ - createHsApi: this._createHsApi, defaultHomeServer: "https://matrix.org", - loginCallback: loginData => this._onLoginFinished(loginData) + createSessionContainer: this._createSessionContainer, + sessionCallback: this._sessionCallback, }); }) } - _showSession(session, sync) { - this._setSection(() => { - this._sessionViewModel = new SessionViewModel({session, sync}); - }); - } - get activeSection() { if (this._error) { return "error"; @@ -79,66 +86,26 @@ export class BrawlViewModel extends EventEmitter { } } + _setSection(setter) { - const oldSection = this.activeSection; // clear all members the activeSection depends on this._error = null; this._loading = false; this._sessionViewModel = null; this._loginViewModel = null; this._sessionPickerViewModel = null; + + if (this._sessionContainer) { + this._sessionContainer.stop(); + this._sessionContainer = null; + } // now set it again setter(); - const newSection = this.activeSection; - // remove session subscription when navigating away - if (oldSection === "session" && newSection !== oldSection) { - this._sessionSubscription(); - this._sessionSubscription = null; - } this.emit("change", "activeSection"); } - get loadingText() { return this._loadingText; } + get error() { return this._error; } get sessionViewModel() { return this._sessionViewModel; } get loginViewModel() { return this._loginViewModel; } get sessionPickerViewModel() { return this._sessionPickerViewModel; } - get errorText() { return this._error && this._error.message; } - - async _onLoginFinished(loginData) { - if (loginData) { - // TODO: extract random() as it is a source of non-determinism - const sessionId = createNewSessionId(); - const sessionInfo = { - id: sessionId, - deviceId: loginData.device_id, - userId: loginData.user_id, - homeServer: loginData.homeServerUrl, - accessToken: loginData.access_token, - lastUsed: this._clock.now() - }; - await this._sessionInfoStorage.add(sessionInfo); - this._loadSession(sessionInfo); - } else { - this._showPicker(); - } - } - - _onSessionPicked(sessionInfo) { - if (sessionInfo) { - this._loadSession(sessionInfo); - this._sessionInfoStorage.updateLastUsed(sessionInfo.id, this._clock.now()); - } else { - this._showLogin(); - } - } - - async _loadSession(sessionInfo) { - this._setSection(() => { - // TODO this is pseudo code-ish - const container = this._createSessionContainer(); - this._sessionViewModel = new SessionViewModel({session, sync}); - this._sessionSubscription = this._activeSessionContainer.subscribe(this._updateSessionState); - this._activeSessionContainer.start(sessionInfo); - }); - } } From c95981a35d4ae62e13b7683e6c2f04aaf85b496d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 23:10:33 +0200 Subject: [PATCH 26/93] remove unused code --- src/domain/session/SessionLoadViewModel.js | 43 ---------------------- 1 file changed, 43 deletions(-) delete mode 100644 src/domain/session/SessionLoadViewModel.js diff --git a/src/domain/session/SessionLoadViewModel.js b/src/domain/session/SessionLoadViewModel.js deleted file mode 100644 index 9bbd8863..00000000 --- a/src/domain/session/SessionLoadViewModel.js +++ /dev/null @@ -1,43 +0,0 @@ -import {EventEmitter} from "../../utils/EventEmitter.js"; -import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; -import {RoomViewModel} from "./room/RoomViewModel.js"; -import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; - -export class SessionLoadViewModel extends ViewModel { - constructor(options) { - super(options); - this._sessionContainer = options.sessionContainer; - this._updateState(); - } - - onSubscribeFirst() { - this.track(this._sessionContainer.subscribe(this._updateState)); - } - - _updateState(previousState) { - const state = this._sessionContainer.state; - if (previousState !== LoadState.Ready && state === LoadState.Ready) { - this._sessionViewModel = new SessionViewModel(this.childOptions({ - sessionContainer: this._sessionContainer - })); - this.track(this._sessionViewModel); - } else if (previousState === LoadState.Ready && state !== LoadState.Ready) { - this.disposables.disposeTracked(this._sessionViewModel); - this._sessionViewModel = null; - } - this.emit(); - } - - get isLoading() { - const state = this._sessionContainer.state; - return state === LoadState.Loading || state === LoadState.InitialSync; - } - - get loadingLabel() { - switch (this._sessionContainer.state) { - case LoadState.Loading: return "Loading your conversations…"; - case LoadState.InitialSync: return "Getting your conversations from the server…"; - default: return null; - } - } -} From 69a8786f8fc5566092d7e2c246b195c9e64f8e35 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 20 Apr 2020 23:10:54 +0200 Subject: [PATCH 27/93] update notes --- doc/impl-thoughts/RECONNECTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index 933d66a8..f8cb02a5 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -47,9 +47,9 @@ rooms should report how many messages they have queued up, and each time they se - DONE: remove #ifdef - DONE: move EventEmitter to utils - DONE: move all lower-cased files - - change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing + - DONE: change main.js to pass in a creation function of a SessionContainer instead of everything it is replacing + - DONE: adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer - show load progress in LoginView/SessionPickView and do away with loading screen - - adjust BrawlViewModel, SessionPickViewModel and LoginViewModel to use a SessionContainer - DONE: rename SessionsStore to SessionInfoStorage - make sure we've renamed all \*State enums and fields to \*Status - add pendingMessageCount prop to SendQueue and Room, aggregate this in Session From 277c8af6286d037dead3822d884a829512c204fc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:46:47 +0200 Subject: [PATCH 28/93] Headers is a DOM specific class, use Map instead in HomeServerApi --- src/matrix/net/HomeServerApi.js | 8 ++++---- src/matrix/net/request/fetch.js | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 918987c5..99299190 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -55,13 +55,13 @@ export class HomeServerApi { .join("&"); url = `${url}?${queryString}`; let bodyString; - const headers = new Headers(); + const headers = new Map(); if (this._accessToken) { - headers.append("Authorization", `Bearer ${this._accessToken}`); + headers.set("Authorization", `Bearer ${this._accessToken}`); } - headers.append("Accept", "application/json"); + headers.set("Accept", "application/json"); if (body) { - headers.append("Content-Type", "application/json"); + headers.set("Content-Type", "application/json"); bodyString = JSON.stringify(body); } const requestResult = this._requestFn(url, { diff --git a/src/matrix/net/request/fetch.js b/src/matrix/net/request/fetch.js index bc9ea8b7..886e8195 100644 --- a/src/matrix/net/request/fetch.js +++ b/src/matrix/net/request/fetch.js @@ -44,6 +44,13 @@ export function fetchRequest(url, options) { referrer: "no-referrer", cache: "no-cache", }); + if (options.headers) { + const headers = new Headers(); + for(const [name, value] of options.headers.entries()) { + headers.append(name, value); + } + options.headers = headers; + } const promise = fetch(url, options).then(async response => { const {status} = response; const body = await response.json(); From 3f840d9d3338189b6349e2f9e1deb148975edd25 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:47:31 +0200 Subject: [PATCH 29/93] simple unit test for hsApi --- src/matrix/net/HomeServerApi.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 99299190..1526c1e2 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -138,3 +138,27 @@ export class HomeServerApi { return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, options); } } + +export function tests() { + function createRequestMock(result) { + return function() { + return { + abort() {}, + response() { + return Promise.resolve(result); + } + } + } + } + + return { + "superficial happy path for GET": async assert => { + const hsApi = new HomeServerApi({ + request: createRequestMock({body: 42, status: 200}), + homeServer: "https://hs.tld" + }); + const result = await hsApi._get("foo", null, null, null).response(); + assert.strictEqual(result, 42); + } + } +} From 174fd3ea4ab592cc2ca8e366fbda95601f1597b2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:47:46 +0200 Subject: [PATCH 30/93] don't assume options --- src/matrix/net/HomeServerApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 1526c1e2..6454a2a2 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -70,7 +70,7 @@ export class HomeServerApi { body: bodyString, }); - if (options.timeout) { + if (options && options.timeout) { const timeout = this._createTimeout(options.timeout); // abort request if timeout finishes first timeout.elapsed().then( From f826258c75e9e9490a3ad0048d5144190a8c6321 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:47:53 +0200 Subject: [PATCH 31/93] null doesn't set queryParams to {} here, so revert explicitly --- src/matrix/net/HomeServerApi.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 6454a2a2..65deff2e 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -43,8 +43,8 @@ export class HomeServerApi { return `${this._homeserver}/_matrix/client/r0${csPath}`; } - _request(method, url, queryParams = {}, body, options) { - const queryString = Object.entries(queryParams) + _request(method, url, queryParams, body, options) { + const queryString = Object.entries(queryParams || {}) .filter(([, value]) => value !== undefined) .map(([name, value]) => { if (typeof value === "object") { From 08b1c02af7f2c6b82d5e6a44a0a4246ae61f7916 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:48:25 +0200 Subject: [PATCH 32/93] remove start value, can be hardcoded for now also fix params in wrong order this way --- src/matrix/SessionContainer.js | 2 +- src/matrix/net/ExponentialRetryDelay.js | 5 +++-- src/matrix/net/Reconnector.js | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index f0445163..c5db341b 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -115,7 +115,7 @@ export class SessionContainer { this._status.set(LoadStatus.Loading); this._reconnector = new Reconnector({ onlineStatus: this._onlineStatus, - delay: new ExponentialRetryDelay(2000, this._clock.createTimeout), + retryDelay: new ExponentialRetryDelay(this._clock.createTimeout), createMeasure: this._clock.createMeasure }); const hsApi = new HomeServerApi({ diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.js index a1317619..a1bae822 100644 --- a/src/matrix/net/ExponentialRetryDelay.js +++ b/src/matrix/net/ExponentialRetryDelay.js @@ -1,7 +1,8 @@ import {AbortError} from "../../utils/error.js"; export class ExponentialRetryDelay { - constructor(createTimeout, start = 2000) { + constructor(createTimeout) { + const start = 2000; this._start = start; this._current = start; this._createTimeout = createTimeout; @@ -49,7 +50,7 @@ export function tests() { return { "test sequence": async assert => { const clock = new MockClock(); - const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout); let promise; assert.strictEqual(retryDelay.nextValue, 2000); diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index aa1f80da..0489b963 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -137,7 +137,7 @@ export function tests() { const clock = new MockClock(); const {createMeasure} = clock; const onlineStatus = new ObservableValue(false); - const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout); const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); const {connectionStatus} = reconnector; const statuses = []; @@ -162,7 +162,7 @@ export function tests() { const clock = new MockClock(); const {createMeasure} = clock; const onlineStatus = new ObservableValue(false); - const retryDelay = new ExponentialRetryDelay(clock.createTimeout, 2000); + const retryDelay = new ExponentialRetryDelay(clock.createTimeout); const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); const {connectionStatus} = reconnector; reconnector.onRequestFailed(createHsApiMock(1)); From 3359c6950f7e8dd0f5f74bfae42e24dfe9243941 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:49:03 +0200 Subject: [PATCH 33/93] typo (this made the loadStatus undefined :/) --- src/matrix/SessionContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index c5db341b..5c6f6cd8 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -91,7 +91,7 @@ export class SessionContainer { } else { this._loginFailure = LoginFailure.Unknown; } - this._status.set(LoadStatus.LoginFailure); + this._status.set(LoadStatus.LoginFailed); } else if (err instanceof ConnectionError) { this._loginFailure = LoginFailure.Connection; this._status.set(LoadStatus.LoginFailure); From a19e541e1e52e1a48a8b883e9c0921d7c8924705 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:49:21 +0200 Subject: [PATCH 34/93] less http specific --- src/matrix/SessionContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 5c6f6cd8..5468ba70 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -86,7 +86,7 @@ export class SessionContainer { } catch (err) { this._error = err; if (err instanceof HomeServerError) { - if (err.statusCode === 403) { + if (err.errcode === "M_FORBIDDEN") { this._loginFailure = LoginFailure.Credentials; } else { this._loginFailure = LoginFailure.Unknown; From 96aa4f83b98ea58f610436911079507f473a6aea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:49:49 +0200 Subject: [PATCH 35/93] remove loading section in BrawlView --- src/domain/BrawlViewModel.js | 5 ----- src/ui/web/BrawlView.js | 2 -- 2 files changed, 7 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 6e308861..7ecf9f56 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -11,7 +11,6 @@ export class BrawlViewModel extends EventEmitter { this._storageFactory = storageFactory; this._clock = clock; - this._loading = false; this._error = null; this._sessionViewModel = null; this._loginViewModel = null; @@ -75,8 +74,6 @@ export class BrawlViewModel extends EventEmitter { get activeSection() { if (this._error) { return "error"; - } else if(this._loading) { - return "loading"; } else if (this._sessionViewModel) { return "session"; } else if (this._loginViewModel) { @@ -86,11 +83,9 @@ export class BrawlViewModel extends EventEmitter { } } - _setSection(setter) { // clear all members the activeSection depends on this._error = null; - this._loading = false; this._sessionViewModel = null; this._loginViewModel = null; this._sessionPickerViewModel = null; diff --git a/src/ui/web/BrawlView.js b/src/ui/web/BrawlView.js index f4325a77..27779dc6 100644 --- a/src/ui/web/BrawlView.js +++ b/src/ui/web/BrawlView.js @@ -16,8 +16,6 @@ export class BrawlView { switch (this._vm.activeSection) { case "error": return new StatusView({header: "Something went wrong", message: this._vm.errorText}); - case "loading": - return new StatusView({header: "Loading", message: this._vm.loadingText}); case "session": return new SessionView(this._vm.sessionViewModel); case "login": From e37101210c4dd8dc5e96d73c492220f804ba0d8f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:52:28 +0200 Subject: [PATCH 36/93] adjust LoginView(Model) further to showing loading status in place --- src/domain/LoginViewModel.js | 78 ++++++++++++++++++++++++++--------- src/ui/web/login/LoginView.js | 20 ++++++--- 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 68e1f7c6..64843209 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,5 +1,5 @@ import {EventEmitter} from "../utils/EventEmitter.js"; -import {LoadStatus} from "../matrix/SessionContainer.js"; +import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {AbortError} from "../utils/error.js"; export class LoginViewModel extends EventEmitter { @@ -8,6 +8,7 @@ export class LoginViewModel extends EventEmitter { this._createSessionContainer = createSessionContainer; this._sessionCallback = sessionCallback; this._defaultHomeServer = defaultHomeServer; + this._homeserver = null; this._sessionContainer = null; this._loadWaitHandle = null; this._loading = false; @@ -18,18 +19,24 @@ export class LoginViewModel extends EventEmitter { get passwordPlaceholder() { return "Password"; } get hsPlaceholder() { return "Your matrix homeserver"; } get defaultHomeServer() { return this._defaultHomeServer; } - get error() { return this._error; } - get loading() { return this._loading; } + get loading() {return this._loading} + + get showLoadLabel() { + return this._loading || this._sessionContainer; + } async login(username, password, homeserver) { try { this._loading = true; this.emit("change", "loading"); + this._homeserver = homeserver; this._sessionContainer = this._createSessionContainer(); this._sessionContainer.startWithLogin(homeserver, username, password); this._loadWaitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change", "loadStatus"); - return s === LoadStatus.Ready; + this.emit("change", "loadLabel"); + return s === LoadStatus.Ready || + s === LoadStatus.LoginFailed || + s === LoadStatus.Error; }); try { await this._loadWaitHandle.promise; @@ -40,8 +47,17 @@ export class LoginViewModel extends EventEmitter { } } this._loadWaitHandle = null; - this._sessionCallback(this._sessionContainer); - // wait for parent view model to switch away here + if (this._sessionContainer.loadStatus.get() === LoadStatus.Ready) { + this._sessionCallback(this._sessionContainer); + // wait for parent view model to switch away here + } else { + this._loading = false; + this.emit("change", "loading"); + if (this._sessionContainer.loadError) { + console.error(this._sessionContainer.loadError); + } + } + } catch (err) { this._error = err; this._loading = false; @@ -49,21 +65,43 @@ export class LoginViewModel extends EventEmitter { } } - get loadStatus() { - return this._sessionContainer && this._sessionContainer.loadStatus; - } - - get loadError() { - if (this._sessionContainer) { - const error = this._sessionContainer.loadError; - if (error) { - return error.message; + get loadLabel() { + if (this._error) { + return `Something went wrong: ${this._error.message}.`; + } + if (this.showLoadLabel) { + if (this._sessionContainer) { + switch (this._sessionContainer.loadStatus.get()) { + case LoadStatus.NotLoading: + return `Preparing…`; + case LoadStatus.Login: + return `Checking your login and password…`; + case LoadStatus.LoginFailed: + switch (this._sessionContainer.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.Loading: + return `Loading your conversations…`; + case LoadStatus.FirstSync: + return `Getting your conversations from the server…`; + case LoadStatus.Error: + return `Something went wrong: ${this._sessionContainer.loadError.message}.`; + default: + return this._sessionContainer.loadStatus.get(); + } } + return `Preparing…`; } return null; } - async cancelLogin() { + async cancel() { if (!this._loading) { return; } @@ -81,7 +119,9 @@ export class LoginViewModel extends EventEmitter { } } - cancel() { - this._sessionCallback(); + goBack() { + if (!this._loading) { + this._sessionCallback(); + } } } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 9eccc062..c4b68477 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -7,9 +7,10 @@ export class LoginView extends TemplateView { } render(t, vm) { - const username = t.input({type: "text", placeholder: vm.usernamePlaceholder}); - const password = t.input({type: "password", placeholder: vm.passwordPlaceholder}); - const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer}); + const disabled = vm => vm.loading; + const username = t.input({type: "text", placeholder: vm.usernamePlaceholder, disabled}); + const password = t.input({type: "password", placeholder: vm.passwordPlaceholder, disabled}); + const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled}); return t.div({className: "LoginView form"}, [ t.h1(["Log in to your homeserver"]), t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), @@ -18,10 +19,19 @@ export class LoginView extends TemplateView { t.div(homeserver), t.div(t.button({ onClick: () => vm.login(username.value, password.value, homeserver.value), - disabled: vm => vm.loading + disabled }, "Log In")), - t.div(t.button({onClick: () => vm.cancel()}, ["Pick an existing session"])), + t.div(t.button({onClick: () => vm.goBack(), disabled}, ["Pick an existing session"])), + t.if(vm => vm.showLoadLabel, renderLoadProgress), t.p(brawlGithubLink(t)) ]); } } + +function renderLoadProgress(t) { + return t.div({className: "loadProgress"}, [ + t.div({className: "spinner"}), + t.p(vm => vm.loadLabel), + t.if(vm => vm.loading, t => t.button({onClick: vm => vm.cancel()}, "Cancel login")) + ]); +} From 449262e3c1bf370cfbef70c521220c57b2f6401c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:52:56 +0200 Subject: [PATCH 37/93] adjust ctor of SessionViewModel to accept sessionContainer now --- src/domain/session/SessionViewModel.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index cef60944..4f4e0b57 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -4,10 +4,10 @@ import {RoomViewModel} from "./room/RoomViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; export class SessionViewModel extends EventEmitter { - constructor({session, sync}) { + constructor(sessionContainer) { super(); - this._session = session; - this._syncStatusViewModel = new SyncStatusViewModel(sync); + this._session = sessionContainer.session; + this._syncStatusViewModel = new SyncStatusViewModel(sessionContainer.sync); this._currentRoomViewModel = null; const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { return new RoomTileViewModel({ From e0799181d93d9548c4b42193fdaba78adc45d23c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:53:18 +0200 Subject: [PATCH 38/93] show error when mount() fails in SwitchView --- src/ui/web/general/SwitchView.js | 9 ++++++++- src/ui/web/general/error.js | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/ui/web/general/error.js diff --git a/src/ui/web/general/SwitchView.js b/src/ui/web/general/SwitchView.js index 80ee7198..66950052 100644 --- a/src/ui/web/general/SwitchView.js +++ b/src/ui/web/general/SwitchView.js @@ -1,3 +1,5 @@ +import {errorToDOM} from "./error.js"; + export class SwitchView { constructor(defaultView) { this._childView = defaultView; @@ -23,7 +25,12 @@ export class SwitchView { const oldRoot = this.root(); this._childView.unmount(); this._childView = newView; - const newRoot = this._childView.mount(); + let newRoot; + try { + newRoot = this._childView.mount(); + } catch (err) { + newRoot = errorToDOM(err); + } const parent = oldRoot.parentElement; if (parent) { parent.replaceChild(newRoot, oldRoot); diff --git a/src/ui/web/general/error.js b/src/ui/web/general/error.js new file mode 100644 index 00000000..c218504e --- /dev/null +++ b/src/ui/web/general/error.js @@ -0,0 +1,12 @@ +import {tag} from "./html.js"; + +export function errorToDOM(error) { + const stack = new Error().stack; + const callee = stack.split("\n")[1]; + return tag.div([ + tag.h2("Something went wrong…"), + tag.h3(error.message), + tag.p(`This occurred while running ${callee}.`), + tag.pre(error.stack), + ]); +} From e080bf28a768d1c1c080133610ec044bf7e95c0c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:53:38 +0200 Subject: [PATCH 39/93] expose root view model as global variable for console inspecting --- src/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.js b/src/main.js index c9440927..7c8d46b6 100644 --- a/src/main.js +++ b/src/main.js @@ -40,6 +40,7 @@ export default async function main(container) { storageFactory, clock, }); + window.__brawlViewModel = vm; await vm.load(); const view = new BrawlView(vm); container.appendChild(view.mount()); From 0424ffe231bf96cfed8c5f2b622acfd8f8cf87fc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:53:57 +0200 Subject: [PATCH 40/93] disable this for now as sync is not an EventEmitter anymore --- src/domain/session/SyncStatusViewModel.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js index 738aefa6..7d26ee11 100644 --- a/src/domain/session/SyncStatusViewModel.js +++ b/src/domain/session/SyncStatusViewModel.js @@ -18,13 +18,13 @@ export class SyncStatusViewModel extends EventEmitter { onFirstSubscriptionAdded(name) { if (name === "change") { - this._sync.on("status", this._onStatus); + //this._sync.status.("status", this._onStatus); } } onLastSubscriptionRemoved(name) { if (name === "change") { - this._sync.on("status", this._onStatus); + //this._sync.status.("status", this._onStatus); } } From 067027d3762ff867bda49d9e4a17d499f80f539f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 20:54:17 +0200 Subject: [PATCH 41/93] note updates --- doc/- sync comes under session | 3 --- doc/impl-thoughts/LOCAL-ECHO-STATE.md | 16 ++++++++++++++++ doc/impl-thoughts/RECONNECTING.md | 7 ++++++- 3 files changed, 22 insertions(+), 4 deletions(-) delete mode 100644 doc/- sync comes under session create mode 100644 doc/impl-thoughts/LOCAL-ECHO-STATE.md diff --git a/doc/- sync comes under session b/doc/- sync comes under session deleted file mode 100644 index 4db2baf1..00000000 --- a/doc/- sync comes under session +++ /dev/null @@ -1,3 +0,0 @@ - - sync comes under session - - sessioncontainer/client orchestrating reconnection - - \ No newline at end of file diff --git a/doc/impl-thoughts/LOCAL-ECHO-STATE.md b/doc/impl-thoughts/LOCAL-ECHO-STATE.md new file mode 100644 index 00000000..4d5a7215 --- /dev/null +++ b/doc/impl-thoughts/LOCAL-ECHO-STATE.md @@ -0,0 +1,16 @@ +# Local echo + +## Remote vs local state for account_data, etc ... + +For things like account data, and other requests that might fail, we could persist what we are sending next to the last remote version we have (with a flag for which one is remote and local, part of the key). E.g. for account data the key would be: [type, localOrRemoteFlag] + +localOrRemoteFlag would be 1 of 3: + - Remote + - (Local)Unsent + - (Local)Sent + +although we only want 1 remote and 1 local value for a given key, perhaps a second field where localOrRemoteFlag is a boolean, and a sent=boolean field as well? We need this to know if we need to retry. + +This will allow resending of these requests if needed. Once the request goes through, we remove the local version. + +then we can also see what the current value is with or without the pending local changes, and we don't have to wait for remote echo... diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index f8cb02a5..fc7a3b41 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -66,7 +66,12 @@ rooms should report how many messages they have queued up, and each time they se NO: When connected, syncing and not sending anything, just hide the thing for now? although when you send messages it will just pop in and out all the time. - see if it makes sense for SendScheduler to use the same RetryDelay as Reconnector - - finally adjust all file names to their class names? e.g. camel case + - DONE: finally adjust all file names to their class names? e.g. camel case - see if we want more dependency injection - for classes from outside sdk - for internal sdk classes? probably not yet + + + + +thought: do we want to retry a request a couple of times when we can't reach the server before handing it over to the reconnector? Not that some requests may succeed while others may fail, like when matrix.org is really slow, some requests may timeout and others may not. Although starting a service like sync while it is still succeeding should be mostly fine. Perhaps we can pass a canRetry flag to the HomeServerApi that if we get a ConnectionError, we will retry. Only when the flag is not set, we'd call the Reconnector. The downside of this is that if 2 parts are doing requests, 1 retries and 1 does not, and the both requests fail, the other part of the code would still be retrying when the reconnector already kicked in. The HomeServerApi should perhaps tell the retryer if it should give up if a non-retrying request already caused the reconnector to kick in? From b2954fd774d08e849b140f9d7eb8b4c8ce4c7382 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 21:53:37 +0200 Subject: [PATCH 42/93] extract loadLabel logic to reuse in SessionPickerViewModel --- src/domain/LoginViewModel.js | 57 ++++++++++++++++-------------------- src/domain/common.js | 22 ++++++++++++++ 2 files changed, 48 insertions(+), 31 deletions(-) create mode 100644 src/domain/common.js diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 64843209..f5488790 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,6 +1,21 @@ import {EventEmitter} from "../utils/EventEmitter.js"; import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {AbortError} from "../utils/error.js"; +import {loadLabel} from "./common.js"; + +function loadLoginLabel(loadStatus, loadError, loginFailure, homeserver) { + if (!loadError && loadStatus && loadStatus.get() === LoadStatus.LoginFailed) { + switch (loginFailure) { + case LoginFailure.LoginFailure: + return `Your username and/or password don't seem to be correct.`; + case LoginFailure.Connection: + return `Can't connect to ${homeserver}.`; + case LoginFailure.Unknown: + return `Something went wrong while checking your login and password.`; + } + } + return loadLabel(loadStatus, loadError); +} export class LoginViewModel extends EventEmitter { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { @@ -22,7 +37,7 @@ export class LoginViewModel extends EventEmitter { get loading() {return this._loading} get showLoadLabel() { - return this._loading || this._sessionContainer; + return this._loading || this._sessionContainer || this._error; } async login(username, password, homeserver) { @@ -43,7 +58,8 @@ export class LoginViewModel extends EventEmitter { } catch (err) { if (err instanceof AbortError) { // login was cancelled - return; + } else { + throw err; } } this._loadWaitHandle = null; @@ -57,7 +73,6 @@ export class LoginViewModel extends EventEmitter { console.error(this._sessionContainer.loadError); } } - } catch (err) { this._error = err; this._loading = false; @@ -67,36 +82,16 @@ export class LoginViewModel extends EventEmitter { get loadLabel() { if (this._error) { - return `Something went wrong: ${this._error.message}.`; + return loadLabel(null, this._error); } if (this.showLoadLabel) { - if (this._sessionContainer) { - switch (this._sessionContainer.loadStatus.get()) { - case LoadStatus.NotLoading: - return `Preparing…`; - case LoadStatus.Login: - return `Checking your login and password…`; - case LoadStatus.LoginFailed: - switch (this._sessionContainer.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.Loading: - return `Loading your conversations…`; - case LoadStatus.FirstSync: - return `Getting your conversations from the server…`; - case LoadStatus.Error: - return `Something went wrong: ${this._sessionContainer.loadError.message}.`; - default: - return this._sessionContainer.loadStatus.get(); - } - } - return `Preparing…`; + const sc = this._sessionContainer; + return loadLoginLabel( + sc && sc.loadStatus, + sc && sc.loadError, + sc && sc.loginFailure, + this._homeserver + ); } return null; } diff --git a/src/domain/common.js b/src/domain/common.js new file mode 100644 index 00000000..99ade58e --- /dev/null +++ b/src/domain/common.js @@ -0,0 +1,22 @@ +import {LoadStatus} from "../matrix/SessionContainer.js"; + +export function loadLabel(loadStatus, loadError) { + if (loadError || loadStatus.get() === LoadStatus.Error) { + return `Something went wrong: ${loadError && loadError.message}.`; + } + if (loadStatus) { + switch (loadStatus.get()) { + case LoadStatus.NotLoading: + return `Preparing…`; + case LoadStatus.Login: + return `Checking your login and password…`; + case LoadStatus.Loading: + return `Loading your conversations…`; + case LoadStatus.FirstSync: + return `Getting your conversations from the server…`; + default: + return this._sessionContainer.loadStatus.get(); + } + } + return `Preparing…`; +} From acc511e69ff168904157af572fafa4d22d52f130 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Apr 2020 21:53:55 +0200 Subject: [PATCH 43/93] extract loading into a sub viewmodel to show the loading in a separate view --- src/domain/SessionPickerViewModel.js | 122 +++++++++++++++++++-------- 1 file changed, 89 insertions(+), 33 deletions(-) diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 98fe89f4..7c122b16 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -2,6 +2,7 @@ import {SortedArray} from "../observable/index.js"; import {EventEmitter} from "../utils/EventEmitter.js"; import {LoadStatus} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; +import {loadLabel} from "./common.js"; class SessionItemViewModel extends EventEmitter { constructor(sessionInfo, pickerVM) { @@ -99,14 +100,79 @@ class SessionItemViewModel extends EventEmitter { } } -export class SessionPickerViewModel { +class LoadViewModel extends EventEmitter { + constructor({createSessionContainer, sessionCallback, sessionId}) { + super(); + this._createSessionContainer = createSessionContainer; + this._sessionCallback = sessionCallback; + this._sessionId = sessionId; + this._loading = false; + } + + async _start() { + try { + this._loading = true; + this.emit("change", "loading"); + this._sessionContainer = this._createSessionContainer(); + this._sessionContainer.startWithExistingSession(this._sessionId); + this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { + this.emit("change", "loadStatus"); + // wait for initial sync, but not catchup sync + const isCatchupSync = s === LoadStatus.FirstSync && + this._sessionContainer.sync.status === SyncStatus.CatchupSync; + return isCatchupSync || + s === LoadStatus.Error || + s === LoadStatus.Ready; + }); + try { + await this._waitHandle.promise; + } catch (err) { + // swallow AbortError + } + if (this._sessionContainer.loadStatus.get() !== LoadStatus.Error) { + this._sessionCallback(this._sessionContainer); + } + } catch (err) { + this._error = err; + } finally { + this._loading = false; + this.emit("change", "loading"); + } + } + + get loading() { + return this._loading; + } + + goBack() { + if (this._sessionContainer) { + this._sessionContainer.stop(); + this._sessionContainer = null; + if (this._waitHandle) { + this._waitHandle.dispose(); + } + } + this._sessionCallback(); + } + + get loadLabel() { + const sc = this._sessionContainer; + return loadLabel( + sc && sc.loadStatus, + sc && sc.loadError || this._error); + } +} + +export class SessionPickerViewModel extends EventEmitter { constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { + super(); this._storageFactory = storageFactory; this._sessionInfoStorage = sessionInfoStorage; this._sessionCallback = sessionCallback; this._createSessionContainer = createSessionContainer; this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id)); - this._loading = false; + this._loadViewModel = null; + this._error = null; } // this loads all the sessions @@ -115,43 +181,33 @@ export class SessionPickerViewModel { this._sessions.setManyUnsorted(sessions.map(s => new SessionItemViewModel(s, this))); } - // this is the loading of a single picked session - get loading() { - return this._loading; - } - - get loadStatus() { - return this._sessionContainer && this._sessionContainer.loadStatus; - } - - get loadError() { - if (this._sessionContainer) { - const error = this._sessionContainer.loadError; - if (error) { - return error.message; - } - } - return null; + // for the loading of 1 picked session + get loadViewModel() { + return this._loadViewModel; } async pick(id) { + if (this._loadViewModel) { + return; + } const sessionVM = this._sessions.array.find(s => s.id === id); if (sessionVM) { - this._loading = true; - this.emit("change", "loading"); - this._sessionContainer = this._createSessionContainer(); - this._sessionContainer.startWithExistingSession(sessionVM.sessionInfo.id); - // TODO: allow to cancel here - const waitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change", "loadStatus"); - // wait for initial sync, but not catchup sync - return ( - s === LoadStatus.FirstSync && - this._sessionContainer.sync.status === SyncStatus.CatchupSync - ) || s === LoadStatus.Ready; + this._loadViewModel = new LoadViewModel({ + createSessionContainer: this._createSessionContainer, + sessionCallback: sessionContainer => { + if (sessionContainer) { + // make parent view model move away + this._sessionCallback(sessionContainer); + } else { + // show list of session again + this._loadViewModel = null; + this.emit("change", "loadViewModel"); + } + }, + sessionId: sessionVM.id, }); - await waitHandle.promise; - this._sessionCallback(this._sessionContainer); + this._loadViewModel.start(); + this.emit("change", "loadViewModel"); } } From 657ec9aa6211b75a4f4f4e6bc1d6facf5a7531a1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Apr 2020 09:06:04 +0200 Subject: [PATCH 44/93] move loading view state to own view model, so we're more free how to show it, and we can better reuse it --- src/domain/LoginViewModel.js | 36 +++++---- src/domain/SessionLoadViewModel.js | 106 +++++++++++++++++++++++++++ src/domain/SessionPickerViewModel.js | 74 ++----------------- src/domain/common.js | 22 ------ 4 files changed, 135 insertions(+), 103 deletions(-) create mode 100644 src/domain/SessionLoadViewModel.js delete mode 100644 src/domain/common.js diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index f5488790..3d7853f3 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -3,19 +3,7 @@ import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {AbortError} from "../utils/error.js"; import {loadLabel} from "./common.js"; -function loadLoginLabel(loadStatus, loadError, loginFailure, homeserver) { - if (!loadError && loadStatus && loadStatus.get() === LoadStatus.LoginFailed) { - switch (loginFailure) { - case LoginFailure.LoginFailure: - return `Your username and/or password don't seem to be correct.`; - case LoginFailure.Connection: - return `Can't connect to ${homeserver}.`; - case LoginFailure.Unknown: - return `Something went wrong while checking your login and password.`; - } - } - return loadLabel(loadStatus, loadError); -} + export class LoginViewModel extends EventEmitter { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { @@ -81,6 +69,28 @@ export class LoginViewModel extends EventEmitter { } get loadLabel() { + const sc = this._sessionContainer; + const error = this._error || (sc && sc.loadError); + + if (error || (sc && sc.loadStatus.get() === LoadStatus.Error)) { + return `Something went wrong: ${error && error.message}.`; + } + if (loadStatus) { + switch (loadStatus.get()) { + case LoadStatus.NotLoading: + return `Preparing…`; + case LoadStatus.Login: + return `Checking your login and password…`; + case LoadStatus.Loading: + return `Loading your conversations…`; + case LoadStatus.FirstSync: + return `Getting your conversations from the server…`; + default: + return this._sessionContainer.loadStatus.get(); + } + } + return `Preparing…`; + if (this._error) { return loadLabel(null, this._error); } diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js new file mode 100644 index 00000000..a5d2e971 --- /dev/null +++ b/src/domain/SessionLoadViewModel.js @@ -0,0 +1,106 @@ +import {EventEmitter} from "../utils/EventEmitter.js"; +import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; +import {SyncStatus} from "../matrix/Sync.js"; + +export class SessionLoadViewModel extends EventEmitter { + constructor({createAndStartSessionContainer, sessionCallback, homeserver}) { + super(); + this._createAndStartSessionContainer = createAndStartSessionContainer; + this._sessionCallback = sessionCallback; + this._homeserver = homeserver; + this._loading = false; + this._error = null; + } + + async start() { + if (this._loading) { + return; + } + try { + this._loading = true; + this.emit("change"); + this._sessionContainer = this._createAndStartSessionContainer(); + this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { + this.emit("change"); + // wait for initial sync, but not catchup sync + const isCatchupSync = s === LoadStatus.FirstSync && + this._sessionContainer.sync.status === SyncStatus.CatchupSync; + return isCatchupSync || + s === LoadStatus.LoginFailed || + s === LoadStatus.Error || + s === LoadStatus.Ready; + }); + try { + await this._waitHandle.promise; + } catch (err) { + // swallow AbortError + } + // TODO: should we deal with no connection during initial sync + // and we're retrying as well here? + // e.g. show in the label what is going on wrt connectionstatus + // much like we will once you are in the app. Probably a good idea + + // did it finish or get stuck at LoginFailed or Error? + const loadStatus = this._sessionContainer.loadStatus.get(); + if (loadStatus === LoadStatus.FirstSync || loadStatus === LoadStatus.Ready) { + this._sessionCallback(this._sessionContainer); + } + } catch (err) { + this._error = err; + } finally { + this._loading = false; + this.emit("change"); + } + } + + get loading() { + return this._loading; + } + + goBack() { + if (this._sessionContainer) { + this._sessionContainer.stop(); + this._sessionContainer = null; + if (this._waitHandle) { + this._waitHandle.dispose(); + } + } + this._sessionCallback(); + } + + get loadLabel() { + const sc = this._sessionContainer; + const error = this._error || (sc && sc.loadError); + + if (error || (sc && sc.loadStatus.get() === LoadStatus.Error)) { + return `Something went wrong: ${error && error.message}.`; + } + + if (sc) { + switch (sc.loadStatus.get()) { + case LoadStatus.NotLoading: + return `Preparing…`; + case LoadStatus.Login: + return `Checking your login and password…`; + case LoadStatus.LoginFailed: + switch (sc.loginFailure) { + case LoginFailure.LoginFailure: + return `Your username and/or password don't seem to be correct.`; + case LoginFailure.Connection: + return `Can't connect to ${this._homeserver}.`; + case LoginFailure.Unknown: + return `Something went wrong while checking your login and password.`; + } + break; + case LoadStatus.Loading: + return `Loading your conversations…`; + case LoadStatus.FirstSync: + return `Getting your conversations from the server…`; + default: + return this._sessionContainer.loadStatus.get(); + } + } + + return `Preparing…`; + } +} diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 7c122b16..1959ff85 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,8 +1,5 @@ import {SortedArray} from "../observable/index.js"; import {EventEmitter} from "../utils/EventEmitter.js"; -import {LoadStatus} from "../matrix/SessionContainer.js"; -import {SyncStatus} from "../matrix/Sync.js"; -import {loadLabel} from "./common.js"; class SessionItemViewModel extends EventEmitter { constructor(sessionInfo, pickerVM) { @@ -100,68 +97,6 @@ class SessionItemViewModel extends EventEmitter { } } -class LoadViewModel extends EventEmitter { - constructor({createSessionContainer, sessionCallback, sessionId}) { - super(); - this._createSessionContainer = createSessionContainer; - this._sessionCallback = sessionCallback; - this._sessionId = sessionId; - this._loading = false; - } - - async _start() { - try { - this._loading = true; - this.emit("change", "loading"); - this._sessionContainer = this._createSessionContainer(); - this._sessionContainer.startWithExistingSession(this._sessionId); - this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change", "loadStatus"); - // wait for initial sync, but not catchup sync - const isCatchupSync = s === LoadStatus.FirstSync && - this._sessionContainer.sync.status === SyncStatus.CatchupSync; - return isCatchupSync || - s === LoadStatus.Error || - s === LoadStatus.Ready; - }); - try { - await this._waitHandle.promise; - } catch (err) { - // swallow AbortError - } - if (this._sessionContainer.loadStatus.get() !== LoadStatus.Error) { - this._sessionCallback(this._sessionContainer); - } - } catch (err) { - this._error = err; - } finally { - this._loading = false; - this.emit("change", "loading"); - } - } - - get loading() { - return this._loading; - } - - goBack() { - if (this._sessionContainer) { - this._sessionContainer.stop(); - this._sessionContainer = null; - if (this._waitHandle) { - this._waitHandle.dispose(); - } - } - this._sessionCallback(); - } - - get loadLabel() { - const sc = this._sessionContainer; - return loadLabel( - sc && sc.loadStatus, - sc && sc.loadError || this._error); - } -} export class SessionPickerViewModel extends EventEmitter { constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { @@ -193,7 +128,11 @@ export class SessionPickerViewModel extends EventEmitter { const sessionVM = this._sessions.array.find(s => s.id === id); if (sessionVM) { this._loadViewModel = new LoadViewModel({ - createSessionContainer: this._createSessionContainer, + createAndStartSessionContainer: () => { + const sessionContainer = this._createSessionContainer(); + sessionContainer.startWithExistingSession(sessionVM.id); + return sessionContainer; + }, sessionCallback: sessionContainer => { if (sessionContainer) { // make parent view model move away @@ -203,8 +142,7 @@ export class SessionPickerViewModel extends EventEmitter { this._loadViewModel = null; this.emit("change", "loadViewModel"); } - }, - sessionId: sessionVM.id, + } }); this._loadViewModel.start(); this.emit("change", "loadViewModel"); diff --git a/src/domain/common.js b/src/domain/common.js deleted file mode 100644 index 99ade58e..00000000 --- a/src/domain/common.js +++ /dev/null @@ -1,22 +0,0 @@ -import {LoadStatus} from "../matrix/SessionContainer.js"; - -export function loadLabel(loadStatus, loadError) { - if (loadError || loadStatus.get() === LoadStatus.Error) { - return `Something went wrong: ${loadError && loadError.message}.`; - } - if (loadStatus) { - switch (loadStatus.get()) { - case LoadStatus.NotLoading: - return `Preparing…`; - case LoadStatus.Login: - return `Checking your login and password…`; - case LoadStatus.Loading: - return `Loading your conversations…`; - case LoadStatus.FirstSync: - return `Getting your conversations from the server…`; - default: - return this._sessionContainer.loadStatus.get(); - } - } - return `Preparing…`; -} From 2008cf74f125c5da158e14e06a3ce80b6658b674 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 10:00:51 +0200 Subject: [PATCH 45/93] Template becomes a view --- src/ui/web/general/ListView.js | 6 +- src/ui/web/general/Template.js | 136 +++++++++++++++++++++-------- src/ui/web/general/TemplateView.js | 39 --------- 3 files changed, 103 insertions(+), 78 deletions(-) delete mode 100644 src/ui/web/general/TemplateView.js diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js index cc01191b..80c2394c 100644 --- a/src/ui/web/general/ListView.js +++ b/src/ui/web/general/ListView.js @@ -10,6 +10,8 @@ function insertAt(parentNode, idx, childNode) { } } +const MOUNT_ARGS = {parentProvidesUpdates: true}; + export class ListView { constructor({list, onItemClick, className}, childCreator) { this._onItemClick = onItemClick; @@ -86,7 +88,7 @@ export class ListView { for (let item of this._list) { const child = this._childCreator(item); this._childInstances.push(child); - const childDomNode = child.mount(); + const childDomNode = child.mount(MOUNT_ARGS); this._root.appendChild(childDomNode); } } @@ -95,7 +97,7 @@ export class ListView { this.onBeforeListChanged(); const child = this._childCreator(value); this._childInstances.splice(idx, 0, child); - insertAt(this._root, idx, child.mount()); + insertAt(this._root, idx, child.mount(MOUNT_ARGS)); this.onListChanged(); } diff --git a/src/ui/web/general/Template.js b/src/ui/web/general/Template.js index 43891951..577937ab 100644 --- a/src/ui/web/general/Template.js +++ b/src/ui/web/general/Template.js @@ -1,5 +1,5 @@ import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js"; - +import {errorToDOM} from "./error.js"; function objHasFns(obj) { for(const value of Object.values(obj)) { @@ -23,43 +23,39 @@ function objHasFns(obj) { - create views */ export class Template { - constructor(value, render) { + constructor(value, render = undefined) { this._value = value; + this._render = render; this._eventListeners = null; this._bindings = null; - this._subTemplates = null; - this._root = render(this, this._value); - this._attach(); + // this should become _subViews and also include templates. + // How do we know which ones we should update though? + // Wrapper class? + this._subViews = null; + this._root = null; + this._boundUpdateFromValue = null; } - root() { - return this._root; - } + _subscribe() { + this._boundUpdateFromValue = this._updateFromValue.bind(this); - update(value) { - this._value = value; - if (this._bindings) { - for (const binding of this._bindings) { - binding(); - } + if (typeof this._value.on === "function") { + this._value.on("change", this._boundUpdateFromValue); } - if (this._subTemplates) { - for (const sub of this._subTemplates) { - sub.update(value); - } + else if (typeof this._value.subscribe === "function") { + this._value.subscribe(this._boundUpdateFromValue); } } - dispose() { - if (this._eventListeners) { - for (let {node, name, fn} of this._eventListeners) { - node.removeEventListener(name, fn); + _unsubscribe() { + if (this._boundUpdateFromValue) { + if (typeof this._value.off === "function") { + this._value.off("change", this._boundUpdateFromValue); } - } - if (this._subTemplates) { - for (const sub of this._subTemplates) { - sub.dispose(); + else if (typeof this._value.unsubscribe === "function") { + this._value.unsubscribe(this._boundUpdateFromValue); } + this._boundUpdateFromValue = null; } } @@ -71,6 +67,53 @@ export class Template { } } + _detach() { + if (this._eventListeners) { + for (let {node, name, fn} of this._eventListeners) { + node.removeEventListener(name, fn); + } + } + } + + mount(options) { + if (this._render) { + this._root = this._render(this, this._value); + } else if (this.render) { // overriden in subclass + this._root = this.render(this, this._value); + } + const parentProvidesUpdates = options && options.parentProvidesUpdates; + if (!parentProvidesUpdates) { + this._subscribe(); + } + this._attach(); + return this._root; + } + + unmount() { + this._detach(); + this._unsubscribe(); + for (const v of this._subViews) { + v.unmount(); + } + } + + root() { + return this._root; + } + + _updateFromValue() { + this.update(this._value); + } + + update(value) { + this._value = value; + if (this._bindings) { + for (const binding of this._bindings) { + binding(); + } + } + } + _addEventListener(node, name, fn) { if (!this._eventListeners) { this._eventListeners = []; @@ -85,11 +128,11 @@ export class Template { this._bindings.push(bindingFn); } - _addSubTemplate(t) { - if (!this._subTemplates) { - this._subTemplates = []; + _addSubView(view) { + if (!this._subViews) { + this._subViews = []; } - this._subTemplates.push(t); + this._subViews.push(view); } _addAttributeBinding(node, name, fn) { @@ -199,19 +242,38 @@ export class Template { return node; } + // this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template + // you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree). + view(view) { + let root; + try { + root = view.mount(); + } catch (err) { + return errorToDOM(err); + } + this._addSubView(view); + return root; + } + + // sugar + createTemplate(render) { + return vm => new Template(vm, render); + } + // creates a conditional subtemplate - if(fn, render) { + if(fn, viewCreator) { const boolFn = value => !!fn(value); return this._addReplaceNodeBinding(boolFn, (prevNode) => { if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) { - const templateIdx = this._subTemplates.findIndex(t => t.root() === prevNode); - const [template] = this._subTemplates.splice(templateIdx, 1); - template.dispose(); + const viewIdx = this._subViews.findIndex(v => v.root() === prevNode); + if (viewIdx !== -1) { + const [view] = this._subViews.splice(viewIdx, 1); + view.unmount(); + } } if (boolFn(this._value)) { - const template = new Template(this._value, render); - this._addSubTemplate(template); - return template.root(); + const view = viewCreator(this._value); + return this.view(view); } else { return document.createComment("if placeholder"); } diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js deleted file mode 100644 index 9afa1efe..00000000 --- a/src/ui/web/general/TemplateView.js +++ /dev/null @@ -1,39 +0,0 @@ -import {Template} from "./Template.js"; - -export class TemplateView { - constructor(vm, bindToChangeEvent) { - this.viewModel = vm; - this._changeEventHandler = bindToChangeEvent ? this.update.bind(this, this.viewModel) : null; - this._template = null; - } - - render() { - throw new Error("render not implemented"); - } - - mount() { - if (this._changeEventHandler) { - this.viewModel.on("change", this._changeEventHandler); - } - this._template = new Template(this.viewModel, (t, value) => this.render(t, value)); - return this.root(); - } - - root() { - return this._template.root(); - } - - unmount() { - if (this._changeEventHandler) { - this.viewModel.off("change", this._changeEventHandler); - } - this._template.dispose(); - this._template = null; - } - - update(value, prop) { - if (this._template) { - this._template.update(value); - } - } -} From 37c602f7d2ce5daa407e2612daa858dfa8eb17a0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 10:04:40 +0200 Subject: [PATCH 46/93] rename Template to TemplateView, as it is a view now. --- src/ui/web/general/{Template.js => TemplateView.js} | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) rename src/ui/web/general/{Template.js => TemplateView.js} (96%) diff --git a/src/ui/web/general/Template.js b/src/ui/web/general/TemplateView.js similarity index 96% rename from src/ui/web/general/Template.js rename to src/ui/web/general/TemplateView.js index 577937ab..7bd4ef05 100644 --- a/src/ui/web/general/Template.js +++ b/src/ui/web/general/TemplateView.js @@ -22,7 +22,7 @@ function objHasFns(obj) { missing: - create views */ -export class Template { +export class TemplateView { constructor(value, render = undefined) { this._value = value; this._render = render; @@ -257,7 +257,7 @@ export class Template { // sugar createTemplate(render) { - return vm => new Template(vm, render); + return vm => new TemplateView(vm, render); } // creates a conditional subtemplate @@ -282,7 +282,9 @@ export class Template { } for (const tag of TAG_NAMES) { - Template.prototype[tag] = function(attributes, children) { + TemplateView.prototype[tag] = function(attributes, children) { return this.el(tag, attributes, children); }; } + +// TODO: should we an instance of something else than the view itself into the render method? That way you can't call template functions outside of the render method. From e6ae60abb40b962b87c05c172289f2067633c6c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 10:05:07 +0200 Subject: [PATCH 47/93] notes about how view updates work and should work --- doc/impl-thoughts/VIEW-UPDATES.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 doc/impl-thoughts/VIEW-UPDATES.md diff --git a/doc/impl-thoughts/VIEW-UPDATES.md b/doc/impl-thoughts/VIEW-UPDATES.md new file mode 100644 index 00000000..93a909f5 --- /dev/null +++ b/doc/impl-thoughts/VIEW-UPDATES.md @@ -0,0 +1,23 @@ +# View updates + +## Current situation + + - arguments of View.update are not standardized, it's either: + - name of property that was updated on viewmodel + - names of property that was updated on viewmodel + - map of updated values + - we have 2 update mechanisms: + - listening on viewmodel change event + - through ObservableCollection which parent view listens on and calls `update(newValue)` on the child view. This is an optimization to prevent every view in a collection to need to subscribe and unsubscribe to a viewmodel. + + - should updates on a template value propagate to subviews? + - either a view listens on the view model, ... + - or waits for updates from parent view: + - item view in a list view + - subtemplate (not needed, we could just have 2 subscriptions!!) + +ok, we always subscribe in a (sub)template. But for example RoomTile and it's viewmodel; RoomTileViewModel doesn't extend EventEmitter or ObservableValue today because it (would) emit(s) updates through the parent collection. So today it's view would not subscribe to it. But if it wants to extend ViewModel to have all the other infrastructure, you'd receive double updates. + +I think we might need to make it explicit whether or not the parent will provide updates for the children or not. Maybe as a mount() parameter? Yeah, I like that. ListView would pass in `true`. Most other things would pass in `false`/`undefined`. `Template` can then choose to bind or not based on that param. + +Should we make a base/subclass of Template that does not do event binding to save a few bytes in memory for the event subscription fields that are not needed? Not now, this is less ergonimic, and a small optimization. We can always do that later, and we'd just have to replace the base class of the few views that appear in a `ListView`. From 8bde627cdbcb131fc226ee14b6ed698b7a632338 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 10:10:20 +0200 Subject: [PATCH 48/93] more WIP --- doc/impl-thoughts/RECONNECTING.md | 2 + src/domain/LoginViewModel.js | 131 ++++---------------- src/domain/SessionLoadViewModel.js | 39 ++++-- src/domain/SessionPickerViewModel.js | 7 +- src/observable/BaseObservable.js | 20 +-- src/ui/web/css/main.css | 3 +- src/ui/web/login/LoginView.js | 6 +- src/ui/web/login/SessionPickerView.js | 10 +- src/ui/web/session/SyncStatusBar.js | 2 +- src/ui/web/session/room/timeline/GapView.js | 2 +- 10 files changed, 79 insertions(+), 143 deletions(-) diff --git a/doc/impl-thoughts/RECONNECTING.md b/doc/impl-thoughts/RECONNECTING.md index fc7a3b41..310889e1 100644 --- a/doc/impl-thoughts/RECONNECTING.md +++ b/doc/impl-thoughts/RECONNECTING.md @@ -75,3 +75,5 @@ rooms should report how many messages they have queued up, and each time they se thought: do we want to retry a request a couple of times when we can't reach the server before handing it over to the reconnector? Not that some requests may succeed while others may fail, like when matrix.org is really slow, some requests may timeout and others may not. Although starting a service like sync while it is still succeeding should be mostly fine. Perhaps we can pass a canRetry flag to the HomeServerApi that if we get a ConnectionError, we will retry. Only when the flag is not set, we'd call the Reconnector. The downside of this is that if 2 parts are doing requests, 1 retries and 1 does not, and the both requests fail, the other part of the code would still be retrying when the reconnector already kicked in. The HomeServerApi should perhaps tell the retryer if it should give up if a non-retrying request already caused the reconnector to kick in? + +CatchupSync should also use timeout 0, in case there is nothing to report we spend 30s with a catchup spinner. Riot-web sync also says something about using a 0 timeout until there are no more to_device messages as they are queued up by the server and not all returned at once if there are a lot? This is needed for crypto to be aware of all to_device messages. diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 3d7853f3..11d43dc9 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,9 +1,5 @@ import {EventEmitter} from "../utils/EventEmitter.js"; -import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; -import {AbortError} from "../utils/error.js"; -import {loadLabel} from "./common.js"; - - +import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; export class LoginViewModel extends EventEmitter { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { @@ -11,121 +7,42 @@ export class LoginViewModel extends EventEmitter { this._createSessionContainer = createSessionContainer; this._sessionCallback = sessionCallback; this._defaultHomeServer = defaultHomeServer; - this._homeserver = null; - this._sessionContainer = null; - this._loadWaitHandle = null; - this._loading = false; - this._error = null; + this._loadViewModel = null; } get usernamePlaceholder() { return "Username"; } get passwordPlaceholder() { return "Password"; } get hsPlaceholder() { return "Your matrix homeserver"; } get defaultHomeServer() { return this._defaultHomeServer; } - get loading() {return this._loading} - get showLoadLabel() { - return this._loading || this._sessionContainer || this._error; - } + get loadViewModel() {return this._loadViewModel; } async login(username, password, homeserver) { - try { - this._loading = true; - this.emit("change", "loading"); - this._homeserver = homeserver; - this._sessionContainer = this._createSessionContainer(); - this._sessionContainer.startWithLogin(homeserver, username, password); - this._loadWaitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change", "loadLabel"); - return s === LoadStatus.Ready || - s === LoadStatus.LoginFailed || - s === LoadStatus.Error; - }); - try { - await this._loadWaitHandle.promise; - } catch (err) { - if (err instanceof AbortError) { - // login was cancelled + this._loadViewModel = new SessionLoadViewModel({ + createAndStartSessionContainer: () => { + const sessionContainer = this._createSessionContainer(); + sessionContainer.startWithLogin(homeserver, username, password); + return sessionContainer; + }, + sessionCallback: sessionContainer => { + if (sessionContainer) { + // make parent view model move away + this._sessionCallback(sessionContainer); } else { - throw err; + // show list of session again + this._loadViewModel = null; + this.emit("change", "loadViewModel"); } - } - this._loadWaitHandle = null; - if (this._sessionContainer.loadStatus.get() === LoadStatus.Ready) { - this._sessionCallback(this._sessionContainer); - // wait for parent view model to switch away here - } else { - this._loading = false; - this.emit("change", "loading"); - if (this._sessionContainer.loadError) { - console.error(this._sessionContainer.loadError); - } - } - } catch (err) { - this._error = err; - this._loading = false; - this.emit("change", "loading"); - } + }, + deleteSessionOnCancel: true, + homeserver, + }); + this._loadViewModel.start(); + this.emit("change", "loadViewModel"); } - get loadLabel() { - const sc = this._sessionContainer; - const error = this._error || (sc && sc.loadError); - - if (error || (sc && sc.loadStatus.get() === LoadStatus.Error)) { - return `Something went wrong: ${error && error.message}.`; - } - if (loadStatus) { - switch (loadStatus.get()) { - case LoadStatus.NotLoading: - return `Preparing…`; - case LoadStatus.Login: - return `Checking your login and password…`; - case LoadStatus.Loading: - return `Loading your conversations…`; - case LoadStatus.FirstSync: - return `Getting your conversations from the server…`; - default: - return this._sessionContainer.loadStatus.get(); - } - } - return `Preparing…`; - - if (this._error) { - return loadLabel(null, this._error); - } - if (this.showLoadLabel) { - const sc = this._sessionContainer; - return loadLoginLabel( - sc && sc.loadStatus, - sc && sc.loadError, - sc && sc.loginFailure, - this._homeserver - ); - } - return null; - } - - async cancel() { - if (!this._loading) { - return; - } - this._loading = false; - this.emit("change", "loading"); - if (this._sessionContainer) { - this._sessionContainer.stop(); - await this._sessionContainer.deleteSession(); - this._sessionContainer = null; - } - if (this._loadWaitHandle) { - // rejects with AbortError - this._loadWaitHandle.dispose(); - this._loadWaitHandle = null; - } - } - - goBack() { - if (!this._loading) { + cancel() { + if (!this._loadViewModel) { this._sessionCallback(); } } diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index a5d2e971..659abeea 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -3,11 +3,12 @@ import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; export class SessionLoadViewModel extends EventEmitter { - constructor({createAndStartSessionContainer, sessionCallback, homeserver}) { + constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) { super(); this._createAndStartSessionContainer = createAndStartSessionContainer; this._sessionCallback = sessionCallback; this._homeserver = homeserver; + this._deleteSessionOnCancel = deleteSessionOnCancel; this._loading = false; this._error = null; } @@ -33,7 +34,7 @@ export class SessionLoadViewModel extends EventEmitter { try { await this._waitHandle.promise; } catch (err) { - // swallow AbortError + return; // aborted by goBack } // TODO: should we deal with no connection during initial sync // and we're retrying as well here? @@ -53,19 +54,31 @@ export class SessionLoadViewModel extends EventEmitter { } } - get loading() { - return this._loading; + + async cancel() { + try { + if (this._sessionContainer) { + this._sessionContainer.stop(); + if (this._deleteSessionOnCancel) { + await this._sessionContainer.deletSession(); + } + this._sessionContainer = null; + } + if (this._waitHandle) { + // rejects with AbortError + this._waitHandle.dispose(); + this._waitHandle = null; + } + this._sessionCallback(); + } catch (err) { + this._error = err; + this.emit("change"); + } } - goBack() { - if (this._sessionContainer) { - this._sessionContainer.stop(); - this._sessionContainer = null; - if (this._waitHandle) { - this._waitHandle.dispose(); - } - } - this._sessionCallback(); + // to show a spinner or not + get loading() { + return this._loading; } get loadLabel() { diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 1959ff85..8274106a 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,5 +1,6 @@ import {SortedArray} from "../observable/index.js"; import {EventEmitter} from "../utils/EventEmitter.js"; +import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; class SessionItemViewModel extends EventEmitter { constructor(sessionInfo, pickerVM) { @@ -127,7 +128,7 @@ export class SessionPickerViewModel extends EventEmitter { } const sessionVM = this._sessions.array.find(s => s.id === id); if (sessionVM) { - this._loadViewModel = new LoadViewModel({ + this._loadViewModel = new SessionLoadViewModel({ createAndStartSessionContainer: () => { const sessionContainer = this._createSessionContainer(); sessionContainer.startWithExistingSession(sessionVM.id); @@ -182,6 +183,8 @@ export class SessionPickerViewModel extends EventEmitter { } cancel() { - this._sessionCallback(); + if (!this._loadViewModel) { + this._sessionCallback(); + } } } diff --git a/src/observable/BaseObservable.js b/src/observable/BaseObservable.js index 75d8023f..3afdd4c0 100644 --- a/src/observable/BaseObservable.js +++ b/src/observable/BaseObservable.js @@ -17,17 +17,21 @@ export class BaseObservable { this.onSubscribeFirst(); } return () => { - if (handler) { - this._handlers.delete(handler); - if (this._handlers.size === 0) { - this.onUnsubscribeLast(); - } - handler = null; - } - return null; + return this.unsubscribe(handler); }; } + unsubscribe(handler) { + if (handler) { + this._handlers.delete(handler); + if (this._handlers.size === 0) { + this.onUnsubscribeLast(); + } + handler = null; + } + return null; + } + // Add iterator over handlers here } diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index 1ba316a7..e01e3fc6 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -6,7 +6,8 @@ body { margin: 0; - font-family: sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: black; color: white; } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index c4b68477..b472e436 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -13,7 +13,7 @@ export class LoginView extends TemplateView { const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled}); return t.div({className: "LoginView form"}, [ t.h1(["Log in to your homeserver"]), - t.if(vm => vm.error, t => t.div({className: "error"}, vm => vm.error)), + t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))), t.div(username), t.div(password), t.div(homeserver), @@ -22,7 +22,7 @@ export class LoginView extends TemplateView { disabled }, "Log In")), t.div(t.button({onClick: () => vm.goBack(), disabled}, ["Pick an existing session"])), - t.if(vm => vm.showLoadLabel, renderLoadProgress), + t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)), t.p(brawlGithubLink(t)) ]); } @@ -32,6 +32,6 @@ function renderLoadProgress(t) { return t.div({className: "loadProgress"}, [ t.div({className: "spinner"}), t.p(vm => vm.loadLabel), - t.if(vm => vm.loading, t => t.button({onClick: vm => vm.cancel()}, "Cancel login")) + t.if(vm => vm.loading, t.template(t => t.button({onClick: vm => vm.cancel()}, "Cancel login"))) ]); } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index ed335033..e443ed81 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -27,10 +27,6 @@ function selectFileAsText(mimeType) { class SessionPickerItemView extends TemplateView { - constructor(vm) { - super(vm, true); - } - _onDeleteClick() { if (confirm("Are you sure?")) { this.viewModel.delete(); @@ -50,16 +46,16 @@ class SessionPickerItemView extends TemplateView { disabled: vm => vm.isClearing, onClick: () => this.viewModel.export(), }, "Export"); - const downloadExport = t.if(vm => vm.exportDataUrl, (t, vm) => { + const downloadExport = t.if(vm => vm.exportDataUrl, t.template((t, vm) => { return t.a({ href: vm.exportDataUrl, download: `brawl-session-${this.viewModel.id}.json`, onClick: () => setTimeout(() => this.viewModel.clearExport(), 100), }, "Download"); - }); + })); const userName = t.span({className: "userId"}, vm => vm.label); - const errorMessage = t.if(vm => vm.error, t => t.span({className: "error"}, vm => vm.error)); + const errorMessage = t.if(vm => vm.error, t.template(t => t.span({className: "error"}, vm => vm.error))); return t.li([t.div({className: "sessionInfo"}, [ userName, errorMessage, diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js index e8d4e95c..c9d22a8c 100644 --- a/src/ui/web/session/SyncStatusBar.js +++ b/src/ui/web/session/SyncStatusBar.js @@ -11,7 +11,7 @@ export class SyncStatusBar extends TemplateView { "SyncStatusBar_shown": true, }}, [ vm => vm.status, - t.if(vm => !vm.isSyncing, t => t.button({onClick: () => vm.trySync()}, "Try syncing")), + t.if(vm => !vm.isSyncing, t.template(t => t.button({onClick: () => vm.trySync()}, "Try syncing"))), window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : "" ]); } diff --git a/src/ui/web/session/room/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js index db79f161..a0daef24 100644 --- a/src/ui/web/session/room/timeline/GapView.js +++ b/src/ui/web/session/room/timeline/GapView.js @@ -12,7 +12,7 @@ export class GapView extends TemplateView { onClick: () => this.viewModel.fill(), disabled: vm => vm.isLoading }, label), - t.if(vm => vm.error, t => t.strong(vm => vm.error)) + t.if(vm => vm.error, t.template(t => t.strong(vm => vm.error))) ]); } } From f4bb609ab61009bb1f2ffa74f43b8028c32bb37d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 10:10:33 +0200 Subject: [PATCH 49/93] notes --- doc/impl-thoughts/DESIGN.md | 3 +++ src/ui/web/general/TemplateView.js | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 doc/impl-thoughts/DESIGN.md diff --git a/doc/impl-thoughts/DESIGN.md b/doc/impl-thoughts/DESIGN.md new file mode 100644 index 00000000..5fc345a2 --- /dev/null +++ b/doc/impl-thoughts/DESIGN.md @@ -0,0 +1,3 @@ +use mock view models or even a mock session to render different states of the app in a static html document, where we can somehow easily tweak the css (just browser tools, or do something in the page?) how to persist css after changes? + +Also dialogs, forms, ... could be shown on this page. diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 7bd4ef05..6fd0d7d2 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -19,8 +19,7 @@ function objHasFns(obj) { - one way binding of text values (child fn value) - refs to get dom nodes - className binding returning object with className => enabled map - missing: - - create views + - add subviews inside the template */ export class TemplateView { constructor(value, render = undefined) { @@ -288,3 +287,10 @@ for (const tag of TAG_NAMES) { } // TODO: should we an instance of something else than the view itself into the render method? That way you can't call template functions outside of the render method. +// methods that should be on the Template: +// el & all the tag names +// view +// if +// createTemplate +// +// all the binding stuff goes on this class, we just set the bindings on the members of the view. From 6a9315e70a19dd147ce9db9e2ce33bdc5deee107 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 19:12:12 +0200 Subject: [PATCH 50/93] do what we said in the comment --- src/ui/web/general/TemplateView.js | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 6fd0d7d2..ae7766dc 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -75,10 +75,13 @@ export class TemplateView { } mount(options) { + const builder = new TemplateBuilder(this); if (this._render) { - this._root = this._render(this, this._value); + this._root = this._render(builder, this._value); } else if (this.render) { // overriden in subclass - this._root = this.render(this, this._value); + this._root = this.render(builder, this._value); + } else { + throw new Error("no render function passed in, or overriden in subclass"); } const parentProvidesUpdates = options && options.parentProvidesUpdates; if (!parentProvidesUpdates) { @@ -133,6 +136,17 @@ export class TemplateView { } this._subViews.push(view); } +} + +// what is passed to render +class TemplateBuilder { + constructor(templateView) { + this._templateView = templateView; + } + + get _value() { + return this._templateView._value; + } _addAttributeBinding(node, name, fn) { let prevValue = undefined; @@ -143,7 +157,7 @@ export class TemplateView { setAttribute(node, name, newValue); } }; - this._addBinding(binding); + this._templateView._addBinding(binding); binding(); } @@ -163,7 +177,7 @@ export class TemplateView { } }; - this._addBinding(binding); + this._templateView._addBinding(binding); return node; } @@ -198,7 +212,7 @@ export class TemplateView { } else if (key.startsWith("on") && key.length > 2 && isFn) { const eventName = key.substr(2, 1).toLowerCase() + key.substr(3); const handler = value; - this._addEventListener(node, eventName, handler); + this._templateView._addEventListener(node, eventName, handler); } else if (isFn) { this._addAttributeBinding(node, key, value); } else { @@ -237,7 +251,7 @@ export class TemplateView { node = newNode; } }; - this._addBinding(binding); + this._templateView._addBinding(binding); return node; } @@ -250,7 +264,7 @@ export class TemplateView { } catch (err) { return errorToDOM(err); } - this._addSubView(view); + this._templateView._addSubView(view); return root; } @@ -281,16 +295,7 @@ export class TemplateView { } for (const tag of TAG_NAMES) { - TemplateView.prototype[tag] = function(attributes, children) { + TemplateBuilder.prototype[tag] = function(attributes, children) { return this.el(tag, attributes, children); }; } - -// TODO: should we an instance of something else than the view itself into the render method? That way you can't call template functions outside of the render method. -// methods that should be on the Template: -// el & all the tag names -// view -// if -// createTemplate -// -// all the binding stuff goes on this class, we just set the bindings on the members of the view. From 2c8c1eb7678e8877b02c6afbb1b451a6db0faf7b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 19:12:48 +0200 Subject: [PATCH 51/93] move el with the publics --- src/ui/web/general/TemplateView.js | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index ae7766dc..22bb7e43 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -181,24 +181,6 @@ class TemplateBuilder { return node; } - el(name, attributes, children) { - if (attributes && isChildren(attributes)) { - children = attributes; - attributes = null; - } - - const node = document.createElement(name); - - if (attributes) { - this._setNodeAttributes(node, attributes); - } - if (children) { - this._setNodeChildren(node, children); - } - - return node; - } - _setNodeAttributes(node, attributes) { for(let [key, value] of Object.entries(attributes)) { const isFn = typeof value === "function"; @@ -255,6 +237,24 @@ class TemplateBuilder { return node; } + el(name, attributes, children) { + if (attributes && isChildren(attributes)) { + children = attributes; + attributes = null; + } + + const node = document.createElement(name); + + if (attributes) { + this._setNodeAttributes(node, attributes); + } + if (children) { + this._setNodeChildren(node, children); + } + + return node; + } + // this insert a view, and is not a view factory for `if`, so returns the root element to insert in the template // you should not call t.view() and not use the result (e.g. attach the result to the template DOM tree). view(view) { From cdf051f19bffda07476b5cb67cf521078bceee67 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 29 Apr 2020 19:16:53 +0200 Subject: [PATCH 52/93] api changes --- src/ui/web/login/LoginView.js | 2 +- src/ui/web/login/SessionPickerView.js | 4 ++-- src/ui/web/session/SyncStatusBar.js | 2 +- src/ui/web/session/room/timeline/GapView.js | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index b472e436..ca8ea945 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -32,6 +32,6 @@ function renderLoadProgress(t) { return t.div({className: "loadProgress"}, [ t.div({className: "spinner"}), t.p(vm => vm.loadLabel), - t.if(vm => vm.loading, t.template(t => t.button({onClick: vm => vm.cancel()}, "Cancel login"))) + t.if(vm => vm.loading, t.createTemplate(t => t.button({onClick: vm => vm.cancel()}, "Cancel login"))) ]); } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index e443ed81..5380efbe 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -46,7 +46,7 @@ class SessionPickerItemView extends TemplateView { disabled: vm => vm.isClearing, onClick: () => this.viewModel.export(), }, "Export"); - const downloadExport = t.if(vm => vm.exportDataUrl, t.template((t, vm) => { + const downloadExport = t.if(vm => vm.exportDataUrl, t.createTemplate((t, vm) => { return t.a({ href: vm.exportDataUrl, download: `brawl-session-${this.viewModel.id}.json`, @@ -55,7 +55,7 @@ class SessionPickerItemView extends TemplateView { })); const userName = t.span({className: "userId"}, vm => vm.label); - const errorMessage = t.if(vm => vm.error, t.template(t => t.span({className: "error"}, vm => vm.error))); + const errorMessage = t.if(vm => vm.error, t.createTemplate(t => t.span({className: "error"}, vm => vm.error))); return t.li([t.div({className: "sessionInfo"}, [ userName, errorMessage, diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js index c9d22a8c..79fbdefe 100644 --- a/src/ui/web/session/SyncStatusBar.js +++ b/src/ui/web/session/SyncStatusBar.js @@ -11,7 +11,7 @@ export class SyncStatusBar extends TemplateView { "SyncStatusBar_shown": true, }}, [ vm => vm.status, - t.if(vm => !vm.isSyncing, t.template(t => t.button({onClick: () => vm.trySync()}, "Try syncing"))), + t.if(vm => !vm.isSyncing, t.createTemplate(t => t.button({onClick: () => vm.trySync()}, "Try syncing"))), window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : "" ]); } diff --git a/src/ui/web/session/room/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js index a0daef24..637d3ce3 100644 --- a/src/ui/web/session/room/timeline/GapView.js +++ b/src/ui/web/session/room/timeline/GapView.js @@ -12,7 +12,7 @@ export class GapView extends TemplateView { onClick: () => this.viewModel.fill(), disabled: vm => vm.isLoading }, label), - t.if(vm => vm.error, t.template(t => t.strong(vm => vm.error))) + t.if(vm => vm.error, t.createTemplate(t => t.strong(vm => vm.error))) ]); } } From ceec8937ef5bcb9836c50d0c639ea33ab96d3bfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Apr 2020 18:27:21 +0200 Subject: [PATCH 53/93] apply template view api changes --- src/ui/web/general/TemplateView.js | 10 ++++-- src/ui/web/general/html.js | 2 +- src/ui/web/login/LoginView.js | 25 ++++++++------- src/ui/web/login/SessionPickerView.js | 34 ++++++++------------- src/ui/web/session/RoomTile.js | 2 +- src/ui/web/session/SyncStatusBar.js | 4 --- src/ui/web/session/room/MessageComposer.js | 2 +- src/ui/web/session/room/RoomView.js | 6 ++-- src/ui/web/session/room/timeline/GapView.js | 2 +- 9 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 22bb7e43..31043e54 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -35,6 +35,10 @@ export class TemplateView { this._boundUpdateFromValue = null; } + get value() { + return this._value; + } + _subscribe() { this._boundUpdateFromValue = this._updateFromValue.bind(this); @@ -94,8 +98,10 @@ export class TemplateView { unmount() { this._detach(); this._unsubscribe(); - for (const v of this._subViews) { - v.unmount(); + if (this._subViews) { + for (const v of this._subViews) { + v.unmount(); + } } } diff --git a/src/ui/web/general/html.js b/src/ui/web/general/html.js index 9bea640f..24f34ff4 100644 --- a/src/ui/web/general/html.js +++ b/src/ui/web/general/html.js @@ -70,7 +70,7 @@ export function text(str) { export const TAG_NAMES = [ "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "main", "article", "aside", - "pre", "button", "time", "input", "textarea"]; + "pre", "button", "time", "input", "textarea", "svg", "circle"]; export const tag = {}; diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index ca8ea945..6ba6459c 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -2,15 +2,11 @@ import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; export class LoginView extends TemplateView { - constructor(vm) { - super(vm, true); - } - render(t, vm) { - const disabled = vm => vm.loading; const username = t.input({type: "text", placeholder: vm.usernamePlaceholder, disabled}); const password = t.input({type: "password", placeholder: vm.passwordPlaceholder, disabled}); const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled}); + const disabled = vm => !!vm.loadViewModel; return t.div({className: "LoginView form"}, [ t.h1(["Log in to your homeserver"]), t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))), @@ -28,10 +24,17 @@ export class LoginView extends TemplateView { } } -function renderLoadProgress(t) { - return t.div({className: "loadProgress"}, [ - t.div({className: "spinner"}), - t.p(vm => vm.loadLabel), - t.if(vm => vm.loading, t.createTemplate(t => t.button({onClick: vm => vm.cancel()}, "Cancel login"))) - ]); +function spinner(t, extraClasses = undefined) { + return t.svg({className: Object.assign({"spinner": true}, extraClasses), viewBox:"0 0 100% 100%"}, + t.circle({cx:"50%", cy:"50%", r:"45%", pathLength:"100"}) + ); +} + +class SessionLoadView extends TemplateView { + render(t) { + return t.div([ + spinner(t, {hidden: vm => !vm.loading}), + t.p(vm => vm.loadLabel) + ]); + } } diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 5380efbe..176c015e 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -29,28 +29,28 @@ function selectFileAsText(mimeType) { class SessionPickerItemView extends TemplateView { _onDeleteClick() { if (confirm("Are you sure?")) { - this.viewModel.delete(); + this.value.delete(); } } - render(t) { + render(t, vm) { const deleteButton = t.button({ disabled: vm => vm.isDeleting, onClick: this._onDeleteClick.bind(this), }, "Delete"); const clearButton = t.button({ disabled: vm => vm.isClearing, - onClick: () => this.viewModel.clear(), + onClick: () => vm.clear(), }, "Clear"); const exportButton = t.button({ disabled: vm => vm.isClearing, - onClick: () => this.viewModel.export(), + onClick: () => vm.export(), }, "Export"); const downloadExport = t.if(vm => vm.exportDataUrl, t.createTemplate((t, vm) => { return t.a({ href: vm.exportDataUrl, - download: `brawl-session-${this.viewModel.id}.json`, - onClick: () => setTimeout(() => this.viewModel.clearExport(), 100), + download: `brawl-session-${vm.id}.json`, + onClick: () => setTimeout(() => vm.clearExport(), 100), }, "Download"); })); @@ -68,32 +68,24 @@ class SessionPickerItemView extends TemplateView { } export class SessionPickerView extends TemplateView { - mount() { - this._sessionList = new ListView({ - list: this.viewModel.sessions, + render(t, vm) { + const sessionList = new ListView({ + list: vm.sessions, onItemClick: (item, event) => { if (event.target.closest(".userId")) { - this.viewModel.pick(item.viewModel.id); + vm.pick(item.value.id); } }, }, sessionInfo => { return new SessionPickerItemView(sessionInfo); }); - return super.mount(); - } - render(t) { return t.div({className: "SessionPickerView"}, [ t.h1(["Pick a session"]), - this._sessionList.mount(), - t.p(t.button({onClick: () => this.viewModel.cancel()}, ["Log in to a new session instead"])), - t.p(t.button({onClick: async () => this.viewModel.import(await selectFileAsText("application/json"))}, "Import")), + t.view(sessionList), + t.p(t.button({onClick: () => vm.cancel()}, ["Log in to a new session instead"])), + t.p(t.button({onClick: async () => vm.import(await selectFileAsText("application/json"))}, "Import")), t.p(brawlGithubLink(t)) ]); } - - unmount() { - super.unmount(); - this._sessionList.unmount(); - } } diff --git a/src/ui/web/session/RoomTile.js b/src/ui/web/session/RoomTile.js index 38210003..c7f3edd9 100644 --- a/src/ui/web/session/RoomTile.js +++ b/src/ui/web/session/RoomTile.js @@ -10,6 +10,6 @@ export class RoomTile extends TemplateView { // called from ListView clicked() { - this.viewModel.open(); + this.value.open(); } } diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js index 79fbdefe..fa17cc56 100644 --- a/src/ui/web/session/SyncStatusBar.js +++ b/src/ui/web/session/SyncStatusBar.js @@ -1,10 +1,6 @@ import {TemplateView} from "../general/TemplateView.js"; export class SyncStatusBar extends TemplateView { - constructor(vm) { - super(vm, true); - } - render(t, vm) { return t.div({className: { "SyncStatusBar": true, diff --git a/src/ui/web/session/room/MessageComposer.js b/src/ui/web/session/room/MessageComposer.js index 664b246c..13211965 100644 --- a/src/ui/web/session/room/MessageComposer.js +++ b/src/ui/web/session/room/MessageComposer.js @@ -16,7 +16,7 @@ export class MessageComposer extends TemplateView { _onKeyDown(event) { if (event.key === "Enter") { - if (this.viewModel.sendMessage(this._input.value)) { + if (this.value.sendMessage(this._input.value)) { this._input.value = ""; } } diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js index d3d0d849..116de8fd 100644 --- a/src/ui/web/session/room/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -4,7 +4,7 @@ import {MessageComposer} from "./MessageComposer.js"; export class RoomView extends TemplateView { constructor(viewModel) { - super(viewModel, true); + super(viewModel); this._timelineList = null; } @@ -26,7 +26,7 @@ export class RoomView extends TemplateView { } mount() { - this._composer = new MessageComposer(this.viewModel); + this._composer = new MessageComposer(this.value); this._timelineList = new TimelineList(); return super.mount(); } @@ -40,7 +40,7 @@ export class RoomView extends TemplateView { update(value, prop) { super.update(value, prop); if (prop === "timelineViewModel") { - this._timelineList.update({viewModel: this.viewModel.timelineViewModel}); + this._timelineList.update({viewModel: this.value.timelineViewModel}); } } } diff --git a/src/ui/web/session/room/timeline/GapView.js b/src/ui/web/session/room/timeline/GapView.js index 637d3ce3..5687ce58 100644 --- a/src/ui/web/session/room/timeline/GapView.js +++ b/src/ui/web/session/room/timeline/GapView.js @@ -9,7 +9,7 @@ export class GapView extends TemplateView { const label = (vm.isUp ? "🠝" : "🠟") + " fill gap"; //no binding return t.li({className}, [ t.button({ - onClick: () => this.viewModel.fill(), + onClick: () => vm.fill(), disabled: vm => vm.isLoading }, label), t.if(vm => vm.error, t.createTemplate(t => t.strong(vm => vm.error))) From d3f7fb5089a13a9c6e10f7f3c7f7a16b64eea314 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Apr 2020 18:27:59 +0200 Subject: [PATCH 54/93] prototype how i18n would look like --- src/domain/LoginViewModel.js | 17 ++++++++++++++--- src/ui/web/login/LoginView.js | 12 ++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 11d43dc9..544bb980 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -10,9 +10,20 @@ export class LoginViewModel extends EventEmitter { this._loadViewModel = null; } - get usernamePlaceholder() { return "Username"; } - get passwordPlaceholder() { return "Password"; } - get hsPlaceholder() { return "Your matrix homeserver"; } + // TODO: this will need to support binding + // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves + i18n(parts, ...expr) { + // just concat for now + let result = ""; + for (let i = 0; i < parts.length; ++i) { + result = result + parts[i]; + if (i < expr.length) { + result = result + expr[i]; + } + } + return result; + } + get defaultHomeServer() { return this._defaultHomeServer; } get loadViewModel() {return this._loadViewModel; } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 6ba6459c..7baf707d 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -3,12 +3,12 @@ import {brawlGithubLink} from "./common.js"; export class LoginView extends TemplateView { render(t, vm) { - const username = t.input({type: "text", placeholder: vm.usernamePlaceholder, disabled}); - const password = t.input({type: "password", placeholder: vm.passwordPlaceholder, disabled}); - const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled}); const disabled = vm => !!vm.loadViewModel; + const username = t.input({type: "text", placeholder: vm.i18n`Username`, disabled}); + const password = t.input({type: "password", placeholder: vm.i18n`Password`, disabled}); + const homeserver = t.input({type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled}); return t.div({className: "LoginView form"}, [ - t.h1(["Log in to your homeserver"]), + t.h1([vm.i18n`Log in to your homeserver`]), t.if(vm => vm.error, t.createTemplate(t => t.div({className: "error"}, vm => vm.error))), t.div(username), t.div(password), @@ -16,8 +16,8 @@ export class LoginView extends TemplateView { t.div(t.button({ onClick: () => vm.login(username.value, password.value, homeserver.value), disabled - }, "Log In")), - t.div(t.button({onClick: () => vm.goBack(), disabled}, ["Pick an existing session"])), + }, vm.i18n`Log In`)), + t.div(t.button({onClick: () => vm.goBack(), disabled}, [vm.i18n`Pick an existing session`])), t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)), t.p(brawlGithubLink(t)) ]); From d69987b4261607a1503b9a362eda638634a719e6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Apr 2020 18:28:21 +0200 Subject: [PATCH 55/93] make view gallery of views in different states --- index.html | 2 +- src/ui/web/css/main.css | 7 ++++- src/ui/web/css/spinner.css | 34 +++++++++++++++++++++++ src/ui/web/view-gallery.html | 53 ++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/ui/web/css/spinner.css create mode 100644 src/ui/web/view-gallery.html diff --git a/index.html b/index.html index 7778f94c..ae62da91 100644 --- a/index.html +++ b/index.html @@ -10,7 +10,7 @@ - + diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index e01e3fc6..b2d3af04 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -3,8 +3,9 @@ @import url('room.css'); @import url('timeline.css'); @import url('avatar.css'); +@import url('spinner.css'); -body { +.brawl { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; @@ -12,6 +13,10 @@ body { color: white; } +.hidden { + visibility: hidden; +} + .SyncStatusBar { background-color: #555; display: none; diff --git a/src/ui/web/css/spinner.css b/src/ui/web/css/spinner.css new file mode 100644 index 00000000..39b0e6be --- /dev/null +++ b/src/ui/web/css/spinner.css @@ -0,0 +1,34 @@ +@keyframes spinner { + 0% { + transform: rotate(0); + stroke-dasharray: 0 0 10 90; + } + 45% { + stroke-dasharray: 0 0 90 10; + } + 75% { + stroke-dasharray: 0 50 50 0; + } + 100% { + transform: rotate(360deg); + stroke-dasharray: 10 90 0 0; + } +} + +.spinner circle { + transform-origin: 50% 50%; + animation-name: spinner; + animation-duration: 2s; + animation-iteration-count: infinite; + animation-timing-function: linear; + fill: none; + stroke: currentcolor; + stroke-width: calc(var(--size) * 0.1); + stroke-linecap: butt; +} + +.spinner { + --size: 20px; + width: var(--size); + height: var(--size); +} diff --git a/src/ui/web/view-gallery.html b/src/ui/web/view-gallery.html new file mode 100644 index 00000000..ac8d8f19 --- /dev/null +++ b/src/ui/web/view-gallery.html @@ -0,0 +1,53 @@ + + + + + + + + +

View Gallery

+

Login

+
+ +

Login Loading

+
+ + + + From 34549a2ecbcb3e06f37ef210259158f287e13faa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Apr 2020 18:28:35 +0200 Subject: [PATCH 56/93] css notes --- doc/CSS.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 doc/CSS.md diff --git a/doc/CSS.md b/doc/CSS.md new file mode 100644 index 00000000..7365ec5b --- /dev/null +++ b/doc/CSS.md @@ -0,0 +1,83 @@ +https://nio.chat/ looks nice. + +We could do top to bottom gradients in default avatars to make them look a bit cooler. Automatically generate them from a single color, e.g. from slightly lighter to slightly darker. + +## How to organize the CSS? + +Can take ideas/adopt from OOCSS and SMACSS. + +### Root + - maybe we should not assume `body` is the root, but rather a `.brawl` class. The root is where we'd set root level css variables, fonts?, etc. Should we scope all css to this root class? That could get painful with just vanilla css. We could use something like https://github.com/domwashburn/postcss-parent-selector to only do this at build time. Other useful plugin for postcss: https://github.com/postcss/postcss-selector-parser + +We would still you `rem` for size units though. + +### Class names + +#### View + - view name? + +#### Not quite a View + +Some things might not be a view, as they don't have their own view model. + + - a spinner, has .spinner for now + - avatar + +#### modifier classes + +are these modifiers? + - contrast-hi, contrast-mid, contrast-low + - font-large, font-medium, font-small + + - large, medium, small (for spinner and avatar) + - hidden: hides the element, can be useful when not wanting to use an if-binding to databind on a css class + - inline: can be applied to any item if it needs to look good in an inline layout + - flex: can be applied to any item if it is placed in a flex container. You'd combine this with some other class to set a `flex` that makes sense, e.g.: +```css +.spinner.flex, +.avatar.flex, +.icon.flex, +button.flex { + flex: 0; +} +``` +you could end up with a lot of these though? + +well... for flex we don't really need a class, as `flex` doesn't do anything if the parent is not a flex container. + +Modifier classes can be useful though. Should we prefix them? + +### Theming + +do we want as system with HSL or RGBA to define shades and contrasts? + +we could define colors as HS and have a separate value for L: + +``` +/* for dark theme */ +--lightness-mod: -1; +--accent-shade: 310, 70%; +/* then at every level */ +--lightness: 60%; +/* add/remove (based on dark theme) 20% lightness */ +--lightness: calc(var(--lightness) + calc(var(--lightness-mod) * 20%)); +--bg-color: hsl(var(-accent-shade), var(--lightness)); +``` + +this makes it easy to derive colors, but if there is no override with rga values, could be limiting. +I guess --fg-color and --bg-color can be those overrides? + +what theme color variables do we want? + + - accent color + - avatar/name colors + - background color (panels are shades of this?) + +Themes are specified as JSON and need javascript to be set. The JSON contains colors in rgb, the theme code will generate css variables containing shades as specified? Well, that could be custom theming, but built-in themes should have full css flexibility. + +what hierarchical variables do we want? + + - `--fg-color` (we use this instead of color so icons and borders can also take the color, we could use the `currentcolor` constant for this though!) + - `--bg-color` (we use this instead of background so icons and borders can also take the color) + - `--lightness` + - `--size` for things like spinner, avatar From d91ab5355cca3f90e13fe2ef8360ffc1653e533d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Sun, 3 May 2020 01:08:53 +0200 Subject: [PATCH 57/93] support svg namespace, fix spinner --- src/ui/web/common.js | 5 +++++ src/ui/web/css/login.css | 12 ++++++++++++ src/ui/web/css/main.css | 1 + src/ui/web/general/TemplateView.js | 19 +++++++++++++------ src/ui/web/general/html.js | 29 +++++++++++++++++++++-------- src/ui/web/login/LoginView.js | 2 +- 6 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 src/ui/web/common.js create mode 100644 src/ui/web/css/login.css diff --git a/src/ui/web/common.js b/src/ui/web/common.js new file mode 100644 index 00000000..4ef5cc6f --- /dev/null +++ b/src/ui/web/common.js @@ -0,0 +1,5 @@ +export function spinner(t, extraClasses = undefined) { + return t.svg({className: Object.assign({"spinner": true}, extraClasses), viewBox:"0 0 100% 100%"}, + t.circle({cx:"50%", cy:"50%", r:"45%", pathLength:"100"}) + ); +} \ No newline at end of file diff --git a/src/ui/web/css/login.css b/src/ui/web/css/login.css new file mode 100644 index 00000000..3a12f849 --- /dev/null +++ b/src/ui/web/css/login.css @@ -0,0 +1,12 @@ +.SessionLoadView { + display: flex; +} + +.SessionLoadView p { + flex: 1; + margin: 0 0 0 10px; +} + +.SessionLoadView .spinner { + --size: 20px; +} diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index b2d3af04..085bd479 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -1,4 +1,5 @@ @import url('layout.css'); +@import url('login.css'); @import url('left-panel.css'); @import url('room.css'); @import url('timeline.css'); diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 31043e54..71d0ee8b 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -1,4 +1,4 @@ -import { setAttribute, text, isChildren, classNames, TAG_NAMES } from "./html.js"; +import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js"; import {errorToDOM} from "./error.js"; function objHasFns(obj) { @@ -244,12 +244,16 @@ class TemplateBuilder { } el(name, attributes, children) { + return this.elNS(HTML_NS, name, attributes, children); + } + + elNS(ns, name, attributes, children) { if (attributes && isChildren(attributes)) { children = attributes; attributes = null; } - const node = document.createElement(name); + const node = document.createElementNS(ns, name); if (attributes) { this._setNodeAttributes(node, attributes); @@ -300,8 +304,11 @@ class TemplateBuilder { } } -for (const tag of TAG_NAMES) { - TemplateBuilder.prototype[tag] = function(attributes, children) { - return this.el(tag, attributes, children); - }; + +for (const [ns, tags] of Object.entries(TAG_NAMES)) { + for (const tag of tags) { + TemplateBuilder.prototype[tag] = function(attributes, children) { + return this.elNS(ns, tag, attributes, children); + }; + } } diff --git a/src/ui/web/general/html.js b/src/ui/web/general/html.js index 24f34ff4..7cb001c0 100644 --- a/src/ui/web/general/html.js +++ b/src/ui/web/general/html.js @@ -33,12 +33,16 @@ export function setAttribute(el, name, value) { } export function el(elementName, attributes, children) { + return elNS(HTML_NS, elementName, attributes, children); +} + +export function elNS(ns, elementName, attributes, children) { if (attributes && isChildren(attributes)) { children = attributes; attributes = null; } - const e = document.createElement(elementName); + const e = document.createElementNS(ns, elementName); if (attributes) { for (let [name, value] of Object.entries(attributes)) { @@ -67,15 +71,24 @@ export function text(str) { return document.createTextNode(str); } -export const TAG_NAMES = [ - "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "strong", "em", "span", "img", "section", "main", "article", "aside", - "pre", "button", "time", "input", "textarea", "svg", "circle"]; +export const HTML_NS = "http://www.w3.org/1999/xhtml"; +export const SVG_NS = "http://www.w3.org/2000/svg"; + +export const TAG_NAMES = { + [HTML_NS]: [ + "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", + "p", "strong", "em", "span", "img", "section", "main", "article", "aside", + "pre", "button", "time", "input", "textarea"], + [SVG_NS]: ["svg", "circle"] +}; export const tag = {}; -for (const tagName of TAG_NAMES) { - tag[tagName] = function(attributes, children) { - return el(tagName, attributes, children); + +for (const [ns, tags] of Object.entries(TAG_NAMES)) { + for (const tagName of tags) { + tag[tagName] = function(attributes, children) { + return elNS(ns, tagName, attributes, children); + } } } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 7baf707d..cf1fab36 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -32,7 +32,7 @@ function spinner(t, extraClasses = undefined) { class SessionLoadView extends TemplateView { render(t) { - return t.div([ + return t.div({className: "SessionLoadView"}, [ spinner(t, {hidden: vm => !vm.loading}), t.p(vm => vm.loadLabel) ]); From cc87e35f23d936234a9ca1175b65ba76267a0281 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:23:11 +0200 Subject: [PATCH 58/93] use ViewModel super class for all view models that need binding --- src/domain/BrawlViewModel.js | 8 ++--- src/domain/LoginViewModel.js | 22 +++---------- src/domain/SessionLoadViewModel.js | 12 +++---- src/domain/SessionPickerViewModel.js | 26 +++++++-------- src/domain/ViewModel.js | 32 +++++++++++++++++-- src/domain/session/SessionViewModel.js | 19 +++++------ src/domain/session/SyncStatusViewModel.js | 8 ++--- src/domain/session/room/RoomViewModel.js | 23 +++++++------ .../room/timeline/TimelineViewModel.js | 2 +- 9 files changed, 85 insertions(+), 67 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 7ecf9f56..16e5a622 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,9 +1,9 @@ import {SessionViewModel} from "./session/SessionViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; -import {EventEmitter} from "../utils/EventEmitter.js"; +import {ViewModel} from "./ViewModel.js"; -export class BrawlViewModel extends EventEmitter { +export class BrawlViewModel extends ViewModel { constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) { super(); this._createSessionContainer = createSessionContainer; @@ -32,7 +32,7 @@ export class BrawlViewModel extends EventEmitter { if (sessionContainer) { this._setSection(() => { this._sessionContainer = sessionContainer; - this._sessionViewModel = new SessionViewModel(sessionContainer); + this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer})); }); } else { // switch between picker and login @@ -96,7 +96,7 @@ export class BrawlViewModel extends EventEmitter { } // now set it again setter(); - this.emit("change", "activeSection"); + this.emitChange("activeSection"); } get error() { return this._error; } diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 544bb980..3f70108f 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,7 +1,7 @@ -import {EventEmitter} from "../utils/EventEmitter.js"; +import {ViewModel} from "./ViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -export class LoginViewModel extends EventEmitter { +export class LoginViewModel extends ViewModel { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { super(); this._createSessionContainer = createSessionContainer; @@ -10,20 +10,6 @@ export class LoginViewModel extends EventEmitter { this._loadViewModel = null; } - // TODO: this will need to support binding - // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves - i18n(parts, ...expr) { - // just concat for now - let result = ""; - for (let i = 0; i < parts.length; ++i) { - result = result + parts[i]; - if (i < expr.length) { - result = result + expr[i]; - } - } - return result; - } - get defaultHomeServer() { return this._defaultHomeServer; } get loadViewModel() {return this._loadViewModel; } @@ -42,14 +28,14 @@ export class LoginViewModel extends EventEmitter { } else { // show list of session again this._loadViewModel = null; - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } }, deleteSessionOnCancel: true, homeserver, }); this._loadViewModel.start(); - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } cancel() { diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 659abeea..d40b7f08 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -1,8 +1,8 @@ -import {EventEmitter} from "../utils/EventEmitter.js"; import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; +import {ViewModel} from "./ViewModel.js"; -export class SessionLoadViewModel extends EventEmitter { +export class SessionLoadViewModel extends ViewModel { constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) { super(); this._createAndStartSessionContainer = createAndStartSessionContainer; @@ -19,10 +19,10 @@ export class SessionLoadViewModel extends EventEmitter { } try { this._loading = true; - this.emit("change"); + this.emitChange(); this._sessionContainer = this._createAndStartSessionContainer(); this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change"); + this.emitChange(); // wait for initial sync, but not catchup sync const isCatchupSync = s === LoadStatus.FirstSync && this._sessionContainer.sync.status === SyncStatus.CatchupSync; @@ -50,7 +50,7 @@ export class SessionLoadViewModel extends EventEmitter { this._error = err; } finally { this._loading = false; - this.emit("change"); + this.emitChange(); } } @@ -72,7 +72,7 @@ export class SessionLoadViewModel extends EventEmitter { this._sessionCallback(); } catch (err) { this._error = err; - this.emit("change"); + this.emitChange(); } } diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 8274106a..cc4416b5 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,8 +1,8 @@ import {SortedArray} from "../observable/index.js"; -import {EventEmitter} from "../utils/EventEmitter.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; +import {ViewModel} from "./ViewModel.js"; -class SessionItemViewModel extends EventEmitter { +class SessionItemViewModel extends ViewModel { constructor(sessionInfo, pickerVM) { super(); this._pickerVM = pickerVM; @@ -19,31 +19,31 @@ class SessionItemViewModel extends EventEmitter { async delete() { this._isDeleting = true; - this.emit("change", "isDeleting"); + this.emitChange("isDeleting"); try { await this._pickerVM.delete(this.id); } catch(err) { this._error = err; console.error(err); - this.emit("change", "error"); + this.emitChange("error"); } finally { this._isDeleting = false; - this.emit("change", "isDeleting"); + this.emitChange("isDeleting"); } } async clear() { this._isClearing = true; - this.emit("change"); + this.emitChange(); try { await this._pickerVM.clear(this.id); } catch(err) { this._error = err; console.error(err); - this.emit("change", "error"); + this.emitChange("error"); } finally { this._isClearing = false; - this.emit("change", "isClearing"); + this.emitChange("isClearing"); } } @@ -82,7 +82,7 @@ class SessionItemViewModel extends EventEmitter { const json = JSON.stringify(data, undefined, 2); const blob = new Blob([json], {type: "application/json"}); this._exportDataUrl = URL.createObjectURL(blob); - this.emit("change", "exportDataUrl"); + this.emitChange("exportDataUrl"); } catch (err) { alert(err.message); console.error(err); @@ -93,13 +93,13 @@ class SessionItemViewModel extends EventEmitter { if (this._exportDataUrl) { URL.revokeObjectURL(this._exportDataUrl); this._exportDataUrl = null; - this.emit("change", "exportDataUrl"); + this.emitChange("exportDataUrl"); } } } -export class SessionPickerViewModel extends EventEmitter { +export class SessionPickerViewModel extends ViewModel { constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { super(); this._storageFactory = storageFactory; @@ -141,12 +141,12 @@ export class SessionPickerViewModel extends EventEmitter { } else { // show list of session again this._loadViewModel = null; - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } } }); this._loadViewModel.start(); - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } } diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index cc4a6fff..c49e0ef6 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -2,10 +2,13 @@ // as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down) // we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter -export class ViewModel extends ObservableValue { +import {EventEmitter} from "../utils/EventEmitter.js"; +import {Disposables} from "../utils/Disposables.js"; + +export class ViewModel extends EventEmitter { constructor(options) { super(); - this.disposables = new Disposables(); + this.disposables = null; this._options = options; } @@ -14,10 +17,33 @@ export class ViewModel extends ObservableValue { } track(disposable) { + if (!this.disposables) { + this.disposables = new Disposables(); + } this.disposables.track(disposable); } dispose() { - this.disposables.dispose(); + if (this.disposables) { + this.disposables.dispose(); + } + } + + // TODO: this will need to support binding + // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves + i18n(parts, ...expr) { + // just concat for now + let result = ""; + for (let i = 0; i < parts.length; ++i) { + result = result + parts[i]; + if (i < expr.length) { + result = result + expr[i]; + } + } + return result; + } + + emitChange(changedProps) { + this.emit("change", changedProps); } } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 4f4e0b57..9482b40a 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,13 +1,14 @@ -import {EventEmitter} from "../../utils/EventEmitter.js"; import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; +import {ViewModel} from "../ViewModel.js"; -export class SessionViewModel extends EventEmitter { - constructor(sessionContainer) { - super(); +export class SessionViewModel extends ViewModel { + constructor(options) { + super(options); + const sessionContainer = options.sessionContainer; this._session = sessionContainer.session; - this._syncStatusViewModel = new SyncStatusViewModel(sessionContainer.sync); + this._syncStatusViewModel = new SyncStatusViewModel(this.childOptions(sessionContainer.sync)); this._currentRoomViewModel = null; const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { return new RoomTileViewModel({ @@ -42,7 +43,7 @@ export class SessionViewModel extends EventEmitter { if (this._currentRoomViewModel) { this._currentRoomViewModel.dispose(); this._currentRoomViewModel = null; - this.emit("change", "currentRoom"); + this.emitChange("currentRoom"); } } @@ -50,13 +51,13 @@ export class SessionViewModel extends EventEmitter { if (this._currentRoomViewModel) { this._currentRoomViewModel.dispose(); } - this._currentRoomViewModel = new RoomViewModel({ + this._currentRoomViewModel = new RoomViewModel(this.childOptions({ room, ownUserId: this._session.user.id, closeCallback: () => this._closeCurrentRoom(), - }); + })); this._currentRoomViewModel.load(); - this.emit("change", "currentRoom"); + this.emitChange("currentRoom"); } } diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js index 7d26ee11..9c5b5010 100644 --- a/src/domain/session/SyncStatusViewModel.js +++ b/src/domain/session/SyncStatusViewModel.js @@ -1,6 +1,6 @@ -import {EventEmitter} from "../../utils/EventEmitter.js"; +import {ViewModel} from "../ViewModel.js"; -export class SyncStatusViewModel extends EventEmitter { +export class SyncStatusViewModel extends ViewModel { constructor(sync) { super(); this._sync = sync; @@ -13,7 +13,7 @@ export class SyncStatusViewModel extends EventEmitter { } else if (status === "started") { this._error = null; } - this.emit("change"); + this.emitChange(); } onFirstSubscriptionAdded(name) { @@ -30,7 +30,7 @@ export class SyncStatusViewModel extends EventEmitter { trySync() { this._sync.start(); - this.emit("change"); + this.emitChange(); } get status() { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 074070bc..fccc665f 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -1,10 +1,11 @@ -import {EventEmitter} from "../../../utils/EventEmitter.js"; import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {avatarInitials} from "../avatar.js"; +import {ViewModel} from "../../ViewModel.js"; -export class RoomViewModel extends EventEmitter { - constructor({room, ownUserId, closeCallback}) { - super(); +export class RoomViewModel extends ViewModel { + constructor(options) { + super(options); + const {room, ownUserId, closeCallback} = options; this._room = room; this._ownUserId = ownUserId; this._timeline = null; @@ -19,12 +20,16 @@ export class RoomViewModel extends EventEmitter { this._room.on("change", this._onRoomChange); try { this._timeline = await this._room.openTimeline(); - this._timelineVM = new TimelineViewModel(this._room, this._timeline, this._ownUserId); - this.emit("change", "timelineViewModel"); + this._timelineVM = new TimelineViewModel(this.childOptions({ + room: this._room, + timeline: this._timeline, + ownUserId: this._ownUserId, + })); + this.emitChange("timelineViewModel"); } catch (err) { console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`); this._timelineError = err; - this.emit("change", "error"); + this.emitChange("error"); } } @@ -43,7 +48,7 @@ export class RoomViewModel extends EventEmitter { // room doesn't tell us yet which fields changed, // so emit all fields originating from summary _onRoomChange() { - this.emit("change", "name"); + this.emitChange("name"); } get name() { @@ -76,7 +81,7 @@ export class RoomViewModel extends EventEmitter { console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); this._sendError = err; this._timelineError = null; - this.emit("change", "error"); + this.emitChange("error"); return false; } return true; diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 1c19c15f..4550d7bd 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -18,7 +18,7 @@ import {TilesCollection} from "./TilesCollection.js"; import {tilesCreator} from "./tilesCreator.js"; export class TimelineViewModel { - constructor(room, timeline, ownUserId) { + constructor({room, timeline, ownUserId}) { this._timeline = timeline; // once we support sending messages we could do // timeline.entries.concat(timeline.pendingEvents) From 1ef564bdb08fe3528ee635a8c5d64cfacdb1d275 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:24:20 +0200 Subject: [PATCH 59/93] cleanup --- src/ui/web/login/LoginView.js | 7 +------ src/utils/Disposables.js | 1 - 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index cf1fab36..110476c2 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -1,5 +1,6 @@ import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; +import {spinner} from "../common.js"; export class LoginView extends TemplateView { render(t, vm) { @@ -24,12 +25,6 @@ export class LoginView extends TemplateView { } } -function spinner(t, extraClasses = undefined) { - return t.svg({className: Object.assign({"spinner": true}, extraClasses), viewBox:"0 0 100% 100%"}, - t.circle({cx:"50%", cy:"50%", r:"45%", pathLength:"100"}) - ); -} - class SessionLoadView extends TemplateView { render(t) { return t.div({className: "SessionLoadView"}, [ diff --git a/src/utils/Disposables.js b/src/utils/Disposables.js index 739fbe8b..8d90b8c6 100644 --- a/src/utils/Disposables.js +++ b/src/utils/Disposables.js @@ -24,7 +24,6 @@ export class Disposables { } } - disposeTracked(value) { const idx = this._disposables.indexOf(value); if (idx !== -1) { From 3dde23fc4b1ff0c68dde3311faa7976f7a59e49a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:24:27 +0200 Subject: [PATCH 60/93] dispose viewmodels from TemplateView --- src/ui/web/general/TemplateView.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 71d0ee8b..01480668 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -103,6 +103,9 @@ export class TemplateView { v.unmount(); } } + if (typeof this._value.dispose === "function") { + this._value.dispose(); + } } root() { From 225fe873be6a7498daf74bf3573b493dd35d2785 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:38:03 +0200 Subject: [PATCH 61/93] show session load view when picking a session --- src/ui/web/login/LoginView.js | 10 +--------- src/ui/web/login/SessionLoadView.js | 11 +++++++++++ src/ui/web/login/SessionPickerView.js | 2 ++ 3 files changed, 14 insertions(+), 9 deletions(-) create mode 100644 src/ui/web/login/SessionLoadView.js diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index 110476c2..e361f1a6 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -1,6 +1,6 @@ import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; -import {spinner} from "../common.js"; +import {SessionLoadView} from "./SessionLoadView.js"; export class LoginView extends TemplateView { render(t, vm) { @@ -25,11 +25,3 @@ export class LoginView extends TemplateView { } } -class SessionLoadView extends TemplateView { - render(t) { - return t.div({className: "SessionLoadView"}, [ - spinner(t, {hidden: vm => !vm.loading}), - t.p(vm => vm.loadLabel) - ]); - } -} diff --git a/src/ui/web/login/SessionLoadView.js b/src/ui/web/login/SessionLoadView.js new file mode 100644 index 00000000..a7a29df8 --- /dev/null +++ b/src/ui/web/login/SessionLoadView.js @@ -0,0 +1,11 @@ +import {TemplateView} from "../general/TemplateView.js"; +import {spinner} from "../common.js"; + +export class SessionLoadView extends TemplateView { + render(t) { + return t.div({className: "SessionLoadView"}, [ + spinner(t, {hidden: vm => !vm.loading}), + t.p(vm => vm.loadLabel) + ]); + } +} diff --git a/src/ui/web/login/SessionPickerView.js b/src/ui/web/login/SessionPickerView.js index 176c015e..d81247ac 100644 --- a/src/ui/web/login/SessionPickerView.js +++ b/src/ui/web/login/SessionPickerView.js @@ -1,6 +1,7 @@ import {ListView} from "../general/ListView.js"; import {TemplateView} from "../general/TemplateView.js"; import {brawlGithubLink} from "./common.js"; +import {SessionLoadView} from "./SessionLoadView.js"; function selectFileAsText(mimeType) { const input = document.createElement("input"); @@ -85,6 +86,7 @@ export class SessionPickerView extends TemplateView { t.view(sessionList), t.p(t.button({onClick: () => vm.cancel()}, ["Log in to a new session instead"])), t.p(t.button({onClick: async () => vm.import(await selectFileAsText("application/json"))}, "Import")), + t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)), t.p(brawlGithubLink(t)) ]); } From 1fa14a99e99ac1805081f1aed8b387f09b97d92b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:38:23 +0200 Subject: [PATCH 62/93] correctly wait for catchup sync --- src/domain/SessionLoadViewModel.js | 2 +- src/matrix/SessionContainer.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index d40b7f08..10f4f7ee 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -25,7 +25,7 @@ export class SessionLoadViewModel extends ViewModel { this.emitChange(); // wait for initial sync, but not catchup sync const isCatchupSync = s === LoadStatus.FirstSync && - this._sessionContainer.sync.status === SyncStatus.CatchupSync; + this._sessionContainer.sync.status.get() === SyncStatus.CatchupSync; return isCatchupSync || s === LoadStatus.LoginFailed || s === LoadStatus.Error || diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 5468ba70..c9d8af90 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -159,8 +159,8 @@ export class SessionContainer { async _waitForFirstSync() { try { - this._status.set(LoadStatus.FirstSync); this._sync.start(); + this._status.set(LoadStatus.FirstSync); } catch (err) { // swallow ConnectionError here and continue, // as the reconnector above will call From 28bed56b5a085087a233ea3c491612552a0a1901 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 22:21:56 +0200 Subject: [PATCH 63/93] prevent closing more than once --- src/matrix/room/timeline/Timeline.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index c6321876..95a39022 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -60,6 +60,9 @@ export class Timeline { /** @public */ close() { - this._closeCallback(); + if (this._closeCallback) { + this._closeCallback(); + this._closeCallback = null; + } } } From d7a8b1616a2cccf318f7292738d4e0e7599da8b0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 22:23:43 +0200 Subject: [PATCH 64/93] use t.view for room sub views also move composer to own vm --- src/domain/session/room/RoomViewModel.js | 19 ++++++++++++++++++- src/ui/web/session/room/RoomView.js | 17 +++-------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index fccc665f..2d585a0d 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -14,6 +14,7 @@ export class RoomViewModel extends ViewModel { this._timelineError = null; this._sendError = null; this._closeCallback = closeCallback; + this._composerVM = new ComposerViewModel(this); } async load() { @@ -73,7 +74,9 @@ export class RoomViewModel extends ViewModel { return avatarInitials(this._room.name); } - async sendMessage(message) { + + + async _sendMessage(message) { if (message) { try { await this._room.sendEvent("m.room.message", {msgtype: "m.text", body: message}); @@ -88,4 +91,18 @@ export class RoomViewModel extends ViewModel { } return false; } + + get composerViewModel() { + return this._composerVM; + } +} + +class ComposerViewModel { + constructor(roomVM) { + this._roomVM = roomVM; + } + + sendMessage(message) { + return this._roomVM._sendMessage(message); + } } diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js index 116de8fd..fbf6bd10 100644 --- a/src/ui/web/session/room/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -9,6 +9,7 @@ export class RoomView extends TemplateView { } render(t, vm) { + this._timelineList = new TimelineList(); return t.div({className: "RoomView"}, [ t.div({className: "TimelinePanel"}, [ t.div({className: "RoomHeader"}, [ @@ -19,24 +20,12 @@ export class RoomView extends TemplateView { ]), ]), t.div({className: "RoomView_error"}, vm => vm.error), - this._timelineList.mount(), - this._composer.mount(), + t.view(this._timelineList), + t.view(new MessageComposer(this.value.composerViewModel)), ]) ]); } - mount() { - this._composer = new MessageComposer(this.value); - this._timelineList = new TimelineList(); - return super.mount(); - } - - unmount() { - this._composer.unmount(); - this._timelineList.unmount(); - super.unmount(); - } - update(value, prop) { super.update(value, prop); if (prop === "timelineViewModel") { From d6645cbba9a357294dcf1a6be751b2d070e9c7c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 22:24:10 +0200 Subject: [PATCH 65/93] null guard if list was never set --- src/ui/web/general/ListView.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/web/general/ListView.js b/src/ui/web/general/ListView.js index 80c2394c..18fc524b 100644 --- a/src/ui/web/general/ListView.js +++ b/src/ui/web/general/ListView.js @@ -55,7 +55,9 @@ export class ListView { } unmount() { - this._unloadList(); + if (this._list) { + this._unloadList(); + } } _onClick(event) { From d70a57a7c93f42e0147ee2e026729863a1f4ff46 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 22:24:49 +0200 Subject: [PATCH 66/93] remove support for observablevalues from templateview it's not used, and so params haven't been standardized trying to unify it, it messes up overriding update in RoomView that extends it to set the timelineViewModel upon update. --- src/ui/web/general/TemplateView.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index 01480668..e557e2b3 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -40,14 +40,10 @@ export class TemplateView { } _subscribe() { - this._boundUpdateFromValue = this._updateFromValue.bind(this); - if (typeof this._value.on === "function") { + this._boundUpdateFromValue = this._updateFromValue.bind(this); this._value.on("change", this._boundUpdateFromValue); } - else if (typeof this._value.subscribe === "function") { - this._value.subscribe(this._boundUpdateFromValue); - } } _unsubscribe() { @@ -55,9 +51,6 @@ export class TemplateView { if (typeof this._value.off === "function") { this._value.off("change", this._boundUpdateFromValue); } - else if (typeof this._value.unsubscribe === "function") { - this._value.unsubscribe(this._boundUpdateFromValue); - } this._boundUpdateFromValue = null; } } @@ -103,17 +96,14 @@ export class TemplateView { v.unmount(); } } - if (typeof this._value.dispose === "function") { - this._value.dispose(); - } } root() { return this._root; } - _updateFromValue() { - this.update(this._value); + _updateFromValue(changedProps) { + this.update(this._value, changedProps); } update(value) { From f90b435362310d456a706bcff51626a98a583ebd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 23:42:34 +0200 Subject: [PATCH 67/93] No need for Offline state if we're always in Waiting really --- src/matrix/net/Reconnector.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index 0489b963..d394cb01 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -4,7 +4,6 @@ import {ConnectionError} from "../error.js" import {ObservableValue} from "../../observable/ObservableValue.js"; export const ConnectionStatus = createEnum( - "Offline", "Waiting", "Reconnecting", "Online" @@ -37,9 +36,7 @@ export class Reconnector { } async onRequestFailed(hsApi) { - if (!this._isReconnecting) { - this._setState(ConnectionStatus.Offline); - + if (!this._isReconnecting) { const onlineStatusSubscription = this._onlineStatus && this._onlineStatus.subscribe(online => { if (online) { this.tryNow(); @@ -149,7 +146,6 @@ export function tests() { clock.elapse(2000); await connectionStatus.waitFor(s => s === ConnectionStatus.Online).promise; assert.deepEqual(statuses, [ - ConnectionStatus.Offline, ConnectionStatus.Reconnecting, ConnectionStatus.Waiting, ConnectionStatus.Reconnecting, From 4ffaa8243593d5a24ed1c3bece79bc0bd5b2c037 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:12:14 +0200 Subject: [PATCH 68/93] add interval to clock --- src/mocks/Clock.js | 42 +++++++++++++++++++++++++++++++++++++++++ src/ui/web/dom/Clock.js | 22 +++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/mocks/Clock.js b/src/mocks/Clock.js index fd49b313..6bbf5a1c 100644 --- a/src/mocks/Clock.js +++ b/src/mocks/Clock.js @@ -18,6 +18,34 @@ class Timeout { } } +class Interval { + constructor(elapsed, ms, callback) { + this._start = elapsed.get(); + this._last = this._start; + this._interval = ms; + this._callback = callback; + this._subscription = elapsed.subscribe(this._update.bind(this)); + } + + _update(elapsed) { + const prevAmount = Math.floor((this._last - this._start) / this._interval); + const newAmount = Math.floor((elapsed - this._start) / this._interval); + const amountDiff = Math.max(0, newAmount - prevAmount); + this._last = elapsed; + + for (let i = 0; i < amountDiff; ++i) { + this._callback(); + } + } + + dispose() { + if (this._subscription) { + this._subscription(); + this._subscription = null; + } + } +} + class TimeMeasure { constructor(elapsed) { this._elapsed = elapsed; @@ -47,6 +75,10 @@ export class Clock { return new Timeout(this._elapsed, ms); } + createInterval(callback, ms) { + return new Interval(this._elapsed, ms, callback); + } + now() { return this._baseTimestamp + this.elapsed; } @@ -72,6 +104,16 @@ export function tests() { const promise = timeout.elapsed(); assert(promise instanceof Promise); await promise; + }, + "test interval": assert => { + const clock = new Clock(); + let counter = 0; + const interval = clock.createInterval(() => counter += 1, 200); + clock.elapse(150); + assert.strictEqual(counter, 0); + clock.elapse(500); + assert.strictEqual(counter, 3); + interval.dispose(); } } } diff --git a/src/ui/web/dom/Clock.js b/src/ui/web/dom/Clock.js index 8a10499a..f5cc6ed0 100644 --- a/src/ui/web/dom/Clock.js +++ b/src/ui/web/dom/Clock.js @@ -25,8 +25,26 @@ class Timeout { this._reject = null; } } + + dispose() { + this.abort(); + } } +class Interval { + constructor(ms, callback) { + this._handle = setInterval(callback, ms); + } + + dispose() { + if (this._handle) { + clearInterval(this._handle); + this._handle = null; + } + } +} + + class TimeMeasure { constructor() { this._start = window.performance.now(); @@ -46,6 +64,10 @@ export class Clock { return new Timeout(ms); } + createInterval(callback, ms) { + return new Interval(ms, callback); + } + now() { return Date.now(); } From 4de29779c708f7db4a6387003f7cc2aa78e6e4cc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:12:46 +0200 Subject: [PATCH 69/93] also abort timeout timer when response is rejected --- src/matrix/net/HomeServerApi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 65deff2e..4ac7f9e2 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -78,7 +78,8 @@ export class HomeServerApi { () => {} // ignore AbortError ); // abort timeout if request finishes first - requestResult.response().then(() => timeout.abort()); + const abort = () => timeout.abort(); + requestResult.response().then(abort, abort); } const wrapper = new RequestWrapper(method, url, requestResult); From 8e9c76c26bdb3e35ad2d1116ea67d4007a5c121f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:13:05 +0200 Subject: [PATCH 70/93] options is the 5th arg --- src/matrix/net/HomeServerApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.js index 4ac7f9e2..008ee8a9 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.js @@ -136,7 +136,7 @@ export class HomeServerApi { } versions(options = null) { - return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, options); + return this._request("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); } } From 0eefc88fe3261a25a0ab7bd5fa993aeabad2b225 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:13:41 +0200 Subject: [PATCH 71/93] waitForRetry doesn't reject when aborted --- src/matrix/net/Reconnector.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.js index d394cb01..78e5e66f 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.js @@ -92,13 +92,7 @@ export class Reconnector { } catch (err) { if (err instanceof ConnectionError) { this._setState(ConnectionStatus.Waiting); - try { - await this._retryDelay.waitForRetry(); - } catch (err) { - if (!(err instanceof AbortError)) { - throw err; - } - } + await this._retryDelay.waitForRetry(); } else { throw err; } From b0e59c30dd20da0534ef3c09db4ec113020f0b40 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:14:58 +0200 Subject: [PATCH 72/93] correctly pass options to ViewModel ctor --- src/domain/BrawlViewModel.js | 6 +++--- src/domain/SessionLoadViewModel.js | 5 +++-- src/domain/SessionPickerViewModel.js | 7 ++++--- src/domain/ViewModel.js | 19 +++++++++++++++++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 16e5a622..82b085cd 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -4,12 +4,12 @@ import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel.js"; export class BrawlViewModel extends ViewModel { - constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) { - super(); + constructor(options) { + super(options); + const {createSessionContainer, sessionInfoStorage, storageFactory} = options; this._createSessionContainer = createSessionContainer; this._sessionInfoStorage = sessionInfoStorage; this._storageFactory = storageFactory; - this._clock = clock; this._error = null; this._sessionViewModel = null; diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 10f4f7ee..e4a1fc0a 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -3,8 +3,9 @@ import {SyncStatus} from "../matrix/Sync.js"; import {ViewModel} from "./ViewModel.js"; export class SessionLoadViewModel extends ViewModel { - constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) { - super(); + constructor(options) { + super(options); + const {createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel} = options; this._createAndStartSessionContainer = createAndStartSessionContainer; this._sessionCallback = sessionCallback; this._homeserver = homeserver; diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index cc4416b5..78cb5bac 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -4,7 +4,7 @@ import {ViewModel} from "./ViewModel.js"; class SessionItemViewModel extends ViewModel { constructor(sessionInfo, pickerVM) { - super(); + super({}); this._pickerVM = pickerVM; this._sessionInfo = sessionInfo; this._isDeleting = false; @@ -100,8 +100,9 @@ class SessionItemViewModel extends ViewModel { export class SessionPickerViewModel extends ViewModel { - constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { - super(); + constructor(options) { + super(options); + const {storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer} = options; this._storageFactory = storageFactory; this._sessionInfoStorage = sessionInfoStorage; this._sessionCallback = sessionCallback; diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index c49e0ef6..7e08f8d3 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -6,10 +6,10 @@ import {EventEmitter} from "../utils/EventEmitter.js"; import {Disposables} from "../utils/Disposables.js"; export class ViewModel extends EventEmitter { - constructor(options) { + constructor({clock} = {}) { super(); this.disposables = null; - this._options = options; + this._options = {clock}; } childOptions(explicitOptions) { @@ -21,6 +21,7 @@ export class ViewModel extends EventEmitter { this.disposables = new Disposables(); } this.disposables.track(disposable); + return disposable; } dispose() { @@ -29,8 +30,18 @@ export class ViewModel extends EventEmitter { } } + disposeTracked(disposable) { + if (this.disposables) { + return this.disposables.disposeTracked(disposable); + } + return null; + } + // TODO: this will need to support binding // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves + // + // translated string should probably always be bindings, unless we're fine with a refresh when changing the language? + // we probably are, if we're using routing with a url, we could just refresh. i18n(parts, ...expr) { // just concat for now let result = ""; @@ -46,4 +57,8 @@ export class ViewModel extends EventEmitter { emitChange(changedProps) { this.emit("change", changedProps); } + + get clock() { + return this._options.clock; + } } From 3adc609e0792775a2b9d9af88eb5704f20c80763 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:16:51 +0200 Subject: [PATCH 73/93] implement session status bar, with feedback on connection status --- src/domain/BrawlViewModel.js | 1 + src/domain/session/SessionStatusViewModel.js | 109 +++++++++++++++++++ src/domain/session/SessionViewModel.js | 13 ++- src/domain/session/SyncStatusViewModel.js | 51 --------- src/matrix/SessionContainer.js | 7 +- src/ui/web/session/SessionStatusView.js | 16 +++ src/ui/web/session/SessionView.js | 6 +- src/ui/web/session/SyncStatusBar.js | 14 --- 8 files changed, 145 insertions(+), 72 deletions(-) create mode 100644 src/domain/session/SessionStatusViewModel.js delete mode 100644 src/domain/session/SyncStatusViewModel.js create mode 100644 src/ui/web/session/SessionStatusView.js delete mode 100644 src/ui/web/session/SyncStatusBar.js diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 82b085cd..2b41b1e7 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -33,6 +33,7 @@ export class BrawlViewModel extends ViewModel { this._setSection(() => { this._sessionContainer = sessionContainer; this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer})); + this._sessionViewModel.start(); }); } else { // switch between picker and login diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js new file mode 100644 index 00000000..c471f6b8 --- /dev/null +++ b/src/domain/session/SessionStatusViewModel.js @@ -0,0 +1,109 @@ +import {ViewModel} from "../ViewModel.js"; +import {createEnum} from "../../utils/enum.js"; +import {ConnectionStatus} from "../../matrix/net/Reconnector.js"; +import {SyncStatus} from "../../matrix/Sync.js"; + +const SessionStatus = createEnum( + "Disconnected", + "Connecting", + "FirstSync", + "Sending", + "Syncing" +); + +export class SessionStatusViewModel extends ViewModel { + constructor(options) { + super(options); + const {syncStatus, reconnector} = options; + this._syncStatus = syncStatus; + this._reconnector = reconnector; + this._status = this._calculateState(reconnector.connectionStatus.get(), syncStatus.get()); + + } + + start() { + const update = () => this._updateStatus(); + this.track(this._syncStatus.subscribe(update)); + this.track(this._reconnector.connectionStatus.subscribe(update)); + } + + get isShown() { + return this._status !== SessionStatus.Syncing; + } + + get statusLabel() { + switch (this._status) { + case SessionStatus.Disconnected:{ + const retryIn = Math.round(this._reconnector.retryIn / 1000); + return this.i18n`Disconnected, trying to reconnect in ${retryIn}s…`; + } + case SessionStatus.Connecting: + return this.i18n`Trying to reconnect now…`; + case SessionStatus.FirstSync: + return this.i18n`Catching up with your conversations…`; + } + return ""; + } + + get isWaiting() { + switch (this._status) { + case SessionStatus.Connecting: + case SessionStatus.FirstSync: + return true; + default: + return false; + } + } + + _updateStatus() { + const newStatus = this._calculateState( + this._reconnector.connectionStatus.get(), + this._syncStatus.get() + ); + if (newStatus !== this._status) { + if (newStatus === SessionStatus.Disconnected) { + this._retryTimer = this.track(this.clock.createInterval(() => { + this.emitChange("statusLabel"); + }, 1000)); + } else { + this._retryTimer = this.disposeTracked(this._retryTimer); + } + this._status = newStatus; + console.log("newStatus", newStatus); + this.emitChange(); + } + } + + _calculateState(connectionStatus, syncStatus) { + if (connectionStatus !== ConnectionStatus.Online) { + switch (connectionStatus) { + case ConnectionStatus.Reconnecting: + return SessionStatus.Connecting; + case ConnectionStatus.Waiting: + return SessionStatus.Disconnected; + } + } else if (syncStatus !== SyncStatus.Syncing) { + switch (syncStatus) { + // InitialSync should be awaited in the SessionLoadViewModel, + // but include it here anyway + case SyncStatus.InitialSync: + case SyncStatus.CatchupSync: + return SessionStatus.FirstSync; + } + } /* else if (session.pendingMessageCount) { + return SessionStatus.Sending; + } */ else { + return SessionStatus.Syncing; + } + } + + get isConnectNowShown() { + return this._status === SessionStatus.Disconnected; + } + + connectNow() { + if (this.isConnectNowShown) { + this._reconnector.tryNow(); + } + } +} diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 9482b40a..900e2965 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -8,7 +8,10 @@ export class SessionViewModel extends ViewModel { super(options); const sessionContainer = options.sessionContainer; this._session = sessionContainer.session; - this._syncStatusViewModel = new SyncStatusViewModel(this.childOptions(sessionContainer.sync)); + this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({ + syncStatus: sessionContainer.sync.status, + reconnector: sessionContainer.reconnector + }))); this._currentRoomViewModel = null; const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { return new RoomTileViewModel({ @@ -20,8 +23,12 @@ export class SessionViewModel extends ViewModel { this._roomList = roomTileVMs.sortValues((a, b) => a.compare(b)); } - get syncStatusViewModel() { - return this._syncStatusViewModel; + start() { + this._sessionStatusViewModel.start(); + } + + get sessionStatusViewModel() { + return this._sessionStatusViewModel; } get roomList() { diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js deleted file mode 100644 index 9c5b5010..00000000 --- a/src/domain/session/SyncStatusViewModel.js +++ /dev/null @@ -1,51 +0,0 @@ -import {ViewModel} from "../ViewModel.js"; - -export class SyncStatusViewModel extends ViewModel { - constructor(sync) { - super(); - this._sync = sync; - this._onStatus = this._onStatus.bind(this); - } - - _onStatus(status, err) { - if (status === "error") { - this._error = err; - } else if (status === "started") { - this._error = null; - } - this.emitChange(); - } - - onFirstSubscriptionAdded(name) { - if (name === "change") { - //this._sync.status.("status", this._onStatus); - } - } - - onLastSubscriptionRemoved(name) { - if (name === "change") { - //this._sync.status.("status", this._onStatus); - } - } - - trySync() { - this._sync.start(); - this.emitChange(); - } - - get status() { - if (!this.isSyncing) { - if (this._error) { - return `Error while syncing: ${this._error.message}`; - } else { - return "Sync stopped"; - } - } else { - return "Sync running"; - } - } - - get isSyncing() { - return this._sync.isSyncing; - } -} diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index c9d8af90..b6b2940a 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -71,7 +71,7 @@ export class SessionContainer { this._status.set(LoadStatus.Login); let sessionInfo; try { - const hsApi = new HomeServerApi({homeServer, request: this._request}); + const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout}); const loginData = await hsApi.passwordLogin(username, password).response(); const sessionId = this.createNewSessionId(); sessionInfo = { @@ -123,6 +123,7 @@ export class SessionContainer { accessToken: sessionInfo.accessToken, request: this._request, reconnector: this._reconnector, + createTimeout: this._clock.createTimeout }); this._sessionId = sessionInfo.id; this._storage = await this._storageFactory.create(sessionInfo.id); @@ -203,6 +204,10 @@ export class SessionContainer { return this._session; } + get reconnector() { + return this._reconnector; + } + stop() { this._reconnectSubscription(); this._reconnectSubscription = null; diff --git a/src/ui/web/session/SessionStatusView.js b/src/ui/web/session/SessionStatusView.js new file mode 100644 index 00000000..84c44dd7 --- /dev/null +++ b/src/ui/web/session/SessionStatusView.js @@ -0,0 +1,16 @@ +import {TemplateView} from "../general/TemplateView.js"; +import {spinner} from "../common.js"; + +export class SessionStatusView extends TemplateView { + render(t, vm) { + return t.div({className: { + "SessionStatusView": true, + "hidden": vm => !vm.isShown, + }}, [ + spinner(t, {hidden: vm => !vm.isWaiting}), + t.p(vm => vm.statusLabel), + t.if(vm => vm.isConnectNowShown, t.createTemplate(t => t.button({onClick: () => vm.connectNow()}, "Retry now"))), + window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : "" + ]); + } +} diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js index 19456fd0..f7925f31 100644 --- a/src/ui/web/session/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -3,7 +3,7 @@ import {RoomTile} from "./RoomTile.js"; import {RoomView} from "./room/RoomView.js"; import {SwitchView} from "../general/SwitchView.js"; import {RoomPlaceholderView} from "./RoomPlaceholderView.js"; -import {SyncStatusBar} from "./SyncStatusBar.js"; +import {SessionStatusView} from "./SessionStatusView.js"; import {tag} from "../general/html.js"; export class SessionView { @@ -22,7 +22,7 @@ export class SessionView { mount() { this._viewModel.on("change", this._onViewModelChange); - this._syncStatusBar = new SyncStatusBar(this._viewModel.syncStatusViewModel); + this._sessionStatusBar = new SessionStatusView(this._viewModel.sessionStatusViewModel); this._roomList = new ListView( { className: "RoomList", @@ -34,7 +34,7 @@ export class SessionView { this._middleSwitcher = new SwitchView(new RoomPlaceholderView()); this._root = tag.div({className: "SessionView"}, [ - this._syncStatusBar.mount(), + this._sessionStatusBar.mount(), tag.div({className: "main"}, [ tag.div({className: "LeftPanel"}, this._roomList.mount()), this._middleSwitcher.mount() diff --git a/src/ui/web/session/SyncStatusBar.js b/src/ui/web/session/SyncStatusBar.js deleted file mode 100644 index fa17cc56..00000000 --- a/src/ui/web/session/SyncStatusBar.js +++ /dev/null @@ -1,14 +0,0 @@ -import {TemplateView} from "../general/TemplateView.js"; - -export class SyncStatusBar extends TemplateView { - render(t, vm) { - return t.div({className: { - "SyncStatusBar": true, - "SyncStatusBar_shown": true, - }}, [ - vm => vm.status, - t.if(vm => !vm.isSyncing, t.createTemplate(t => t.button({onClick: () => vm.trySync()}, "Try syncing"))), - window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : "" - ]); - } -} From c4b17e4be63105cdaad207cf8f448910eed3abbe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:17:27 +0200 Subject: [PATCH 74/93] cleanup SessionViewModel, use ViewModel dispose infrastructure --- src/domain/session/SessionViewModel.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 900e2965..fb80edb8 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,12 +1,12 @@ import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; -import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; +import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {ViewModel} from "../ViewModel.js"; export class SessionViewModel extends ViewModel { constructor(options) { super(options); - const sessionContainer = options.sessionContainer; + const {sessionContainer} = options; this._session = sessionContainer.session; this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({ syncStatus: sessionContainer.sync.status, @@ -39,30 +39,22 @@ export class SessionViewModel extends ViewModel { return this._currentRoomViewModel; } - dispose() { - if (this._currentRoomViewModel) { - this._currentRoomViewModel.dispose(); - this._currentRoomViewModel = null; - } - } - _closeCurrentRoom() { if (this._currentRoomViewModel) { - this._currentRoomViewModel.dispose(); - this._currentRoomViewModel = null; + this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); this.emitChange("currentRoom"); } } _openRoom(room) { if (this._currentRoomViewModel) { - this._currentRoomViewModel.dispose(); + this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); } - this._currentRoomViewModel = new RoomViewModel(this.childOptions({ + this._currentRoomViewModel = this.track(new RoomViewModel(this.childOptions({ room, ownUserId: this._session.user.id, closeCallback: () => this._closeCurrentRoom(), - })); + }))); this._currentRoomViewModel.load(); this.emitChange("currentRoom"); } From 0623c1c665f73c750698e034ea67879d93646fe2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:17:48 +0200 Subject: [PATCH 75/93] subViews is in TemplateView --- src/ui/web/general/TemplateView.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index e557e2b3..ae75abc7 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -281,9 +281,10 @@ class TemplateBuilder { const boolFn = value => !!fn(value); return this._addReplaceNodeBinding(boolFn, (prevNode) => { if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) { - const viewIdx = this._subViews.findIndex(v => v.root() === prevNode); + const subViews = this._templateView._subViews; + const viewIdx = subViews.findIndex(v => v.root() === prevNode); if (viewIdx !== -1) { - const [view] = this._subViews.splice(viewIdx, 1); + const [view] = subViews.splice(viewIdx, 1); view.unmount(); } } From 22821af3467d52f47128c3925f61b5f6deb9c645 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:18:31 +0200 Subject: [PATCH 76/93] css changes for status bar --- src/ui/web/css/main.css | 25 +++++++++++++++++++------ src/ui/web/login/SessionLoadView.js | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ui/web/css/main.css b/src/ui/web/css/main.css index 085bd479..7a7f2f74 100644 --- a/src/ui/web/css/main.css +++ b/src/ui/web/css/main.css @@ -14,19 +14,32 @@ color: white; } -.hidden { +.hiddenWithLayout { visibility: hidden; } -.SyncStatusBar { - background-color: #555; - display: none; +.hidden { + display: none !important; } -.SyncStatusBar_shown { - display: unset; +.SessionStatusView { + display: flex; + padding: 5px; + background-color: #555; } +.SessionStatusView p { + margin: 0 10px; +} + +.SessionStatusView button { + border: none; + background: none; + color: currentcolor; + text-decoration: underline; +} + + .RoomPlaceholderView { display: flex; align-items: center; diff --git a/src/ui/web/login/SessionLoadView.js b/src/ui/web/login/SessionLoadView.js index a7a29df8..5340844b 100644 --- a/src/ui/web/login/SessionLoadView.js +++ b/src/ui/web/login/SessionLoadView.js @@ -4,7 +4,7 @@ import {spinner} from "../common.js"; export class SessionLoadView extends TemplateView { render(t) { return t.div({className: "SessionLoadView"}, [ - spinner(t, {hidden: vm => !vm.loading}), + spinner(t, {hiddenWithLayout: vm => !vm.loading}), t.p(vm => vm.loadLabel) ]); } From d0f09c533488ed3da46bd4d5f3b17fb51b5001f0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:18:44 +0200 Subject: [PATCH 77/93] add status bar to view gallery for design --- src/ui/web/view-gallery.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/ui/web/view-gallery.html b/src/ui/web/view-gallery.html index ac8d8f19..822aa89f 100644 --- a/src/ui/web/view-gallery.html +++ b/src/ui/web/view-gallery.html @@ -24,6 +24,19 @@ }

View Gallery

+

Session Status

+
+

Login

View Gallery

-

Session Status

+

Session Status Bar

Login

From e17fc57d9c72f08e3875ad3214b843e180a0f0a2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 May 2020 23:31:36 +0200 Subject: [PATCH 83/93] fix c/p error in disposables --- src/utils/Disposables.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/Disposables.js b/src/utils/Disposables.js index e1d6b0a5..bd84ccf0 100644 --- a/src/utils/Disposables.js +++ b/src/utils/Disposables.js @@ -1,5 +1,5 @@ function disposeValue(value) { - if (typeof d === "function") { + if (typeof value === "function") { value(); } else { value.dispose(); From 06fc3101e8135e128fec245be3cd73663a4641f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 May 2020 23:44:52 +0200 Subject: [PATCH 84/93] make login view enabled again if load view is not busy anymore --- src/domain/LoginViewModel.js | 21 ++++++++++++++++++++- src/ui/web/general/TemplateView.js | 21 ++++++++++++++------- src/ui/web/login/LoginView.js | 6 +++--- src/utils/EventEmitter.js | 7 +++++++ 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 3f70108f..64558295 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -8,13 +8,26 @@ export class LoginViewModel extends ViewModel { this._sessionCallback = sessionCallback; this._defaultHomeServer = defaultHomeServer; 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.cancel(); + } this._loadViewModel = new SessionLoadViewModel({ createAndStartSessionContainer: () => { const sessionContainer = this._createSessionContainer(); @@ -36,10 +49,16 @@ export class LoginViewModel extends ViewModel { }); 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"); + })); } cancel() { - if (!this._loadViewModel) { + if (!this.isBusy) { this._sessionCallback(); } } diff --git a/src/ui/web/general/TemplateView.js b/src/ui/web/general/TemplateView.js index ae75abc7..8cdccdf1 100644 --- a/src/ui/web/general/TemplateView.js +++ b/src/ui/web/general/TemplateView.js @@ -276,10 +276,9 @@ class TemplateBuilder { return vm => new TemplateView(vm, render); } - // creates a conditional subtemplate - if(fn, viewCreator) { - const boolFn = value => !!fn(value); - return this._addReplaceNodeBinding(boolFn, (prevNode) => { + // map a value to a view, every time the value changes + mapView(mapFn, viewCreator) { + return this._addReplaceNodeBinding(mapFn, (prevNode) => { if (prevNode && prevNode.nodeType !== Node.COMMENT_NODE) { const subViews = this._templateView._subViews; const viewIdx = subViews.findIndex(v => v.root() === prevNode); @@ -288,14 +287,22 @@ class TemplateBuilder { view.unmount(); } } - if (boolFn(this._value)) { - const view = viewCreator(this._value); + const view = viewCreator(mapFn(this._value)); + if (view) { return this.view(view); } else { - return document.createComment("if placeholder"); + return document.createComment("node binding placeholder"); } }); } + + // creates a conditional subtemplate + if(fn, viewCreator) { + return this.mapView( + value => !!fn(value), + enabled => enabled ? viewCreator(this._value) : null + ); + } } diff --git a/src/ui/web/login/LoginView.js b/src/ui/web/login/LoginView.js index e361f1a6..18d99f65 100644 --- a/src/ui/web/login/LoginView.js +++ b/src/ui/web/login/LoginView.js @@ -4,7 +4,7 @@ import {SessionLoadView} from "./SessionLoadView.js"; export class LoginView extends TemplateView { render(t, vm) { - const disabled = vm => !!vm.loadViewModel; + const disabled = vm => !!vm.isBusy; const username = t.input({type: "text", placeholder: vm.i18n`Username`, disabled}); const password = t.input({type: "password", placeholder: vm.i18n`Password`, disabled}); const homeserver = t.input({type: "text", placeholder: vm.i18n`Your matrix homeserver`, value: vm.defaultHomeServer, disabled}); @@ -18,8 +18,8 @@ export class LoginView extends TemplateView { onClick: () => vm.login(username.value, password.value, homeserver.value), disabled }, vm.i18n`Log In`)), - t.div(t.button({onClick: () => vm.goBack(), disabled}, [vm.i18n`Pick an existing session`])), - t.if(vm => vm.loadViewModel, vm => new SessionLoadView(vm.loadViewModel)), + t.div(t.button({onClick: () => vm.cancel(), disabled}, [vm.i18n`Pick an existing session`])), + t.mapView(vm => vm.loadViewModel, loadViewModel => loadViewModel ? new SessionLoadView(loadViewModel) : null), t.p(brawlGithubLink(t)) ]); } diff --git a/src/utils/EventEmitter.js b/src/utils/EventEmitter.js index 94da0cde..04944bc8 100644 --- a/src/utils/EventEmitter.js +++ b/src/utils/EventEmitter.js @@ -12,6 +12,13 @@ export class EventEmitter { } } + disposableOn(name, callback) { + this.on(name, callback); + return () => { + this.off(name, callback); + } + } + on(name, callback) { let handlers = this._handlersByName[name]; if (!handlers) { From cc19063c79dc5281d0248cf405f57175e5eedc2a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 6 May 2020 23:50:12 +0200 Subject: [PATCH 85/93] set timeout on /sync request as long running requests run higher risk of getting wedged --- src/matrix/Sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index 673bed82..b772ad2f 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -83,7 +83,8 @@ export class Sync { // TODO: this should be interruptable by stop, we can reuse _currentRequest syncFilterId = (await this._hsApi.createFilter(this._session.user.id, {room: {state: {lazy_load_members: true}}}).response()).filter_id; } - this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout); + const totalRequestTimeout = timeout + (80 * 1000); // same as riot-web, don't get stuck on wedged long requests + this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout, {timeout: totalRequestTimeout}); const response = await this._currentRequest.response(); syncToken = response.next_batch; const storeNames = this._storage.storeNames; From 52e2d3203e98835b0549430c3adfddb35aeb35f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 00:04:41 +0200 Subject: [PATCH 86/93] also make filter request interruptable --- src/matrix/Sync.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/Sync.js b/src/matrix/Sync.js index b772ad2f..de079e4d 100644 --- a/src/matrix/Sync.js +++ b/src/matrix/Sync.js @@ -80,8 +80,8 @@ export class Sync { async _syncRequest(syncToken, timeout) { let {syncFilterId} = this._session; if (typeof syncFilterId !== "string") { - // TODO: this should be interruptable by stop, we can reuse _currentRequest - syncFilterId = (await this._hsApi.createFilter(this._session.user.id, {room: {state: {lazy_load_members: true}}}).response()).filter_id; + this._currentRequest = this._hsApi.createFilter(this._session.user.id, {room: {state: {lazy_load_members: true}}}); + syncFilterId = (await this._currentRequest.response()).filter_id; } const totalRequestTimeout = timeout + (80 * 1000); // same as riot-web, don't get stuck on wedged long requests this._currentRequest = this._hsApi.sync(syncToken, syncFilterId, timeout, {timeout: totalRequestTimeout}); From b65da9b8a9fef4b0b6caa44356f61bb7f92f17f3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 00:05:21 +0200 Subject: [PATCH 87/93] display sync errors in status bar --- src/domain/session/SessionStatusViewModel.js | 17 +++++++++++------ src/domain/session/SessionViewModel.js | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index c471f6b8..20864255 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -8,22 +8,23 @@ const SessionStatus = createEnum( "Connecting", "FirstSync", "Sending", - "Syncing" + "Syncing", + "SyncError" ); export class SessionStatusViewModel extends ViewModel { constructor(options) { super(options); - const {syncStatus, reconnector} = options; - this._syncStatus = syncStatus; + const {sync, reconnector} = options; + this._sync = sync; this._reconnector = reconnector; - this._status = this._calculateState(reconnector.connectionStatus.get(), syncStatus.get()); + this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); } start() { const update = () => this._updateStatus(); - this.track(this._syncStatus.subscribe(update)); + this.track(this._sync.status.subscribe(update)); this.track(this._reconnector.connectionStatus.subscribe(update)); } @@ -41,6 +42,8 @@ export class SessionStatusViewModel extends ViewModel { return this.i18n`Trying to reconnect now…`; case SessionStatus.FirstSync: return this.i18n`Catching up with your conversations…`; + case SessionStatus.SyncError: + return this.i18n`Sync failed because of ${this._sync.error}`; } return ""; } @@ -58,7 +61,7 @@ export class SessionStatusViewModel extends ViewModel { _updateStatus() { const newStatus = this._calculateState( this._reconnector.connectionStatus.get(), - this._syncStatus.get() + this._sync.status.get() ); if (newStatus !== this._status) { if (newStatus === SessionStatus.Disconnected) { @@ -89,6 +92,8 @@ export class SessionStatusViewModel extends ViewModel { case SyncStatus.InitialSync: case SyncStatus.CatchupSync: return SessionStatus.FirstSync; + case SyncStatus.Stopped: + return SessionStatus.SyncError; } } /* else if (session.pendingMessageCount) { return SessionStatus.Sending; diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index fb80edb8..f014f362 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -9,7 +9,7 @@ export class SessionViewModel extends ViewModel { const {sessionContainer} = options; this._session = sessionContainer.session; this._sessionStatusViewModel = this.track(new SessionStatusViewModel(this.childOptions({ - syncStatus: sessionContainer.sync.status, + sync: sessionContainer.sync, reconnector: sessionContainer.reconnector }))); this._currentRoomViewModel = null; From 24cb9e3f5c1bdbb13aed97d401027c878740dc4f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 18:46:16 +0200 Subject: [PATCH 88/93] only increment sent counter after successful send otherwise the message doesn't get sent after coming back online --- src/matrix/room/sending/SendQueue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/sending/SendQueue.js b/src/matrix/room/sending/SendQueue.js index 958901e4..d4ee9828 100644 --- a/src/matrix/room/sending/SendQueue.js +++ b/src/matrix/room/sending/SendQueue.js @@ -31,7 +31,6 @@ export class SendQueue { while (this._amountSent < this._pendingEvents.length) { const pendingEvent = this._pendingEvents.get(this._amountSent); console.log("trying to send", pendingEvent.content.body); - this._amountSent += 1; if (pendingEvent.remoteId) { continue; } @@ -50,6 +49,7 @@ export class SendQueue { console.log("writing remoteId now"); await this._tryUpdateEvent(pendingEvent); console.log("keep sending?", this._amountSent, "<", this._pendingEvents.length); + this._amountSent += 1; } } catch(err) { if (err instanceof ConnectionError) { From 6b453c1ec485e28aa9fe31ee41991e7bce7054a3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 18:46:45 +0200 Subject: [PATCH 89/93] we need to start when online actually, so invert --- src/matrix/SessionContainer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index b6b2940a..04df8680 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -152,7 +152,7 @@ export class SessionContainer { // restored the connection, it would have already // started to session, so check first // to prevent an extra /versions request - if (!this._session.isStarted) { + if (this._session.isStarted) { const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response(); this._session.start(lastVersionsResponse); } From f476ac0dae27922228fba43b9637aed6ab1802b7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 18:47:01 +0200 Subject: [PATCH 90/93] some notes --- doc/impl-thoughts/html-messages.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/impl-thoughts/html-messages.md diff --git a/doc/impl-thoughts/html-messages.md b/doc/impl-thoughts/html-messages.md new file mode 100644 index 00000000..f217c82d --- /dev/null +++ b/doc/impl-thoughts/html-messages.md @@ -0,0 +1,4 @@ +message model: + - paragraphs (p, h1, code block, quote, ...) + - lines + - parts (inline markup), which can be recursive From f9c0b4b53e749da0bf8ddff2964317f61417310c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 19:14:30 +0200 Subject: [PATCH 91/93] add logging in case #45 would happen again --- src/matrix/room/Room.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index e533d932..60983df2 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -115,12 +115,16 @@ export class Room extends EventEmitter { if (this._timeline) { throw new Error("not dealing with load race here for now"); } + console.log(`opening the timeline for ${this._roomId}`); this._timeline = new Timeline({ roomId: this.id, storage: this._storage, fragmentIdComparer: this._fragmentIdComparer, pendingEvents: this._sendQueue.pendingEvents, - closeCallback: () => this._timeline = null, + closeCallback: () => { + console.log(`closing the timeline for ${this._roomId}`); + this._timeline = null; + }, user: this._user, }); await this._timeline.load(); From f56b96b0ff0b023cdada6c25f6e22bcf43fdf1ca Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 19:14:53 +0200 Subject: [PATCH 92/93] add some ideas for relations --- doc/impl-thoughts/RELATIONS.md | 2 ++ src/matrix/room/timeline/Timeline.js | 3 +++ 2 files changed, 5 insertions(+) diff --git a/doc/impl-thoughts/RELATIONS.md b/doc/impl-thoughts/RELATIONS.md index b1146b6c..c7886024 100644 --- a/doc/impl-thoughts/RELATIONS.md +++ b/doc/impl-thoughts/RELATIONS.md @@ -10,3 +10,5 @@ alternatively, SyncWriter/SendQueue could have a section with updatedEntries apa SendQueue will need to pass the non-sent state (redactions & relations) about an event that has it's remote echo received to the SyncWriter so it doesn't flash while redactions and relations for it still have to be synced. Also, related ids should be processed recursively. If event 3 is a redaction of event 2, a reaction to event 1, all 3 entries should be considered as updated. + +As a UI for reactions, we could show (👍 14 + 1) where the + 1 is our own local echo (perhaps style it pulsating and/or in grey?). Clicking it again would just show 14 and when the remote echo comes in it would turn into 15. diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 95a39022..bd4874c5 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -29,6 +29,9 @@ export class Timeline { this._remoteEntries.setManySorted(entries); } + // TODO: should we rather have generic methods for + // - adding new entries + // - updating existing entries (redaction, relations) /** @package */ appendLiveEntries(newEntries) { this._remoteEntries.setManySorted(newEntries); From c55a1973035947cbec365f9ae5887d4bdb7c704f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 7 May 2020 19:17:15 +0200 Subject: [PATCH 93/93] pass options to parent in LoginViewModel --- src/domain/LoginViewModel.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 64558295..0393582c 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -2,8 +2,9 @@ import {ViewModel} from "./ViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; export class LoginViewModel extends ViewModel { - constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { - super(); + constructor(options) { + super(options); + const {sessionCallback, defaultHomeServer, createSessionContainer} = options; this._createSessionContainer = createSessionContainer; this._sessionCallback = sessionCallback; this._defaultHomeServer = defaultHomeServer;