forked from mystiq/hydrogen-web
more WIP
This commit is contained in:
parent
e6ae60abb4
commit
8bde627cdb
10 changed files with 79 additions and 143 deletions
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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") : ""
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue