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._error = null;
|
||||
this._sessionViewModel = null;
|
||||
this._sessionSubscription = null;
|
||||
this._loginViewModel = null;
|
||||
this._sessionPickerViewModel = null;
|
||||
}
|
||||
|
@ -33,45 +34,35 @@ export default class BrawlViewModel extends EventEmitter {
|
|||
}
|
||||
|
||||
async _showPicker() {
|
||||
this._clearSections();
|
||||
this._sessionPickerViewModel = new SessionPickerViewModel({
|
||||
sessionStore: this._sessionStore,
|
||||
storageFactory: this._storageFactory,
|
||||
sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo)
|
||||
this._setSection(() => {
|
||||
this._sessionPickerViewModel = new SessionPickerViewModel({
|
||||
sessionStore: this._sessionStore,
|
||||
storageFactory: this._storageFactory,
|
||||
sessionCallback: sessionInfo => this._onSessionPicked(sessionInfo)
|
||||
});
|
||||
});
|
||||
this.emit("change", "activeSection");
|
||||
try {
|
||||
await this._sessionPickerViewModel.load();
|
||||
} catch (err) {
|
||||
this._clearSections();
|
||||
this._error = err;
|
||||
this.emit("change", "activeSection");
|
||||
this._setSection(() => this._error = err);
|
||||
}
|
||||
}
|
||||
|
||||
_showLogin() {
|
||||
this._clearSections();
|
||||
this._loginViewModel = new LoginViewModel({
|
||||
createHsApi: this._createHsApi,
|
||||
defaultHomeServer: "https://matrix.org",
|
||||
loginCallback: loginData => this._onLoginFinished(loginData)
|
||||
});
|
||||
this.emit("change", "activeSection");
|
||||
this._setSection(() => {
|
||||
this._loginViewModel = new LoginViewModel({
|
||||
createHsApi: this._createHsApi,
|
||||
defaultHomeServer: "https://matrix.org",
|
||||
loginCallback: loginData => this._onLoginFinished(loginData)
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
_showSession(session, sync) {
|
||||
this._clearSections();
|
||||
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;
|
||||
this._setSection(() => {
|
||||
this._sessionViewModel = new SessionViewModel({session, sync});
|
||||
});
|
||||
}
|
||||
|
||||
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 sessionViewModel() { return this._sessionViewModel; }
|
||||
get loginViewModel() { return this._loginViewModel; }
|
||||
|
@ -123,51 +133,12 @@ export default class BrawlViewModel extends EventEmitter {
|
|||
}
|
||||
|
||||
async _loadSession(sessionInfo) {
|
||||
try {
|
||||
this._loading = true;
|
||||
this._loadingText = "Loading your conversations…";
|
||||
const reconnector = new Reconnector(
|
||||
new ExponentialRetryDelay(2000, this._clock.createTimeout),
|
||||
this._clock.createMeasure
|
||||
);
|
||||
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");
|
||||
this._setSection(() => {
|
||||
// TODO this is pseudo code-ish
|
||||
const container = this._createSessionContainer();
|
||||
this._sessionViewModel = new SessionViewModel({session, sync});
|
||||
this._sessionSubscription = this._activeSessionContainer.subscribe(this._updateSessionState);
|
||||
this._activeSessionContainer.start(sessionInfo);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._currentRoomViewModel) {
|
||||
this._currentRoomViewModel.dispose();
|
||||
this._currentRoomViewModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
_closeCurrentRoom() {
|
||||
if (this._currentRoomViewModel) {
|
||||
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 {
|
||||
constructor(start = 2000, createTimeout) {
|
||||
this._start = start;
|
||||
|
@ -76,7 +66,7 @@ export const ConnectionState = createEnum(
|
|||
"Online"
|
||||
);
|
||||
|
||||
export class Reconnector {
|
||||
export class Reconnector extends ObservableValue {
|
||||
constructor({retryDelay, createTimeMeasure}) {
|
||||
this._online
|
||||
this._retryDelay = retryDelay;
|
||||
|
@ -124,7 +114,7 @@ export class Reconnector {
|
|||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
// 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() {
|
||||
class Collection extends BaseObservableCollection {
|
||||
constructor() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {AbortError} from "./error.js";
|
||||
import {AbortError} from "../utils/error.js";
|
||||
|
||||
class DOMTimeout {
|
||||
class Timeout {
|
||||
constructor(ms) {
|
||||
this._reject = null;
|
||||
this._handle = null;
|
||||
|
@ -27,7 +27,7 @@ class DOMTimeout {
|
|||
}
|
||||
}
|
||||
|
||||
class DOMTimeMeasure {
|
||||
class TimeMeasure {
|
||||
constructor() {
|
||||
this._start = window.performance.now();
|
||||
}
|
||||
|
@ -37,13 +37,13 @@ class DOMTimeMeasure {
|
|||
}
|
||||
}
|
||||
|
||||
export class DOMClock {
|
||||
export class Clock {
|
||||
createMeasure() {
|
||||
return new DOMTimeMeasure();
|
||||
return new TimeMeasure();
|
||||
}
|
||||
|
||||
createTimeout(ms) {
|
||||
return new DOMTimeout(ms);
|
||||
return new Timeout(ms);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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