From cc87e35f23d936234a9ca1175b65ba76267a0281 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 4 May 2020 19:23:11 +0200 Subject: [PATCH] use ViewModel super class for all view models that need binding --- src/domain/BrawlViewModel.js | 8 ++--- src/domain/LoginViewModel.js | 22 +++---------- src/domain/SessionLoadViewModel.js | 12 +++---- src/domain/SessionPickerViewModel.js | 26 +++++++-------- src/domain/ViewModel.js | 32 +++++++++++++++++-- src/domain/session/SessionViewModel.js | 19 +++++------ src/domain/session/SyncStatusViewModel.js | 8 ++--- src/domain/session/room/RoomViewModel.js | 23 +++++++------ .../room/timeline/TimelineViewModel.js | 2 +- 9 files changed, 85 insertions(+), 67 deletions(-) diff --git a/src/domain/BrawlViewModel.js b/src/domain/BrawlViewModel.js index 7ecf9f56..16e5a622 100644 --- a/src/domain/BrawlViewModel.js +++ b/src/domain/BrawlViewModel.js @@ -1,9 +1,9 @@ import {SessionViewModel} from "./session/SessionViewModel.js"; import {LoginViewModel} from "./LoginViewModel.js"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; -import {EventEmitter} from "../utils/EventEmitter.js"; +import {ViewModel} from "./ViewModel.js"; -export class BrawlViewModel extends EventEmitter { +export class BrawlViewModel extends ViewModel { constructor({createSessionContainer, sessionInfoStorage, storageFactory, clock}) { super(); this._createSessionContainer = createSessionContainer; @@ -32,7 +32,7 @@ export class BrawlViewModel extends EventEmitter { if (sessionContainer) { this._setSection(() => { this._sessionContainer = sessionContainer; - this._sessionViewModel = new SessionViewModel(sessionContainer); + this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer})); }); } else { // switch between picker and login @@ -96,7 +96,7 @@ export class BrawlViewModel extends EventEmitter { } // now set it again setter(); - this.emit("change", "activeSection"); + this.emitChange("activeSection"); } get error() { return this._error; } diff --git a/src/domain/LoginViewModel.js b/src/domain/LoginViewModel.js index 544bb980..3f70108f 100644 --- a/src/domain/LoginViewModel.js +++ b/src/domain/LoginViewModel.js @@ -1,7 +1,7 @@ -import {EventEmitter} from "../utils/EventEmitter.js"; +import {ViewModel} from "./ViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -export class LoginViewModel extends EventEmitter { +export class LoginViewModel extends ViewModel { constructor({sessionCallback, defaultHomeServer, createSessionContainer}) { super(); this._createSessionContainer = createSessionContainer; @@ -10,20 +10,6 @@ export class LoginViewModel extends EventEmitter { this._loadViewModel = null; } - // TODO: this will need to support binding - // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves - i18n(parts, ...expr) { - // just concat for now - let result = ""; - for (let i = 0; i < parts.length; ++i) { - result = result + parts[i]; - if (i < expr.length) { - result = result + expr[i]; - } - } - return result; - } - get defaultHomeServer() { return this._defaultHomeServer; } get loadViewModel() {return this._loadViewModel; } @@ -42,14 +28,14 @@ export class LoginViewModel extends EventEmitter { } else { // show list of session again this._loadViewModel = null; - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } }, deleteSessionOnCancel: true, homeserver, }); this._loadViewModel.start(); - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } cancel() { diff --git a/src/domain/SessionLoadViewModel.js b/src/domain/SessionLoadViewModel.js index 659abeea..d40b7f08 100644 --- a/src/domain/SessionLoadViewModel.js +++ b/src/domain/SessionLoadViewModel.js @@ -1,8 +1,8 @@ -import {EventEmitter} from "../utils/EventEmitter.js"; import {LoadStatus, LoginFailure} from "../matrix/SessionContainer.js"; import {SyncStatus} from "../matrix/Sync.js"; +import {ViewModel} from "./ViewModel.js"; -export class SessionLoadViewModel extends EventEmitter { +export class SessionLoadViewModel extends ViewModel { constructor({createAndStartSessionContainer, sessionCallback, homeserver, deleteSessionOnCancel}) { super(); this._createAndStartSessionContainer = createAndStartSessionContainer; @@ -19,10 +19,10 @@ export class SessionLoadViewModel extends EventEmitter { } try { this._loading = true; - this.emit("change"); + this.emitChange(); this._sessionContainer = this._createAndStartSessionContainer(); this._waitHandle = this._sessionContainer.loadStatus.waitFor(s => { - this.emit("change"); + this.emitChange(); // wait for initial sync, but not catchup sync const isCatchupSync = s === LoadStatus.FirstSync && this._sessionContainer.sync.status === SyncStatus.CatchupSync; @@ -50,7 +50,7 @@ export class SessionLoadViewModel extends EventEmitter { this._error = err; } finally { this._loading = false; - this.emit("change"); + this.emitChange(); } } @@ -72,7 +72,7 @@ export class SessionLoadViewModel extends EventEmitter { this._sessionCallback(); } catch (err) { this._error = err; - this.emit("change"); + this.emitChange(); } } diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index 8274106a..cc4416b5 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -1,8 +1,8 @@ import {SortedArray} from "../observable/index.js"; -import {EventEmitter} from "../utils/EventEmitter.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; +import {ViewModel} from "./ViewModel.js"; -class SessionItemViewModel extends EventEmitter { +class SessionItemViewModel extends ViewModel { constructor(sessionInfo, pickerVM) { super(); this._pickerVM = pickerVM; @@ -19,31 +19,31 @@ class SessionItemViewModel extends EventEmitter { async delete() { this._isDeleting = true; - this.emit("change", "isDeleting"); + this.emitChange("isDeleting"); try { await this._pickerVM.delete(this.id); } catch(err) { this._error = err; console.error(err); - this.emit("change", "error"); + this.emitChange("error"); } finally { this._isDeleting = false; - this.emit("change", "isDeleting"); + this.emitChange("isDeleting"); } } async clear() { this._isClearing = true; - this.emit("change"); + this.emitChange(); try { await this._pickerVM.clear(this.id); } catch(err) { this._error = err; console.error(err); - this.emit("change", "error"); + this.emitChange("error"); } finally { this._isClearing = false; - this.emit("change", "isClearing"); + this.emitChange("isClearing"); } } @@ -82,7 +82,7 @@ class SessionItemViewModel extends EventEmitter { const json = JSON.stringify(data, undefined, 2); const blob = new Blob([json], {type: "application/json"}); this._exportDataUrl = URL.createObjectURL(blob); - this.emit("change", "exportDataUrl"); + this.emitChange("exportDataUrl"); } catch (err) { alert(err.message); console.error(err); @@ -93,13 +93,13 @@ class SessionItemViewModel extends EventEmitter { if (this._exportDataUrl) { URL.revokeObjectURL(this._exportDataUrl); this._exportDataUrl = null; - this.emit("change", "exportDataUrl"); + this.emitChange("exportDataUrl"); } } } -export class SessionPickerViewModel extends EventEmitter { +export class SessionPickerViewModel extends ViewModel { constructor({storageFactory, sessionInfoStorage, sessionCallback, createSessionContainer}) { super(); this._storageFactory = storageFactory; @@ -141,12 +141,12 @@ export class SessionPickerViewModel extends EventEmitter { } else { // show list of session again this._loadViewModel = null; - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } } }); this._loadViewModel.start(); - this.emit("change", "loadViewModel"); + this.emitChange("loadViewModel"); } } diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index cc4a6fff..c49e0ef6 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -2,10 +2,13 @@ // as in some cases it would really be more convenient to have multiple events (like telling the timeline to scroll down) // we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter -export class ViewModel extends ObservableValue { +import {EventEmitter} from "../utils/EventEmitter.js"; +import {Disposables} from "../utils/Disposables.js"; + +export class ViewModel extends EventEmitter { constructor(options) { super(); - this.disposables = new Disposables(); + this.disposables = null; this._options = options; } @@ -14,10 +17,33 @@ export class ViewModel extends ObservableValue { } track(disposable) { + if (!this.disposables) { + this.disposables = new Disposables(); + } this.disposables.track(disposable); } dispose() { - this.disposables.dispose(); + if (this.disposables) { + this.disposables.dispose(); + } + } + + // TODO: this will need to support binding + // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves + i18n(parts, ...expr) { + // just concat for now + let result = ""; + for (let i = 0; i < parts.length; ++i) { + result = result + parts[i]; + if (i < expr.length) { + result = result + expr[i]; + } + } + return result; + } + + emitChange(changedProps) { + this.emit("change", changedProps); } } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 4f4e0b57..9482b40a 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -1,13 +1,14 @@ -import {EventEmitter} from "../../utils/EventEmitter.js"; import {RoomTileViewModel} from "./roomlist/RoomTileViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SyncStatusViewModel} from "./SyncStatusViewModel.js"; +import {ViewModel} from "../ViewModel.js"; -export class SessionViewModel extends EventEmitter { - constructor(sessionContainer) { - super(); +export class SessionViewModel extends ViewModel { + constructor(options) { + super(options); + const sessionContainer = options.sessionContainer; this._session = sessionContainer.session; - this._syncStatusViewModel = new SyncStatusViewModel(sessionContainer.sync); + this._syncStatusViewModel = new SyncStatusViewModel(this.childOptions(sessionContainer.sync)); this._currentRoomViewModel = null; const roomTileVMs = this._session.rooms.mapValues((room, emitUpdate) => { return new RoomTileViewModel({ @@ -42,7 +43,7 @@ export class SessionViewModel extends EventEmitter { if (this._currentRoomViewModel) { this._currentRoomViewModel.dispose(); this._currentRoomViewModel = null; - this.emit("change", "currentRoom"); + this.emitChange("currentRoom"); } } @@ -50,13 +51,13 @@ export class SessionViewModel extends EventEmitter { if (this._currentRoomViewModel) { this._currentRoomViewModel.dispose(); } - this._currentRoomViewModel = new RoomViewModel({ + this._currentRoomViewModel = new RoomViewModel(this.childOptions({ room, ownUserId: this._session.user.id, closeCallback: () => this._closeCurrentRoom(), - }); + })); this._currentRoomViewModel.load(); - this.emit("change", "currentRoom"); + this.emitChange("currentRoom"); } } diff --git a/src/domain/session/SyncStatusViewModel.js b/src/domain/session/SyncStatusViewModel.js index 7d26ee11..9c5b5010 100644 --- a/src/domain/session/SyncStatusViewModel.js +++ b/src/domain/session/SyncStatusViewModel.js @@ -1,6 +1,6 @@ -import {EventEmitter} from "../../utils/EventEmitter.js"; +import {ViewModel} from "../ViewModel.js"; -export class SyncStatusViewModel extends EventEmitter { +export class SyncStatusViewModel extends ViewModel { constructor(sync) { super(); this._sync = sync; @@ -13,7 +13,7 @@ export class SyncStatusViewModel extends EventEmitter { } else if (status === "started") { this._error = null; } - this.emit("change"); + this.emitChange(); } onFirstSubscriptionAdded(name) { @@ -30,7 +30,7 @@ export class SyncStatusViewModel extends EventEmitter { trySync() { this._sync.start(); - this.emit("change"); + this.emitChange(); } get status() { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 074070bc..fccc665f 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -1,10 +1,11 @@ -import {EventEmitter} from "../../../utils/EventEmitter.js"; import {TimelineViewModel} from "./timeline/TimelineViewModel.js"; import {avatarInitials} from "../avatar.js"; +import {ViewModel} from "../../ViewModel.js"; -export class RoomViewModel extends EventEmitter { - constructor({room, ownUserId, closeCallback}) { - super(); +export class RoomViewModel extends ViewModel { + constructor(options) { + super(options); + const {room, ownUserId, closeCallback} = options; this._room = room; this._ownUserId = ownUserId; this._timeline = null; @@ -19,12 +20,16 @@ export class RoomViewModel extends EventEmitter { this._room.on("change", this._onRoomChange); try { this._timeline = await this._room.openTimeline(); - this._timelineVM = new TimelineViewModel(this._room, this._timeline, this._ownUserId); - this.emit("change", "timelineViewModel"); + this._timelineVM = new TimelineViewModel(this.childOptions({ + room: this._room, + timeline: this._timeline, + ownUserId: this._ownUserId, + })); + this.emitChange("timelineViewModel"); } catch (err) { console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`); this._timelineError = err; - this.emit("change", "error"); + this.emitChange("error"); } } @@ -43,7 +48,7 @@ export class RoomViewModel extends EventEmitter { // room doesn't tell us yet which fields changed, // so emit all fields originating from summary _onRoomChange() { - this.emit("change", "name"); + this.emitChange("name"); } get name() { @@ -76,7 +81,7 @@ export class RoomViewModel extends EventEmitter { console.error(`room.sendMessage(): ${err.message}:\n${err.stack}`); this._sendError = err; this._timelineError = null; - this.emit("change", "error"); + this.emitChange("error"); return false; } return true; diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 1c19c15f..4550d7bd 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -18,7 +18,7 @@ import {TilesCollection} from "./TilesCollection.js"; import {tilesCreator} from "./tilesCreator.js"; export class TimelineViewModel { - constructor(room, timeline, ownUserId) { + constructor({room, timeline, ownUserId}) { this._timeline = timeline; // once we support sending messages we could do // timeline.entries.concat(timeline.pendingEvents)