This commit is contained in:
Bruno Windels 2022-02-03 17:57:35 +01:00
parent 348de312f9
commit bc09ede09f
20 changed files with 360 additions and 128 deletions

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import {ObservableValue} from "../../observable/ObservableValue"; import {ObservableValue} from "../../observable/ObservableValue";
import {RoomStatus} from "../../matrix/room/RoomStatus";
/** /**
Depending on the status of a room (invited, joined, archived, or none), Depending on the status of a room (invited, joined, archived, or none),
@ -34,11 +35,11 @@ the now transferred child view model.
This is also why there is an explicit initialize method, see comment there. This is also why there is an explicit initialize method, see comment there.
*/ */
export class RoomViewModelObservable extends ObservableValue { export class RoomViewModelObservable extends ObservableValue {
constructor(sessionViewModel, roomId) { constructor(sessionViewModel, roomIdOrLocalId) {
super(null); super(null);
this._statusSubscription = null; this._statusSubscription = null;
this._sessionViewModel = sessionViewModel; this._sessionViewModel = sessionViewModel;
this.id = roomId; this.id = roomIdOrLocalId;
} }
/** /**
@ -59,11 +60,24 @@ export class RoomViewModelObservable extends ObservableValue {
} }
async _statusToViewModel(status) { async _statusToViewModel(status) {
if (status.invited) { console.log("RoomViewModelObservable received status", status);
if (status & RoomStatus.Replaced) {
console.log("replaced!");
if (status & RoomStatus.BeingCreated) {
const {session} = this._sessionViewModel._client;
const roomBeingCreated = session.roomsBeingCreated.get(this.id);
console.log("new id is", roomBeingCreated.roomId);
this._sessionViewModel.navigation.push("room", roomBeingCreated.roomId);
} else {
throw new Error("Don't know how to replace a room with this status: " + (status ^ RoomStatus.Replaced));
}
} else if (status & RoomStatus.BeingCreated) {
return this._sessionViewModel._createRoomBeingCreatedViewModel(this.id);
} else if (status & RoomStatus.Invited) {
return this._sessionViewModel._createInviteViewModel(this.id); return this._sessionViewModel._createInviteViewModel(this.id);
} else if (status.joined) { } else if (status & RoomStatus.Joined) {
return this._sessionViewModel._createRoomViewModel(this.id); return this._sessionViewModel._createRoomViewModel(this.id);
} else if (status.archived) { } else if (status & RoomStatus.Archived) {
return await this._sessionViewModel._createArchivedRoomViewModel(this.id); return await this._sessionViewModel._createArchivedRoomViewModel(this.id);
} else { } else {
return this._sessionViewModel._createUnknownRoomViewModel(this.id); return this._sessionViewModel._createUnknownRoomViewModel(this.id);

View file

@ -19,6 +19,7 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js";
import {RoomViewModel} from "./room/RoomViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js";
import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js";
import {InviteViewModel} from "./room/InviteViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js";
import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js";
import {LightboxViewModel} from "./room/LightboxViewModel.js"; import {LightboxViewModel} from "./room/LightboxViewModel.js";
import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js";
@ -37,10 +38,7 @@ export class SessionViewModel extends ViewModel {
reconnector: client.reconnector, reconnector: client.reconnector,
session: client.session, session: client.session,
}))); })));
this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({ this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({session: this._client.session})));
invites: this._client.session.invites,
rooms: this._client.session.rooms
})));
this._settingsViewModel = null; this._settingsViewModel = null;
this._roomViewModelObservable = null; this._roomViewModelObservable = null;
this._gridViewModel = null; this._gridViewModel = null;
@ -200,6 +198,17 @@ export class SessionViewModel extends ViewModel {
return null; return null;
} }
_createRoomBeingCreatedViewModel(localId) {
const roomBeingCreated = this._client.session.roomsBeingCreated.get(localId);
if (roomBeingCreated) {
return new RoomBeingCreatedViewModel(this.childOptions({
roomBeingCreated,
mediaRepository: this._client.session.mediaRepository,
}));
}
return null;
}
_updateRoom(roomId) { _updateRoom(roomId) {
// opening a room and already open? // opening a room and already open?
if (this._roomViewModelObservable?.id === roomId) { if (this._roomViewModelObservable?.id === roomId) {
@ -263,7 +272,7 @@ export class SessionViewModel extends ViewModel {
const enable = !!this.navigation.path.get("right-panel")?.value; const enable = !!this.navigation.path.get("right-panel")?.value;
if (enable) { if (enable) {
const room = this._roomFromNavigation(); const room = this._roomFromNavigation();
this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room}))); this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room, session: this._client.session})));
} }
this.emitChange("rightPanelViewModel"); this.emitChange("rightPanelViewModel");
} }

View file

@ -18,6 +18,7 @@ limitations under the License.
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel.js";
import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {RoomTileViewModel} from "./RoomTileViewModel.js";
import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js";
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
import {RoomFilter} from "./RoomFilter.js"; import {RoomFilter} from "./RoomFilter.js";
import {ApplyMap} from "../../../observable/map/ApplyMap.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js";
import {addPanelIfNeeded} from "../../navigation/index.js"; import {addPanelIfNeeded} from "../../navigation/index.js";
@ -25,8 +26,8 @@ import {addPanelIfNeeded} from "../../navigation/index.js";
export class LeftPanelViewModel extends ViewModel { export class LeftPanelViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
const {rooms, invites} = options; const {session} = options;
this._tileViewModelsMap = this._mapTileViewModels(rooms, invites); this._tileViewModelsMap = this._mapTileViewModels(session.roomsBeingCreated, session.invites, session.rooms);
this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap); this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap);
this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b)); this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b));
this._currentTileVM = null; this._currentTileVM = null;
@ -35,16 +36,18 @@ export class LeftPanelViewModel extends ViewModel {
this._settingsUrl = this.urlCreator.urlForSegment("settings"); this._settingsUrl = this.urlCreator.urlForSegment("settings");
} }
_mapTileViewModels(rooms, invites) { _mapTileViewModels(roomsBeingCreated, invites, rooms) {
// join is not commutative, invites will take precedence over rooms // join is not commutative, invites will take precedence over rooms
return invites.join(rooms).mapValues((roomOrInvite, emitChange) => { return roomsBeingCreated.join(invites).join(rooms).mapValues((item, emitChange) => {
let vm; let vm;
if (roomOrInvite.isInvite) { if (item.isBeingCreated) {
vm = new InviteTileViewModel(this.childOptions({invite: roomOrInvite, emitChange})); vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange}));
} else if (item.isInvite) {
vm = new InviteTileViewModel(this.childOptions({invite: item, emitChange}));
} else { } else {
vm = new RoomTileViewModel(this.childOptions({room: roomOrInvite, emitChange})); vm = new RoomTileViewModel(this.childOptions({room: item, emitChange}));
} }
const isOpen = this.navigation.path.get("room")?.value === roomOrInvite.id; const isOpen = this.navigation.path.get("room")?.value === item.id;
if (isOpen) { if (isOpen) {
vm.open(); vm.open();
this._updateCurrentVM(vm); this._updateCurrentVM(vm);

View file

@ -0,0 +1,53 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020, 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 {BaseTileViewModel} from "./BaseTileViewModel.js";
export class RoomBeingCreatedTileViewModel extends BaseTileViewModel {
constructor(options) {
super(options);
const {roomBeingCreated} = options;
this._roomBeingCreated = roomBeingCreated;
this._url = this.urlCreator.openRoomActionUrl(this._roomBeingCreated.localId);
}
get busy() { return true; }
get kind() {
return "roomBeingCreated";
}
get url() {
return this._url;
}
compare(other) {
const parentComparison = super.compare(other);
if (parentComparison !== 0) {
return parentComparison;
}
return other._roomBeingCreated.name.localeCompare(this._roomBeingCreated.name);
}
get name() {
return this._roomBeingCreated.name;
}
get _avatarSource() {
return this._roomBeingCreated;
}
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import {ViewModel} from "../../ViewModel.js"; import {ViewModel} from "../../ViewModel.js";
import {RoomType} from "../../../matrix/room/create";
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
export class MemberDetailsViewModel extends ViewModel { export class MemberDetailsViewModel extends ViewModel {
@ -25,6 +26,7 @@ export class MemberDetailsViewModel extends ViewModel {
this._member = this._observableMember.get(); this._member = this._observableMember.get();
this._isEncrypted = options.isEncrypted; this._isEncrypted = options.isEncrypted;
this._powerLevelsObservable = options.powerLevelsObservable; this._powerLevelsObservable = options.powerLevelsObservable;
this._session = options.session;
this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange()));
this.track(this._observableMember.subscribe( () => this._onMemberChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange()));
} }
@ -79,4 +81,9 @@ export class MemberDetailsViewModel extends ViewModel {
get linkToUser() { get linkToUser() {
return `https://matrix.to/#/${this._member.userId}`; return `https://matrix.to/#/${this._member.userId}`;
} }
async openDirectMessage() {
const localId = await this._session.createRoom(RoomType.DirectMessage, undefined, undefined, undefined, [this.userId]);
this.navigation.push("room", localId);
}
} }

View file

@ -23,6 +23,7 @@ export class RightPanelViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
this._room = options.room; this._room = options.room;
this._session = options.session;
this._members = null; this._members = null;
this._setupNavigation(); this._setupNavigation();
} }
@ -48,7 +49,13 @@ export class RightPanelViewModel extends ViewModel {
} }
const isEncrypted = this._room.isEncrypted; const isEncrypted = this._room.isEncrypted;
const powerLevelsObservable = await this._room.observePowerLevels(); const powerLevelsObservable = await this._room.observePowerLevels();
return {observableMember, isEncrypted, powerLevelsObservable, mediaRepository: this._room.mediaRepository}; return {
observableMember,
isEncrypted,
powerLevelsObservable,
mediaRepository: this._room.mediaRepository,
session: this._session
};
} }
_setupNavigation() { _setupNavigation() {

View file

@ -1,9 +1,17 @@
# "Room" view models # "Room" view models
InviteViewModel and RoomViewModel are interchangebly used as "room view model": InviteViewModel, RoomViewModel and RoomBeingCreatedViewModel are interchangebly used as "room view model":
- SessionViewModel.roomViewModel can be an instance of either - SessionViewModel.roomViewModel can be an instance of any
- RoomGridViewModel.roomViewModelAt(i) can return an instance of either - RoomGridViewModel.roomViewModelAt(i) can return an instance of any
This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel. This is because they are accessed by the same url and need to transition into each other, in these two locations. Having two methods, especially in RoomGridViewModel would have been more cumbersome, even though this is not in line with how different view models are exposed in SessionViewModel.
They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method. They share an `id` and `kind` property, the latter can be used to differentiate them from the view, and a `focus` method.
Once we convert this folder to typescript, we should use this interface for all the view models:
```ts
interface IGridItemViewModel {
id: string;
kind: string;
focus();
}
```

View file

@ -0,0 +1,65 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020, 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 {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js";
import {ViewModel} from "../../ViewModel.js";
export class RoomBeingCreatedViewModel extends ViewModel {
constructor(options) {
super(options);
const {roomBeingCreated, mediaRepository} = options;
this._roomBeingCreated = roomBeingCreated;
this._mediaRepository = mediaRepository;
this._onRoomChange = this._onRoomChange.bind(this);
this._closeUrl = this.urlCreator.urlUntilSegment("session");
this._roomBeingCreated.on("change", this._onRoomChange);
}
get kind() { return "roomBeingCreated"; }
get closeUrl() { return this._closeUrl; }
get name() { return this._roomBeingCreated.name; }
get id() { return this._roomBeingCreated.localId; }
get isEncrypted() { return this._roomBeingCreated.isEncrypted; }
get avatarLetter() {
return avatarInitials(this.name);
}
get avatarColorNumber() {
return getIdentifierColorNumber(this._roomBeingCreated.avatarColorId);
}
avatarUrl(size) {
return getAvatarHttpUrl(this._roomBeingCreated.avatarUrl, size, this.platform, this._mediaRepository);
}
get avatarTitle() {
return this.name;
}
focus() {}
_onRoomChange() {
this.emitChange();
}
dispose() {
super.dispose();
this._roomBeingCreated.off("change", this._onRoomChange);
}
}

View file

@ -17,7 +17,7 @@ limitations under the License.
import {Room} from "./room/Room.js"; import {Room} from "./room/Room.js";
import {ArchivedRoom} from "./room/ArchivedRoom.js"; import {ArchivedRoom} from "./room/ArchivedRoom.js";
import {RoomStatus} from "./room/RoomStatus.js"; import {RoomStatus} from "./room/RoomStatus";
import {RoomBeingCreated} from "./room/create"; import {RoomBeingCreated} from "./room/create";
import {Invite} from "./room/Invite.js"; import {Invite} from "./room/Invite.js";
import {Pusher} from "./push/Pusher"; import {Pusher} from "./push/Pusher";
@ -64,6 +64,12 @@ export class Session {
this._activeArchivedRooms = new Map(); this._activeArchivedRooms = new Map();
this._invites = new ObservableMap(); this._invites = new ObservableMap();
this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params); this._inviteUpdateCallback = (invite, params) => this._invites.update(invite.id, params);
this._roomsBeingCreatedUpdateCallback = (rbc, params) => {
this._roomsBeingCreated.update(rbc.localId, params);
if (rbc.roomId && !!this.rooms.get(rbc.roomId)) {
this._tryReplaceRoomBeingCreated(rbc.roomId);
}
};
this._roomsBeingCreated = new ObservableMap(); this._roomsBeingCreated = new ObservableMap();
this._user = new User(sessionInfo.userId); this._user = new User(sessionInfo.userId);
this._deviceMessageHandler = new DeviceMessageHandler({storage}); this._deviceMessageHandler = new DeviceMessageHandler({storage});
@ -586,14 +592,16 @@ export class Session {
return this._roomsBeingCreated; return this._roomsBeingCreated;
} }
createRoom(type, isEncrypted, explicitName, topic, invites) { createRoom(type, isEncrypted, explicitName, topic, invites, log = undefined) {
const localId = `room-being-created-${this.platform.random()}`; return this._platform.logger.wrapOrRun(log, "create room", log => {
const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites); const localId = `room-being-created-${this._platform.random()}`;
const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites, this._roomsBeingCreatedUpdateCallback, this._mediaRepository, log);
this._roomsBeingCreated.set(localId, roomBeingCreated); this._roomsBeingCreated.set(localId, roomBeingCreated);
this._platform.logger.runDetached("create room", async log => { log.wrapDetached("create room network", async log => {
roomBeingCreated.start(this._hsApi, log); roomBeingCreated.start(this._hsApi, log);
}); });
return localId; return localId;
});
} }
async obtainSyncLock(syncResponse) { async obtainSyncLock(syncResponse) {
@ -678,18 +686,29 @@ export class Session {
} }
} }
_tryReplaceRoomBeingCreated(roomId) {
console.trace("_tryReplaceRoomBeingCreated " + roomId);
for (const [,roomBeingCreated] of this._roomsBeingCreated) {
if (roomBeingCreated.roomId === roomId) {
const observableStatus = this._observedRoomStatus.get(roomBeingCreated.localId);
if (observableStatus) {
console.log("marking room as replaced", observableStatus.get());
observableStatus.set(observableStatus.get() | RoomStatus.Replaced);
} else {
console.log("no observableStatus");
}
this._roomsBeingCreated.remove(roomBeingCreated.localId);
return;
}
}
}
applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) { applyRoomCollectionChangesAfterSync(inviteStates, roomStates, archivedRoomStates) {
// update the collections after sync // update the collections after sync
for (const rs of roomStates) { for (const rs of roomStates) {
if (rs.shouldAdd) { if (rs.shouldAdd) {
this._rooms.add(rs.id, rs.room); this._rooms.add(rs.id, rs.room);
for (const roomBeingCreated of this._roomsBeingCreated) { this._tryReplaceRoomBeingCreated(rs.id);
if (roomBeingCreated.roomId === rs.id) {
roomBeingCreated.notifyJoinedRoom();
this._roomsBeingCreated.delete(roomBeingCreated.localId);
break;
}
}
} else if (rs.shouldRemove) { } else if (rs.shouldRemove) {
this._rooms.remove(rs.id); this._rooms.remove(rs.id);
} }
@ -707,12 +726,12 @@ export class Session {
if (this._observedRoomStatus.size !== 0) { if (this._observedRoomStatus.size !== 0) {
for (const ars of archivedRoomStates) { for (const ars of archivedRoomStates) {
if (ars.shouldAdd) { if (ars.shouldAdd) {
this._observedRoomStatus.get(ars.id)?.set(RoomStatus.archived); this._observedRoomStatus.get(ars.id)?.set(RoomStatus.Archived);
} }
} }
for (const rs of roomStates) { for (const rs of roomStates) {
if (rs.shouldAdd) { if (rs.shouldAdd) {
this._observedRoomStatus.get(rs.id)?.set(RoomStatus.joined); this._observedRoomStatus.get(rs.id)?.set(RoomStatus.Joined);
} }
} }
for (const is of inviteStates) { for (const is of inviteStates) {
@ -731,7 +750,7 @@ export class Session {
_forgetArchivedRoom(roomId) { _forgetArchivedRoom(roomId) {
const statusObservable = this._observedRoomStatus.get(roomId); const statusObservable = this._observedRoomStatus.get(roomId);
if (statusObservable) { if (statusObservable) {
statusObservable.set(statusObservable.get().withoutArchived()); statusObservable.set(statusObservable.get() ^ RoomStatus.Archived);
} }
} }
@ -820,21 +839,25 @@ export class Session {
} }
async getRoomStatus(roomId) { async getRoomStatus(roomId) {
const isBeingCreated = !!this._roomsBeingCreated.get(roomId);
if (isBeingCreated) {
return RoomStatus.BeingCreated;
}
const isJoined = !!this._rooms.get(roomId); const isJoined = !!this._rooms.get(roomId);
if (isJoined) { if (isJoined) {
return RoomStatus.joined; return RoomStatus.Joined;
} else { } else {
const isInvited = !!this._invites.get(roomId); const isInvited = !!this._invites.get(roomId);
const txn = await this._storage.readTxn([this._storage.storeNames.archivedRoomSummary]); const txn = await this._storage.readTxn([this._storage.storeNames.archivedRoomSummary]);
const isArchived = await txn.archivedRoomSummary.has(roomId); const isArchived = await txn.archivedRoomSummary.has(roomId);
if (isInvited && isArchived) { if (isInvited && isArchived) {
return RoomStatus.invitedAndArchived; return RoomStatus.Invited | RoomStatus.Archived;
} else if (isInvited) { } else if (isInvited) {
return RoomStatus.invited; return RoomStatus.Invited;
} else if (isArchived) { } else if (isArchived) {
return RoomStatus.archived; return RoomStatus.Archived;
} else { } else {
return RoomStatus.none; return RoomStatus.None;
} }
} }
} }
@ -843,9 +866,10 @@ export class Session {
let observable = this._observedRoomStatus.get(roomId); let observable = this._observedRoomStatus.get(roomId);
if (!observable) { if (!observable) {
const status = await this.getRoomStatus(roomId); const status = await this.getRoomStatus(roomId);
observable = new RetainedObservableValue(status, () => { observable = new FooRetainedObservableValue(status, () => {
this._observedRoomStatus.delete(roomId); this._observedRoomStatus.delete(roomId);
}); });
this._observedRoomStatus.set(roomId, observable); this._observedRoomStatus.set(roomId, observable);
} }
return observable; return observable;
@ -897,6 +921,13 @@ export class Session {
} }
} }
class FooRetainedObservableValue extends RetainedObservableValue {
set(value) {
console.log("setting room status to", value);
super.set(value);
}
}
export function tests() { export function tests() {
function createStorageMock(session, pendingEvents = []) { function createStorageMock(session, pendingEvents = []) {
return { return {

View file

@ -231,6 +231,7 @@ export class Room extends BaseRoom {
} }
} }
let emitChange = false; let emitChange = false;
console.log("Room summaryChanges", this.id, summaryChanges);
if (summaryChanges) { if (summaryChanges) {
this._summary.applyChanges(summaryChanges); this._summary.applyChanges(summaryChanges);
if (!this._summary.data.needsHeroes) { if (!this._summary.data.needsHeroes) {

View file

@ -1,61 +0,0 @@
/*
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.
*/
export class RoomStatus {
constructor(joined, invited, archived) {
this.joined = joined;
this.invited = invited;
this.archived = archived;
}
withInvited() {
if (this.invited) {
return this;
} else if (this.archived) {
return RoomStatus.invitedAndArchived;
} else {
return RoomStatus.invited;
}
}
withoutInvited() {
if (!this.invited) {
return this;
} else if (this.joined) {
return RoomStatus.joined;
} else if (this.archived) {
return RoomStatus.archived;
} else {
return RoomStatus.none;
}
}
withoutArchived() {
if (!this.archived) {
return this;
} else if (this.invited) {
return RoomStatus.invited;
} else {
return RoomStatus.none;
}
}
}
RoomStatus.joined = new RoomStatus(true, false, false);
RoomStatus.archived = new RoomStatus(false, false, true);
RoomStatus.invited = new RoomStatus(false, true, false);
RoomStatus.invitedAndArchived = new RoomStatus(false, true, true);
RoomStatus.none = new RoomStatus(false, false, false);

View file

@ -0,0 +1,24 @@
/*
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.
*/
export enum RoomStatus {
None = 1 << 0,
BeingCreated = 1 << 1,
Invited = 1 << 2,
Joined = 1 << 3,
Replaced = 1 << 4,
Archived = 1 << 5,
}

View file

@ -16,6 +16,7 @@ limitations under the License.
import {calculateRoomName} from "./members/Heroes"; import {calculateRoomName} from "./members/Heroes";
import {createRoomEncryptionEvent} from "../e2ee/common"; import {createRoomEncryptionEvent} from "../e2ee/common";
import {MediaRepository} from "../net/MediaRepository";
import {EventEmitter} from "../../utils/EventEmitter"; import {EventEmitter} from "../../utils/EventEmitter";
import type {StateEvent} from "../storage/types"; import type {StateEvent} from "../storage/types";
@ -58,32 +59,35 @@ function presetForType(type: RoomType): string {
} }
} }
export class RoomBeingCreated extends EventEmitter<{change: never, joined: string}> { export class RoomBeingCreated extends EventEmitter<{change: never}> {
private _roomId?: string; private _roomId?: string;
private profiles: Profile[] = []; private profiles: Profile[] = [];
public readonly isEncrypted: boolean; public readonly isEncrypted: boolean;
public readonly name: string; private _name: string;
constructor( constructor(
private readonly localId: string, public readonly localId: string,
private readonly type: RoomType, private readonly type: RoomType,
isEncrypted: boolean | undefined, isEncrypted: boolean | undefined,
private readonly explicitName: string | undefined, private readonly explicitName: string | undefined,
private readonly topic: string | undefined, private readonly topic: string | undefined,
private readonly inviteUserIds: string[] | undefined, private readonly inviteUserIds: string[] | undefined,
private readonly updateCallback,
public readonly mediaRepository: MediaRepository,
log: ILogItem log: ILogItem
) { ) {
super(); super();
this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted;
if (explicitName) { if (explicitName) {
this.name = explicitName; this._name = explicitName;
} else { } else {
const summaryData = { const summaryData = {
joinCount: 1, // ourselves joinCount: 1, // ourselves
inviteCount: (this.inviteUserIds?.length || 0) inviteCount: (this.inviteUserIds?.length || 0)
}; };
this.name = calculateRoomName(this.profiles, summaryData, log); const userIdProfiles = (inviteUserIds || []).map(userId => new UserIdProfile(userId));
this._name = calculateRoomName(userIdProfiles, summaryData, log);
} }
} }
@ -111,31 +115,52 @@ export class RoomBeingCreated extends EventEmitter<{change: never, joined: strin
if (this.isEncrypted) { if (this.isEncrypted) {
options.initial_state = [createRoomEncryptionEvent()]; options.initial_state = [createRoomEncryptionEvent()];
} }
console.log("going to create the room now");
const response = await hsApi.createRoom(options, {log}).response(); const response = await hsApi.createRoom(options, {log}).response();
this._roomId = response["room_id"]; this._roomId = response["room_id"];
this.emit("change"); console.log("done creating the room now", this._roomId);
// TODO: somehow check in Session if we need to replace this with a joined room
// in case the room appears first in sync, and this request returns later
this.emitChange();
} }
private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> { private async loadProfiles(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
// only load profiles if we need it for the room name and avatar // only load profiles if we need it for the room name and avatar
if (!this.explicitName && this.inviteUserIds) { if (!this.explicitName && this.inviteUserIds) {
this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log); this.profiles = await loadProfiles(this.inviteUserIds, hsApi, log);
this.emit("change"); console.log("loaded the profiles", this.profiles);
const summaryData = {
joinCount: 1, // ourselves
inviteCount: this.inviteUserIds.length
};
this._name = calculateRoomName(this.profiles, summaryData, log);
console.log("loaded the profiles and the new name", this.name);
this.emitChange();
} }
} }
notifyJoinedRoom() { private emitChange() {
this.emit("joined", this._roomId); this.updateCallback(this);
this.emit("change");
}
get avatarColorId(): string {
return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId;
} }
get avatarUrl(): string | undefined { get avatarUrl(): string | undefined {
return this.profiles[0]?.avatarUrl; const result = this.profiles[0]?.avatarUrl;
console.log("RoomBeingCreated.avatarUrl", this.profiles, result);
return result;
} }
get roomId(): string | undefined { get roomId(): string | undefined {
return this._roomId; return this._roomId;
} }
get name() { return this._name; }
get isBeingCreated(): boolean { return true; }
} }
export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<Profile[]> { export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise<Profile[]> {
@ -149,8 +174,8 @@ export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log:
interface IProfile { interface IProfile {
get userId(): string; get userId(): string;
get displayName(): string; get displayName(): string | undefined;
get avatarUrl(): string; get avatarUrl(): string | undefined;
get name(): string; get name(): string;
} }
@ -158,8 +183,16 @@ export class Profile implements IProfile {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly displayName: string, public readonly displayName: string,
public readonly avatarUrl: string public readonly avatarUrl: string | undefined
) {} ) {}
get name() { return this.displayName || this.userId; } get name() { return this.displayName || this.userId; }
} }
class UserIdProfile implements IProfile {
constructor(public readonly userId: string) {}
get displayName() { return undefined; }
get name() { return this.userId; }
get avatarUrl() { return undefined; }
}

View file

@ -15,6 +15,7 @@ limitations under the License.
*/ */
import {RoomView} from "./room/RoomView.js"; import {RoomView} from "./room/RoomView.js";
import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js";
import {InviteView} from "./room/InviteView.js"; import {InviteView} from "./room/InviteView.js";
import {TemplateView} from "../general/TemplateView"; import {TemplateView} from "../general/TemplateView";
import {StaticView} from "../general/StaticView.js"; import {StaticView} from "../general/StaticView.js";
@ -33,7 +34,9 @@ export class RoomGridView extends TemplateView {
}, },
}, t.mapView(vm => vm.roomViewModelAt(i), roomVM => { }, t.mapView(vm => vm.roomViewModelAt(i), roomVM => {
if (roomVM) { if (roomVM) {
if (roomVM.kind === "invite") { if (roomVM.kind === "roomBeingCreated") {
return new RoomBeingCreatedView(roomVM);
} else if (roomVM.kind === "invite") {
return new InviteView(roomVM); return new InviteView(roomVM);
} else { } else {
return new RoomView(roomVM); return new RoomView(roomVM);

View file

@ -18,6 +18,7 @@ limitations under the License.
import {LeftPanelView} from "./leftpanel/LeftPanelView.js"; import {LeftPanelView} from "./leftpanel/LeftPanelView.js";
import {RoomView} from "./room/RoomView.js"; import {RoomView} from "./room/RoomView.js";
import {UnknownRoomView} from "./room/UnknownRoomView.js"; import {UnknownRoomView} from "./room/UnknownRoomView.js";
import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js";
import {InviteView} from "./room/InviteView.js"; import {InviteView} from "./room/InviteView.js";
import {LightboxView} from "./room/LightboxView.js"; import {LightboxView} from "./room/LightboxView.js";
import {TemplateView} from "../general/TemplateView"; import {TemplateView} from "../general/TemplateView";
@ -48,6 +49,8 @@ export class SessionView extends TemplateView {
return new InviteView(vm.currentRoomViewModel); return new InviteView(vm.currentRoomViewModel);
} else if (vm.currentRoomViewModel.kind === "room") { } else if (vm.currentRoomViewModel.kind === "room") {
return new RoomView(vm.currentRoomViewModel); return new RoomView(vm.currentRoomViewModel);
} else if (vm.currentRoomViewModel.kind === "roomBeingCreated") {
return new RoomBeingCreatedView(vm.currentRoomViewModel);
} else { } else {
return new UnknownRoomView(vm.currentRoomViewModel); return new UnknownRoomView(vm.currentRoomViewModel);
} }

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import {TemplateView} from "../../general/TemplateView"; import {TemplateView} from "../../general/TemplateView";
import {renderStaticAvatar} from "../../avatar.js"; import {AvatarView} from "../../AvatarView";
import {spinner} from "../../common.js"; import {spinner} from "../../common.js";
export class InviteTileView extends TemplateView { export class InviteTileView extends TemplateView {
@ -27,9 +27,9 @@ export class InviteTileView extends TemplateView {
}; };
return t.li({"className": classes}, [ return t.li({"className": classes}, [
t.a({href: vm.url}, [ t.a({href: vm.url}, [
renderStaticAvatar(vm, 32), t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}),
t.div({className: "description"}, [ t.div({className: "description"}, [
t.div({className: "name"}, vm.name), t.div({className: "name"}, vm => vm.name),
t.map(vm => vm.busy, busy => { t.map(vm => vm.busy, busy => {
if (busy) { if (busy) {
return spinner(t); return spinner(t);
@ -41,4 +41,10 @@ export class InviteTileView extends TemplateView {
]) ])
]); ]);
} }
update(value, props) {
super.update(value);
// update the AvatarView as we told it to not subscribe itself with parentProvidesUpdates
this.updateSubViews(value, props);
}
} }

View file

@ -64,7 +64,7 @@ export class LeftPanelView extends TemplateView {
list: vm.tileViewModels, list: vm.tileViewModels,
}, },
tileVM => { tileVM => {
if (tileVM.kind === "invite") { if (tileVM.kind === "invite" || tileVM.kind === "roomBeingCreated") {
return new InviteTileView(tileVM); return new InviteTileView(tileVM);
} else { } else {
return new RoomTileView(tileVM); return new RoomTileView(tileVM);

View file

@ -46,7 +46,8 @@ export class MemberDetailsView extends TemplateView {
t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`), t.div({className: "MemberDetailsView_label"}, vm.i18n`Options`),
t.div({className: "MemberDetailsView_options"}, t.div({className: "MemberDetailsView_options"},
[ [
t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`) t.a({href: vm.linkToUser, target: "_blank", rel: "noopener"}, vm.i18n`Open Link to User`),
t.button({className: "text", onClick: () => vm.openDirectMessage()}, vm.i18n`Open direct message`)
]) ])
]); ]);
} }

View file

@ -39,7 +39,7 @@ export class MessageComposer extends TemplateView {
this._clearHeight(); this._clearHeight();
} }
}, },
placeholder: vm.isEncrypted ? "Send an encrypted message…" : "Send a message…", placeholder: vm => vm.isEncrypted ? "Send an encrypted message…" : "Send a message…",
rows: "1" rows: "1"
}); });
this._focusInput = () => this._input.focus(); this._focusInput = () => this._input.focus();

View file

@ -0,0 +1,25 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Copyright 2020 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 {TemplateView} from "../../general/TemplateView";
import {renderStaticAvatar} from "../../avatar.js";
export class RoomBeingCreatedView extends TemplateView {
render(t, vm) {
return t.h1({className: "middle"}, ["creating room", vm => vm.name]);
}
}