move switching room view models to a dedicated observable

based on the observing the room status
This commit is contained in:
Bruno Windels 2021-05-07 13:10:35 +02:00
parent 6bb8e2fa43
commit 6c58c61da9
4 changed files with 196 additions and 122 deletions

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import {ViewModel} from "../ViewModel.js";
import {removeRoomFromPath} from "../navigation/index.js";
function dedupeSparse(roomIds) {
return roomIds.map((id, idx) => {
@ -33,10 +32,9 @@ export class RoomGridViewModel extends ViewModel {
this._width = options.width;
this._height = options.height;
this._createRoomViewModel = options.createRoomViewModel;
this._createRoomViewModelObservable = options.createRoomViewModelObservable;
this._selectedIndex = 0;
this._viewModels = [];
this._refreshRoomViewModel = this._refreshRoomViewModel.bind(this);
this._viewModelsObservables = [];
this._setupNavigation();
}
@ -55,38 +53,17 @@ export class RoomGridViewModel extends ViewModel {
this.track(focusedRoom.subscribe(roomId => {
if (roomId) {
// as the room will be in the "rooms" observable
// (monitored by the parent vm) as well,
// (monitored by the parent vmo) as well,
// we only change the focus here and trust
// setRoomIds to have created the vm already
// setRoomIds to have created the vmo already
this._setFocusRoom(roomId);
}
}));
// initial focus for a room is set by initializeRoomIdsAndTransferVM
}
_refreshRoomViewModel(roomId) {
const index = this._viewModels.findIndex(vm => vm?.id === roomId);
if (index === -1) {
return;
}
this._viewModels[index] = this.disposeTracked(this._viewModels[index]);
// this will create a RoomViewModel because the invite is already
// removed from the collection (see Invite.afterSync)
const roomVM = this._createRoomViewModel(roomId, this._refreshRoomViewModel);
if (roomVM) {
this._viewModels[index] = this.track(roomVM);
if (this.focusIndex === index) {
roomVM.focus();
}
} else {
// close room id
this.navigation.applyPath(removeRoomFromPath(this.navigation.path, roomId));
}
this.emitChange();
}
roomViewModelAt(i) {
return this._viewModels[i];
return this._viewModelsObservables[i]?.get();
}
get focusIndex() {
@ -105,9 +82,9 @@ export class RoomGridViewModel extends ViewModel {
if (index === this._selectedIndex) {
return;
}
const vm = this._viewModels[index];
if (vm) {
this.navigation.push("room", vm.id);
const vmo = this._viewModelsObservables[index];
if (vmo) {
this.navigation.push("room", vmo.id);
} else {
this.navigation.push("empty-grid-tile", index);
}
@ -120,7 +97,8 @@ export class RoomGridViewModel extends ViewModel {
if (existingRoomVM) {
const index = roomIds.indexOf(existingRoomVM.id);
if (index !== -1) {
this._viewModels[index] = this.track(existingRoomVM);
this._viewModelsObservables[index] = this.track(existingRoomVM);
existingRoomVM.subscribe(viewModel => this._refreshRoomViewModel(viewModel));
transfered = true;
}
}
@ -128,7 +106,7 @@ export class RoomGridViewModel extends ViewModel {
// now all view models exist, set the focus to the selected room
const focusedRoom = this.navigation.path.get("room");
if (focusedRoom) {
const index = this._viewModels.findIndex(vm => vm && vm.id === focusedRoom.value);
const index = this._viewModelsObservables.findIndex(vmo => vmo && vmo.id === focusedRoom.value);
if (index !== -1) {
this._selectedIndex = index;
}
@ -143,17 +121,17 @@ export class RoomGridViewModel extends ViewModel {
const len = this._height * this._width;
for (let i = 0; i < len; i += 1) {
const newId = roomIds[i];
const vm = this._viewModels[i];
const vmo = this._viewModelsObservables[i];
// did anything change?
if ((!vm && newId) || (vm && vm.id !== newId)) {
if (vm) {
this._viewModels[i] = this.disposeTracked(vm);
if ((!vmo && newId) || (vmo && vmo.id !== newId)) {
if (vmo) {
this._viewModelsObservables[i] = this.disposeTracked(vmo);
}
if (newId) {
const newVM = this._createRoomViewModel(newId, this._refreshRoomViewModel);
if (newVM) {
this._viewModels[i] = this.track(newVM);
}
const vmo = this._createRoomViewModelObservable(newId);
this._viewModelsObservables[i] = this.track(vmo);
vmo.subscribe(viewModel => this._refreshRoomViewModel(viewModel));
vmo.initialize();
}
changed = true;
}
@ -164,14 +142,20 @@ export class RoomGridViewModel extends ViewModel {
return changed;
}
_refreshRoomViewModel(viewModel) {
this.emitChange();
viewModel?.focus();
}
/** called from SessionViewModel */
releaseRoomViewModel(roomId) {
const index = this._viewModels.findIndex(vm => vm && vm.id === roomId);
const index = this._viewModelsObservables.findIndex(vmo => vmo && vmo.id === roomId);
if (index !== -1) {
const vm = this._viewModels[index];
this.untrack(vm);
this._viewModels[index] = null;
return vm;
const vmo = this._viewModelsObservables[index];
this.untrack(vmo);
vmo.unsubscribeAll();
this._viewModelsObservables[index] = null;
return vmo;
}
}
@ -180,13 +164,13 @@ export class RoomGridViewModel extends ViewModel {
return;
}
this._selectedIndex = idx;
const vm = this._viewModels[this._selectedIndex];
vm?.focus();
const vmo = this._viewModelsObservables[this._selectedIndex];
vmo?.get()?.focus();
this.emitChange("focusIndex");
}
_setFocusRoom(roomId) {
const index = this._viewModels.findIndex(vm => vm?.id === roomId);
const index = this._viewModelsObservables.findIndex(vmo => vmo?.id === roomId);
if (index >= 0) {
this._setFocusIndex(index);
}
@ -194,6 +178,8 @@ export class RoomGridViewModel extends ViewModel {
}
import {createNavigation} from "../navigation/index.js";
import {ObservableValue} from "../../observable/ObservableValue.js";
export function tests() {
class RoomVMMock {
constructor(id) {
@ -209,6 +195,12 @@ export function tests() {
}
}
class RoomViewModelObservableMock extends ObservableValue {
async initialize() {}
dispose() { this.get()?.dispose(); }
get id() { return this.get()?.id; }
}
function createNavigationForRoom(rooms, room) {
const navigation = createNavigation();
navigation.applyPath(navigation.pathFrom([
@ -233,7 +225,7 @@ export function tests() {
"initialize with duplicate set of rooms": assert => {
const navigation = createNavigationForRoom(["c", "a", "b", undefined, "a"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,
@ -250,12 +242,12 @@ export function tests() {
"transfer room view model": assert => {
const navigation = createNavigationForRoom(["a"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: () => assert.fail("no vms should be created"),
createRoomViewModelObservable: () => assert.fail("no vms should be created"),
navigation,
width: 3,
height: 2,
});
const existingRoomVM = new RoomVMMock("a");
const existingRoomVM = new RoomViewModelObservableMock(new RoomVMMock("a"));
const transfered = gridVM.initializeRoomIdsAndTransferVM(navigation.path.get("rooms").value, existingRoomVM);
assert.equal(transfered, true);
assert.equal(gridVM.focusIndex, 0);
@ -264,12 +256,12 @@ export function tests() {
"reject transfer for non-matching room view model": assert => {
const navigation = createNavigationForRoom(["a"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,
});
const existingRoomVM = new RoomVMMock("f");
const existingRoomVM = new RoomViewModelObservableMock(new RoomVMMock("f"));
const transfered = gridVM.initializeRoomIdsAndTransferVM(navigation.path.get("rooms").value, existingRoomVM);
assert.equal(transfered, false);
assert.equal(gridVM.focusIndex, 0);
@ -278,7 +270,7 @@ export function tests() {
"created & released room view model is not disposed": assert => {
const navigation = createNavigationForRoom(["a"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,
@ -287,27 +279,27 @@ export function tests() {
assert.equal(transfered, false);
const releasedVM = gridVM.releaseRoomViewModel("a");
gridVM.dispose();
assert.equal(releasedVM.disposed, false);
assert.equal(releasedVM.get().disposed, false);
},
"transfered & released room view model is not disposed": assert => {
const navigation = createNavigationForRoom([undefined, "a"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: () => assert.fail("no vms should be created"),
createRoomViewModelObservable: () => assert.fail("no vms should be created"),
navigation,
width: 3,
height: 2,
});
const existingRoomVM = new RoomVMMock("a");
const existingRoomVM = new RoomViewModelObservableMock(new RoomVMMock("a"));
const transfered = gridVM.initializeRoomIdsAndTransferVM(navigation.path.get("rooms").value, existingRoomVM);
assert.equal(transfered, true);
const releasedVM = gridVM.releaseRoomViewModel("a");
gridVM.dispose();
assert.equal(releasedVM.disposed, false);
assert.equal(releasedVM.get().disposed, false);
},
"try release non-existing room view model is": assert => {
const navigation = createNavigationForEmptyTile([undefined, "b"], 3);
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,
@ -319,7 +311,7 @@ export function tests() {
"initial focus is set to empty tile": assert => {
const navigation = createNavigationForEmptyTile(["a"], 1);
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,
@ -331,7 +323,7 @@ export function tests() {
"change room ids after creation": assert => {
const navigation = createNavigationForRoom(["a", "b"], "a");
const gridVM = new RoomGridViewModel({
createRoomViewModel: id => new RoomVMMock(id),
createRoomViewModelObservable: id => new RoomViewModelObservableMock(new RoomVMMock(id)),
navigation,
width: 3,
height: 2,

View file

@ -0,0 +1,81 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableValue} from "../../observable/ObservableValue.js";
/**
Depending on the status of a room (invited, joined, archived, or none),
we want to show a different view with a different view model
when showing a room. Furthermore, this logic is needed both in the
single room view and in the grid view. So this logic is extracted here,
and this observable updates with the right view model as the status for
a room changes.
To not have to track the subscription manually in the SessionViewModel and
the RoomGridViewModel, all subscriptions are removed in the dispose method.
Only when transferring a RoomViewModelObservable between the SessionViewModel
and RoomGridViewModel, unsubscribeAll should be called prior to doing
the transfer, so either parent view model don't keep getting updates for
the now transferred child view model.
This is also why there is an explicit initialize method, see comment there.
*/
export class RoomViewModelObservable extends ObservableValue {
constructor(sessionViewModel, roomId) {
super(null);
this._sessionViewModel = sessionViewModel;
this.id = roomId;
}
/**
Separate initialize method rather than doing this onSubscribeFirst because
we don't want to run this again when transferring this value between
SessionViewModel and RoomGridViewModel, as onUnsubscribeLast and onSubscribeFirst
are called in that case.
*/
async initialize() {
const {session} = this._sessionViewModel._sessionContainer;
this._statusObservable = await session.observeRoomStatus(this.id);
this.set(await this._statusToViewModel(this._statusObservable.get()));
this._statusObservable.subscribe(async status => {
this.set(await this._statusToViewModel(status));
});
}
async _statusToViewModel(status) {
if (status.invited) {
return this._sessionViewModel._createInviteViewModel(this.id);
} else if (status.joined) {
return this._sessionViewModel._createRoomViewModel(this.id);
} else if (status.archived) {
if (!this.get() || this.get().kind !== "room") {
return await this._sessionViewModel._createArchivedRoomViewModel(this.id);
} else {
// reuse existing Room
return this.get();
}
}
return null;
}
dispose() {
if (this._statusSubscription) {
this._statusSubscription = this._statusSubscription();
}
this.unsubscribeAll();
this.get()?.dispose();
}
}

View file

@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {removeRoomFromPath} from "../navigation/index.js";
import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js";
import {InviteViewModel} from "./room/InviteViewModel.js";
@ -24,6 +23,7 @@ import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js";
import {ViewModel} from "../ViewModel.js";
import {RoomViewModelObservable} from "./RoomViewModelObservable.js";
export class SessionViewModel extends ViewModel {
constructor(options) {
@ -40,10 +40,8 @@ export class SessionViewModel extends ViewModel {
rooms: this._sessionContainer.session.rooms
})));
this._settingsViewModel = null;
this._currentRoomViewModel = null;
this._roomViewModelObservable = null;
this._gridViewModel = null;
this._refreshRoomViewModel = this._refreshRoomViewModel.bind(this);
this._createRoomViewModel = this._createRoomViewModel.bind(this);
this._setupNavigation();
}
@ -90,7 +88,7 @@ export class SessionViewModel extends ViewModel {
}
get activeMiddleViewModel() {
return this._currentRoomViewModel || this._gridViewModel || this._settingsViewModel;
return this._roomViewModelObservable?.get() || this._gridViewModel || this._settingsViewModel;
}
get roomGridViewModel() {
@ -110,7 +108,7 @@ export class SessionViewModel extends ViewModel {
}
get currentRoomViewModel() {
return this._currentRoomViewModel;
return this._roomViewModelObservable?.get();
}
_updateGrid(roomIds) {
@ -121,12 +119,14 @@ export class SessionViewModel extends ViewModel {
this._gridViewModel = this.track(new RoomGridViewModel(this.childOptions({
width: 3,
height: 2,
createRoomViewModel: this._createRoomViewModel,
createRoomViewModelObservable: roomId => new RoomViewModelObservable(this, roomId),
})));
if (this._gridViewModel.initializeRoomIdsAndTransferVM(roomIds, this._currentRoomViewModel)) {
this._currentRoomViewModel = this.untrack(this._currentRoomViewModel);
} else if (this._currentRoomViewModel) {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
// try to transfer the current room view model, so we don't have to reload the timeline
this._roomViewModelObservable?.unsubscribeAll();
if (this._gridViewModel.initializeRoomIdsAndTransferVM(roomIds, this._roomViewModelObservable)) {
this._roomViewModelObservable = this.untrack(this._roomViewModelObservable);
} else if (this._roomViewModelObservable) {
this._roomViewModelObservable = this.disposeTracked(this._roomViewModelObservable);
}
} else {
this._gridViewModel.setRoomIds(roomIds);
@ -134,14 +134,12 @@ export class SessionViewModel extends ViewModel {
} else if (this._gridViewModel && !roomIds) {
// closing grid, try to show focused room in grid
if (currentRoomId) {
const vm = this._gridViewModel.releaseRoomViewModel(currentRoomId.value);
if (vm) {
this._currentRoomViewModel = this.track(vm);
} else {
const newVM = this._createRoomViewModel(currentRoomId.value, this._refreshRoomViewModel);
if (newVM) {
this._currentRoomViewModel = this.track(newVM);
}
const vmo = this._gridViewModel.releaseRoomViewModel(currentRoomId.value);
if (vmo) {
this._roomViewModelObservable = this.track(vmo);
this._roomViewModelObservable.subscribe(() => {
this.emitChange("activeMiddleViewModel");
});
}
}
this._gridViewModel = this.disposeTracked(this._gridViewModel);
@ -151,63 +149,59 @@ export class SessionViewModel extends ViewModel {
}
}
/**
* @param {string} roomId
* @param {function} refreshRoomViewModel passed in as an argument, because the grid needs a different impl of this
* @return {RoomViewModel | InviteViewModel}
*/
_createRoomViewModel(roomId, refreshRoomViewModel) {
_createRoomViewModel(roomId) {
const room = this._sessionContainer.session.rooms.get(roomId);
if (room) {
const roomVM = new RoomViewModel(this.childOptions({
room,
ownUserId: this._sessionContainer.session.user.id,
}));
roomVM.load();
return roomVM;
}
return null;
}
async _createArchivedRoomViewModel(roomId) {
const room = await this._sessionContainer.session.loadArchivedRoom(roomId);
if (room) {
const roomVM = new RoomViewModel(this.childOptions({
room,
ownUserId: this._sessionContainer.session.user.id,
}));
roomVM.load();
return roomVM;
}
return null;
}
_createInviteViewModel(roomId) {
const invite = this._sessionContainer.session.invites.get(roomId);
if (invite) {
return new InviteViewModel(this.childOptions({
invite,
mediaRepository: this._sessionContainer.session.mediaRepository,
refreshRoomViewModel,
}));
} else {
const room = this._sessionContainer.session.rooms.get(roomId);
if (room) {
const roomVM = new RoomViewModel(this.childOptions({
room,
ownUserId: this._sessionContainer.session.user.id,
refreshRoomViewModel
}));
roomVM.load();
return roomVM;
}
}
return null;
}
/** refresh the room view model after an internal change that needs
to change between invite, room or none state */
_refreshRoomViewModel(roomId) {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
const roomVM = this._createRoomViewModel(roomId, this._refreshRoomViewModel);
if (roomVM) {
this._currentRoomViewModel = this.track(roomVM);
} else {
// close room id
this.navigation.applyPath(removeRoomFromPath(this.navigation.path, roomId));
}
this.emitChange("activeMiddleViewModel");
}
_updateRoom(roomId) {
// opening a room and already open?
if (this._currentRoomViewModel?.id === roomId) {
if (this._roomViewModelObservable?.id === roomId) {
return;
}
// close if needed
if (this._currentRoomViewModel) {
this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel);
if (this._roomViewModelObservable) {
this._roomViewModelObservable = this.disposeTracked(this._roomViewModelObservable);
}
// and try opening again
const roomVM = this._createRoomViewModel(roomId, this._refreshRoomViewModel);
if (roomVM) {
this._currentRoomViewModel = this.track(roomVM);
}
this.emitChange("activeMiddleViewModel");
const vmo = new RoomViewModelObservable(this, roomId);
this._roomViewModelObservable = this.track(vmo);
// subscription is unsubscribed in RoomViewModelObservable.dispose, and thus handled by track
this._roomViewModelObservable.subscribe(() => {
this.emitChange("activeMiddleViewModel");
});
vmo.initialize();
}
_updateSettings(settingsOpen) {

View file

@ -48,6 +48,13 @@ export class BaseObservable {
return null;
}
unsubscribeAll() {
if (this._handlers.size !== 0) {
this._handlers.clear();
this.onUnsubscribeLast();
}
}
get hasSubscriptions() {
return this._handlers.size !== 0;
}