This commit is contained in:
Bruno Windels 2020-04-29 10:10:20 +02:00
parent e6ae60abb4
commit 8bde627cdb
10 changed files with 79 additions and 143 deletions

View file

@ -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? 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.

View file

@ -1,9 +1,5 @@
import {EventEmitter} from "../utils/EventEmitter.js"; import {EventEmitter} from "../utils/EventEmitter.js";
import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {AbortError} from "../utils/error.js";
import {loadLabel} from "./common.js";
export class LoginViewModel extends EventEmitter { export class LoginViewModel extends EventEmitter {
constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) {
@ -11,121 +7,42 @@ export class LoginViewModel extends EventEmitter {
this._createSessionContainer = createSessionContainer; this._createSessionContainer = createSessionContainer;
this._sessionCallback = sessionCallback; this._sessionCallback = sessionCallback;
this._defaultHomeServer = defaultHomeServer; this._defaultHomeServer = defaultHomeServer;
this._homeserver = null; this._loadViewModel = null;
this._sessionContainer = null;
this._loadWaitHandle = null;
this._loading = false;
this._error = null;
} }
get usernamePlaceholder() { return "Username"; } get usernamePlaceholder() { return "Username"; }
get passwordPlaceholder() { return "Password"; } get passwordPlaceholder() { return "Password"; }
get hsPlaceholder() { return "Your matrix homeserver"; } get hsPlaceholder() { return "Your matrix homeserver"; }
get defaultHomeServer() { return this._defaultHomeServer; } get defaultHomeServer() { return this._defaultHomeServer; }
get loading() {return this._loading}
get showLoadLabel() { get loadViewModel() {return this._loadViewModel; }
return this._loading || this._sessionContainer || this._error;
}
async login(username, password, homeserver) { async login(username, password, homeserver) {
try { this._loadViewModel = new SessionLoadViewModel({
this._loading = true; createAndStartSessionContainer: () => {
this.emit("change", "loading"); const sessionContainer = this._createSessionContainer();
this._homeserver = homeserver; sessionContainer.startWithLogin(homeserver, username, password);
this._sessionContainer = this._createSessionContainer(); return sessionContainer;
this._sessionContainer.startWithLogin(homeserver, username, password); },
this._loadWaitHandle = this._sessionContainer.loadStatus.waitFor(s => { sessionCallback: sessionContainer => {
this.emit("change", "loadLabel"); if (sessionContainer) {
return s === LoadStatus.Ready || // make parent view model move away
s === LoadStatus.LoginFailed || this._sessionCallback(sessionContainer);
s === LoadStatus.Error;
});
try {
await this._loadWaitHandle.promise;
} catch (err) {
if (err instanceof AbortError) {
// login was cancelled
} else { } else {
throw err; // show list of session again
this._loadViewModel = null;
this.emit("change", "loadViewModel");
} }
} },
this._loadWaitHandle = null; deleteSessionOnCancel: true,
if (this._sessionContainer.loadStatus.get() === LoadStatus.Ready) { homeserver,
this._sessionCallback(this._sessionContainer); });
// wait for parent view model to switch away here this._loadViewModel.start();
} else { this.emit("change", "loadViewModel");
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");
}
} }
get loadLabel() { cancel() {
const sc = this._sessionContainer; if (!this._loadViewModel) {
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) {
this._sessionCallback(); this._sessionCallback();
} }
} }

View file

@ -3,11 +3,12 @@ import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js";
import {SyncStatus} from "../matrix/Sync.js"; import {SyncStatus} from "../matrix/Sync.js";
export class SessionLoadViewModel extends EventEmitter { export class SessionLoadViewModel extends EventEmitter {
constructor({createAndStartSessionContainer, sessionCallback, homeserver}) { constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) {
super(); super();
this._createAndStartSessionContainer = createAndStartSessionContainer; this._createAndStartSessionContainer = createAndStartSessionContainer;
this._sessionCallback = sessionCallback; this._sessionCallback = sessionCallback;
this._homeserver = homeserver; this._homeserver = homeserver;
this._deleteSessionOnCancel = deleteSessionOnCancel;
this._loading = false; this._loading = false;
this._error = null; this._error = null;
} }
@ -33,7 +34,7 @@ export class SessionLoadViewModel extends EventEmitter {
try { try {
await this._waitHandle.promise; await this._waitHandle.promise;
} catch (err) { } catch (err) {
// swallow AbortError return; // aborted by goBack
} }
// TODO: should we deal with no connection during initial sync // TODO: should we deal with no connection during initial sync
// and we're retrying as well here? // 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() { // to show a spinner or not
if (this._sessionContainer) { get loading() {
this._sessionContainer.stop(); return this._loading;
this._sessionContainer = null;
if (this._waitHandle) {
this._waitHandle.dispose();
}
}
this._sessionCallback();
} }
get loadLabel() { get loadLabel() {

View file

@ -1,5 +1,6 @@
import {SortedArray} from "../observable/index.js"; import {SortedArray} from "../observable/index.js";
import {EventEmitter} from "../utils/EventEmitter.js"; import {EventEmitter} from "../utils/EventEmitter.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
class SessionItemViewModel extends EventEmitter { class SessionItemViewModel extends EventEmitter {
constructor(sessionInfo, pickerVM) { constructor(sessionInfo, pickerVM) {
@ -127,7 +128,7 @@ export class SessionPickerViewModel extends EventEmitter {
} }
const sessionVM = this._sessions.array.find(s => s.id === id); const sessionVM = this._sessions.array.find(s => s.id === id);
if (sessionVM) { if (sessionVM) {
this._loadViewModel = new LoadViewModel({ this._loadViewModel = new SessionLoadViewModel({
createAndStartSessionContainer: () => { createAndStartSessionContainer: () => {
const sessionContainer = this._createSessionContainer(); const sessionContainer = this._createSessionContainer();
sessionContainer.startWithExistingSession(sessionVM.id); sessionContainer.startWithExistingSession(sessionVM.id);
@ -182,6 +183,8 @@ export class SessionPickerViewModel extends EventEmitter {
} }
cancel() { cancel() {
this._sessionCallback(); if (!this._loadViewModel) {
this._sessionCallback();
}
} }
} }

View file

@ -17,17 +17,21 @@ export class BaseObservable {
this.onSubscribeFirst(); this.onSubscribeFirst();
} }
return () => { return () => {
if (handler) { return this.unsubscribe(handler);
this._handlers.delete(handler);
if (this._handlers.size === 0) {
this.onUnsubscribeLast();
}
handler = null;
}
return null;
}; };
} }
unsubscribe(handler) {
if (handler) {
this._handlers.delete(handler);
if (this._handlers.size === 0) {
this.onUnsubscribeLast();
}
handler = null;
}
return null;
}
// Add iterator over handlers here // Add iterator over handlers here
} }

View file

@ -6,7 +6,8 @@
body { body {
margin: 0; 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; background-color: black;
color: white; color: white;
} }

View file

@ -13,7 +13,7 @@ export class LoginView extends TemplateView {
const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled}); const homeserver = t.input({type: "text", placeholder: vm.hsPlaceholder, value: vm.defaultHomeServer, disabled});
return t.div({className: "LoginView form"}, [ return t.div({className: "LoginView form"}, [
t.h1(["Log in to your homeserver"]), 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(username),
t.div(password), t.div(password),
t.div(homeserver), t.div(homeserver),
@ -22,7 +22,7 @@ export class LoginView extends TemplateView {
disabled disabled
}, "Log In")), }, "Log In")),
t.div(t.button({onClick: () => vm.goBack(), disabled}, ["Pick an existing session"])), 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)) t.p(brawlGithubLink(t))
]); ]);
} }
@ -32,6 +32,6 @@ function renderLoadProgress(t) {
return t.div({className: "loadProgress"}, [ return t.div({className: "loadProgress"}, [
t.div({className: "spinner"}), t.div({className: "spinner"}),
t.p(vm => vm.loadLabel), 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")))
]); ]);
} }

View file

@ -27,10 +27,6 @@ function selectFileAsText(mimeType) {
class SessionPickerItemView extends TemplateView { class SessionPickerItemView extends TemplateView {
constructor(vm) {
super(vm, true);
}
_onDeleteClick() { _onDeleteClick() {
if (confirm("Are you sure?")) { if (confirm("Are you sure?")) {
this.viewModel.delete(); this.viewModel.delete();
@ -50,16 +46,16 @@ class SessionPickerItemView extends TemplateView {
disabled: vm => vm.isClearing, disabled: vm => vm.isClearing,
onClick: () => this.viewModel.export(), onClick: () => this.viewModel.export(),
}, "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({ return t.a({
href: vm.exportDataUrl, href: vm.exportDataUrl,
download: `brawl-session-${this.viewModel.id}.json`, download: `brawl-session-${this.viewModel.id}.json`,
onClick: () => setTimeout(() => this.viewModel.clearExport(), 100), onClick: () => setTimeout(() => this.viewModel.clearExport(), 100),
}, "Download"); }, "Download");
}); }));
const userName = t.span({className: "userId"}, vm => vm.label); 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"}, [ return t.li([t.div({className: "sessionInfo"}, [
userName, userName,
errorMessage, errorMessage,

View file

@ -11,7 +11,7 @@ export class SyncStatusBar extends TemplateView {
"SyncStatusBar_shown": true, "SyncStatusBar_shown": true,
}}, [ }}, [
vm => vm.status, 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") : "" window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : ""
]); ]);
} }

View file

@ -12,7 +12,7 @@ export class GapView extends TemplateView {
onClick: () => this.viewModel.fill(), onClick: () => this.viewModel.fill(),
disabled: vm => vm.isLoading disabled: vm => vm.isLoading
}, label), }, 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)))
]); ]);
} }
} }