From 3adc609e0792775a2b9d9af88eb5704f20c80763 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 5 May 2020 23:16:51 +0200 Subject: [PATCH] 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") : "" - ]); - } -}