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?
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 {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();
}
}

View file

@ -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() {

View file

@ -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();
}
}
}

View file

@ -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
}

View file

@ -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;
}

View file

@ -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")))
]);
}

View file

@ -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,

View file

@ -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") : ""
]);
}

View file

@ -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)))
]);
}
}