forked from mystiq/hydrogen-web
more WIP and breakage
This commit is contained in:
parent
ef267ca331
commit
378b75c98a
11 changed files with 326 additions and 91 deletions
|
@ -20,6 +20,7 @@ export default class BrawlViewModel extends EventEmitter {
|
||||||
this._loading = false;
|
this._loading = false;
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this._sessionViewModel = null;
|
this._sessionViewModel = null;
|
||||||
|
this._sessionSubscription = null;
|
||||||
this._loginViewModel = null;
|
this._loginViewModel = null;
|
||||||
this._sessionPickerViewModel = null;
|
this._sessionPickerViewModel = null;
|
||||||
}
|
}
|
||||||
|
@ -33,45 +34,35 @@ export default class BrawlViewModel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _showPicker() {
|
async _showPicker() {
|
||||||
this._clearSections();
|
this._setSection(() => {
|
||||||
this._sessionPickerViewModel = new SessionPickerViewModel({
|
this._sessionPickerViewModel = new SessionPickerViewModel({
|
||||||
sessionStore: this._sessionStore,
|
sessionStore: this._sessionStore,
|
||||||
storageFactory: this._storageFactory,
|
storageFactory: this._storageFactory,
|
||||||
sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo)
|
sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.emit("change", "activeSection");
|
|
||||||
try {
|
try {
|
||||||
await this._sessionPickerViewModel.load();
|
await this._sessionPickerViewModel.load();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._clearSections();
|
this._setSection(() => this._error = err);
|
||||||
this._error = err;
|
|
||||||
this.emit("change", "activeSection");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showLogin() {
|
_showLogin() {
|
||||||
this._clearSections();
|
this._setSection(() => {
|
||||||
this._loginViewModel = new LoginViewModel({
|
this._loginViewModel = new LoginViewModel({
|
||||||
createHsApi: this._createHsApi,
|
createHsApi: this._createHsApi,
|
||||||
defaultHomeServer: "https://matrix.org",
|
defaultHomeServer: "https://matrix.org",
|
||||||
loginCallback: loginData => this._onLoginFinished(loginData)
|
loginCallback: loginData => this._onLoginFinished(loginData)
|
||||||
});
|
});
|
||||||
this.emit("change", "activeSection");
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_showSession(session, sync) {
|
_showSession(session, sync) {
|
||||||
this._clearSections();
|
this._setSection(() => {
|
||||||
this._sessionViewModel = new SessionViewModel({session, sync});
|
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeSection() {
|
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 loadingText() { return this._loadingText; }
|
||||||
get sessionViewModel() { return this._sessionViewModel; }
|
get sessionViewModel() { return this._sessionViewModel; }
|
||||||
get loginViewModel() { return this._loginViewModel; }
|
get loginViewModel() { return this._loginViewModel; }
|
||||||
|
@ -123,51 +133,12 @@ export default class BrawlViewModel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadSession(sessionInfo) {
|
async _loadSession(sessionInfo) {
|
||||||
try {
|
this._setSection(() => {
|
||||||
this._loading = true;
|
// TODO this is pseudo code-ish
|
||||||
this._loadingText = "Loading your conversations…";
|
const container = this._createSessionContainer();
|
||||||
const reconnector = new Reconnector(
|
this._sessionViewModel = new SessionViewModel({session, sync});
|
||||||
new ExponentialRetryDelay(2000, this._clock.createTimeout),
|
this._sessionSubscription = this._activeSessionContainer.subscribe(this._updateSessionState);
|
||||||
this._clock.createMeasure
|
this._activeSessionContainer.start(sessionInfo);
|
||||||
);
|
});
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/domain/ViewModel.js
Normal file
19
src/domain/ViewModel.js
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
43
src/domain/session/SessionLoadViewModel.js
Normal file
43
src/domain/session/SessionLoadViewModel.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,13 @@ export default class SessionViewModel extends EventEmitter {
|
||||||
return this._currentRoomViewModel;
|
return this._currentRoomViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._currentRoomViewModel) {
|
||||||
|
this._currentRoomViewModel.dispose();
|
||||||
|
this._currentRoomViewModel = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_closeCurrentRoom() {
|
_closeCurrentRoom() {
|
||||||
if (this._currentRoomViewModel) {
|
if (this._currentRoomViewModel) {
|
||||||
this._currentRoomViewModel.dispose();
|
this._currentRoomViewModel.dispose();
|
||||||
|
|
|
@ -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 {
|
export class ExponentialRetryDelay {
|
||||||
constructor(start = 2000, createTimeout) {
|
constructor(start = 2000, createTimeout) {
|
||||||
this._start = start;
|
this._start = start;
|
||||||
|
@ -76,7 +66,7 @@ export const ConnectionState = createEnum(
|
||||||
"Online"
|
"Online"
|
||||||
);
|
);
|
||||||
|
|
||||||
export class Reconnector {
|
export class Reconnector extends ObservableValue {
|
||||||
constructor({retryDelay, createTimeMeasure}) {
|
constructor({retryDelay, createTimeMeasure}) {
|
||||||
this._online
|
this._online
|
||||||
this._retryDelay = retryDelay;
|
this._retryDelay = retryDelay;
|
||||||
|
@ -124,7 +114,7 @@ export class Reconnector {
|
||||||
} else {
|
} else {
|
||||||
this._stateSince = null;
|
this._stateSince = null;
|
||||||
}
|
}
|
||||||
this.emit("change", state);
|
this.emit(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
95
src/matrix/SessionContainer.js
Normal file
95
src/matrix/SessionContainer.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,15 @@ export default class BaseObservableCollection {
|
||||||
// Add iterator over handlers here
|
// 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() {
|
export function tests() {
|
||||||
class Collection extends BaseObservableCollection {
|
class Collection extends BaseObservableCollection {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {AbortError} from "./error.js";
|
import {AbortError} from "../utils/error.js";
|
||||||
|
|
||||||
class DOMTimeout {
|
class Timeout {
|
||||||
constructor(ms) {
|
constructor(ms) {
|
||||||
this._reject = null;
|
this._reject = null;
|
||||||
this._handle = null;
|
this._handle = null;
|
||||||
|
@ -27,7 +27,7 @@ class DOMTimeout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DOMTimeMeasure {
|
class TimeMeasure {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._start = window.performance.now();
|
this._start = window.performance.now();
|
||||||
}
|
}
|
||||||
|
@ -37,13 +37,13 @@ class DOMTimeMeasure {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DOMClock {
|
export class Clock {
|
||||||
createMeasure() {
|
createMeasure() {
|
||||||
return new DOMTimeMeasure();
|
return new TimeMeasure();
|
||||||
}
|
}
|
||||||
|
|
||||||
createTimeout(ms) {
|
createTimeout(ms) {
|
||||||
return new DOMTimeout(ms);
|
return new Timeout(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
now() {
|
now() {
|
29
src/ui/web/dom/Online.js
Normal file
29
src/ui/web/dom/Online.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,3 +34,38 @@ export default class SwitchView {
|
||||||
return this._childView;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
37
src/utils/Disposables.js
Normal file
37
src/utils/Disposables.js
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue