diff --git a/src/domain/session/RoomViewModelObservable.js b/src/domain/session/RoomViewModelObservable.js index 8d576a98..c16aaf83 100644 --- a/src/domain/session/RoomViewModelObservable.js +++ b/src/domain/session/RoomViewModelObservable.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ObservableValue} from "../../observable/ObservableValue"; +import {RoomStatus} from "../../matrix/room/RoomStatus"; /** 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. */ export class RoomViewModelObservable extends ObservableValue { - constructor(sessionViewModel, roomId) { + constructor(sessionViewModel, roomIdOrLocalId) { super(null); this._statusSubscription = null; this._sessionViewModel = sessionViewModel; - this.id = roomId; + this.id = roomIdOrLocalId; } /** @@ -59,11 +60,24 @@ export class RoomViewModelObservable extends ObservableValue { } 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); - } else if (status.joined) { + } else if (status & RoomStatus.Joined) { return this._sessionViewModel._createRoomViewModel(this.id); - } else if (status.archived) { + } else if (status & RoomStatus.Archived) { return await this._sessionViewModel._createArchivedRoomViewModel(this.id); } else { return this._sessionViewModel._createUnknownRoomViewModel(this.id); diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 822a4d31..11126ca9 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -19,6 +19,7 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js"; +import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js"; import {LightboxViewModel} from "./room/LightboxViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; @@ -37,10 +38,7 @@ export class SessionViewModel extends ViewModel { reconnector: client.reconnector, session: client.session, }))); - this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({ - invites: this._client.session.invites, - rooms: this._client.session.rooms - }))); + this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({session: this._client.session}))); this._settingsViewModel = null; this._roomViewModelObservable = null; this._gridViewModel = null; @@ -200,6 +198,17 @@ export class SessionViewModel extends ViewModel { 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) { // opening a room and already open? if (this._roomViewModelObservable?.id === roomId) { @@ -263,7 +272,7 @@ export class SessionViewModel extends ViewModel { const enable = !!this.navigation.path.get("right-panel")?.value; if (enable) { 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"); } diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 59b68ca1..d85e5943 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -18,6 +18,7 @@ limitations under the License. import {ViewModel} from "../../ViewModel.js"; import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; +import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; import {addPanelIfNeeded} from "../../navigation/index.js"; @@ -25,8 +26,8 @@ import {addPanelIfNeeded} from "../../navigation/index.js"; export class LeftPanelViewModel extends ViewModel { constructor(options) { super(options); - const {rooms, invites} = options; - this._tileViewModelsMap = this._mapTileViewModels(rooms, invites); + const {session} = options; + this._tileViewModelsMap = this._mapTileViewModels(session.roomsBeingCreated, session.invites, session.rooms); this._tileViewModelsFilterMap = new ApplyMap(this._tileViewModelsMap); this._tileViewModels = this._tileViewModelsFilterMap.sortValues((a, b) => a.compare(b)); this._currentTileVM = null; @@ -35,16 +36,18 @@ export class LeftPanelViewModel extends ViewModel { this._settingsUrl = this.urlCreator.urlForSegment("settings"); } - _mapTileViewModels(rooms, invites) { + _mapTileViewModels(roomsBeingCreated, invites, 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; - if (roomOrInvite.isInvite) { - vm = new InviteTileViewModel(this.childOptions({invite: roomOrInvite, emitChange})); + if (item.isBeingCreated) { + vm = new RoomBeingCreatedTileViewModel(this.childOptions({roomBeingCreated: item, emitChange})); + } else if (item.isInvite) { + vm = new InviteTileViewModel(this.childOptions({invite: item, emitChange})); } 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) { vm.open(); this._updateCurrentVM(vm); diff --git a/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js new file mode 100644 index 00000000..8c629892 --- /dev/null +++ b/src/domain/session/leftpanel/RoomBeingCreatedTileViewModel.js @@ -0,0 +1,53 @@ +/* +Copyright 2020 Bruno Windels +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; + } +} diff --git a/src/domain/session/rightpanel/MemberDetailsViewModel.js b/src/domain/session/rightpanel/MemberDetailsViewModel.js index 0303e05d..6effb90c 100644 --- a/src/domain/session/rightpanel/MemberDetailsViewModel.js +++ b/src/domain/session/rightpanel/MemberDetailsViewModel.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel.js"; +import {RoomType} from "../../../matrix/room/create"; import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class MemberDetailsViewModel extends ViewModel { @@ -25,6 +26,7 @@ export class MemberDetailsViewModel extends ViewModel { this._member = this._observableMember.get(); this._isEncrypted = options.isEncrypted; this._powerLevelsObservable = options.powerLevelsObservable; + this._session = options.session; this.track(this._powerLevelsObservable.subscribe(() => this._onPowerLevelsChange())); this.track(this._observableMember.subscribe( () => this._onMemberChange())); } @@ -79,4 +81,9 @@ export class MemberDetailsViewModel extends ViewModel { get linkToUser() { 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); + } } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 670df868..3cfe378b 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -23,6 +23,7 @@ export class RightPanelViewModel extends ViewModel { constructor(options) { super(options); this._room = options.room; + this._session = options.session; this._members = null; this._setupNavigation(); } @@ -48,7 +49,13 @@ export class RightPanelViewModel extends ViewModel { } const isEncrypted = this._room.isEncrypted; 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() { diff --git a/src/domain/session/room/README.md b/src/domain/session/room/README.md index adb673eb..dbbe4dca 100644 --- a/src/domain/session/room/README.md +++ b/src/domain/session/room/README.md @@ -1,9 +1,17 @@ # "Room" view models -InviteViewModel and RoomViewModel are interchangebly used as "room view model": - - SessionViewModel.roomViewModel can be an instance of either - - RoomGridViewModel.roomViewModelAt(i) can return an instance of either +InviteViewModel, RoomViewModel and RoomBeingCreatedViewModel are interchangebly used as "room view model": + - SessionViewModel.roomViewModel can be an instance of any + - 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. 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(); +} +``` diff --git a/src/domain/session/room/RoomBeingCreatedViewModel.js b/src/domain/session/room/RoomBeingCreatedViewModel.js new file mode 100644 index 00000000..eebbb668 --- /dev/null +++ b/src/domain/session/room/RoomBeingCreatedViewModel.js @@ -0,0 +1,65 @@ +/* +Copyright 2020 Bruno Windels +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); + } +} + diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f8d42960..49ae0f9c 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -17,7 +17,7 @@ limitations under the License. import {Room} from "./room/Room.js"; import {ArchivedRoom} from "./room/ArchivedRoom.js"; -import {RoomStatus} from "./room/RoomStatus.js"; +import {RoomStatus} from "./room/RoomStatus"; import {RoomBeingCreated} from "./room/create"; import {Invite} from "./room/Invite.js"; import {Pusher} from "./push/Pusher"; @@ -64,6 +64,12 @@ export class Session { this._activeArchivedRooms = new Map(); this._invites = new ObservableMap(); 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._user = new User(sessionInfo.userId); this._deviceMessageHandler = new DeviceMessageHandler({storage}); @@ -586,14 +592,16 @@ export class Session { return this._roomsBeingCreated; } - createRoom(type, isEncrypted, explicitName, topic, invites) { - const localId = `room-being-created-${this.platform.random()}`; - const roomBeingCreated = new RoomBeingCreated(localId, type, isEncrypted, explicitName, topic, invites); - this._roomsBeingCreated.set(localId, roomBeingCreated); - this._platform.logger.runDetached("create room", async log => { - roomBeingCreated.start(this._hsApi, log); + createRoom(type, isEncrypted, explicitName, topic, invites, log = undefined) { + return this._platform.logger.wrapOrRun(log, "create room", log => { + 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); + log.wrapDetached("create room network", async log => { + roomBeingCreated.start(this._hsApi, log); + }); + return localId; }); - return localId; } 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) { // update the collections after sync for (const rs of roomStates) { if (rs.shouldAdd) { this._rooms.add(rs.id, rs.room); - for (const roomBeingCreated of this._roomsBeingCreated) { - if (roomBeingCreated.roomId === rs.id) { - roomBeingCreated.notifyJoinedRoom(); - this._roomsBeingCreated.delete(roomBeingCreated.localId); - break; - } - } + this._tryReplaceRoomBeingCreated(rs.id); } else if (rs.shouldRemove) { this._rooms.remove(rs.id); } @@ -707,12 +726,12 @@ export class Session { if (this._observedRoomStatus.size !== 0) { for (const ars of archivedRoomStates) { if (ars.shouldAdd) { - this._observedRoomStatus.get(ars.id)?.set(RoomStatus.archived); + this._observedRoomStatus.get(ars.id)?.set(RoomStatus.Archived); } } for (const rs of roomStates) { if (rs.shouldAdd) { - this._observedRoomStatus.get(rs.id)?.set(RoomStatus.joined); + this._observedRoomStatus.get(rs.id)?.set(RoomStatus.Joined); } } for (const is of inviteStates) { @@ -731,7 +750,7 @@ export class Session { _forgetArchivedRoom(roomId) { const statusObservable = this._observedRoomStatus.get(roomId); if (statusObservable) { - statusObservable.set(statusObservable.get().withoutArchived()); + statusObservable.set(statusObservable.get() ^ RoomStatus.Archived); } } @@ -820,21 +839,25 @@ export class Session { } async getRoomStatus(roomId) { + const isBeingCreated = !!this._roomsBeingCreated.get(roomId); + if (isBeingCreated) { + return RoomStatus.BeingCreated; + } const isJoined = !!this._rooms.get(roomId); if (isJoined) { - return RoomStatus.joined; + return RoomStatus.Joined; } else { const isInvited = !!this._invites.get(roomId); const txn = await this._storage.readTxn([this._storage.storeNames.archivedRoomSummary]); const isArchived = await txn.archivedRoomSummary.has(roomId); if (isInvited && isArchived) { - return RoomStatus.invitedAndArchived; + return RoomStatus.Invited | RoomStatus.Archived; } else if (isInvited) { - return RoomStatus.invited; + return RoomStatus.Invited; } else if (isArchived) { - return RoomStatus.archived; + return RoomStatus.Archived; } else { - return RoomStatus.none; + return RoomStatus.None; } } } @@ -843,9 +866,10 @@ export class Session { let observable = this._observedRoomStatus.get(roomId); if (!observable) { const status = await this.getRoomStatus(roomId); - observable = new RetainedObservableValue(status, () => { + observable = new FooRetainedObservableValue(status, () => { this._observedRoomStatus.delete(roomId); }); + this._observedRoomStatus.set(roomId, 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() { function createStorageMock(session, pendingEvents = []) { return { diff --git a/src/matrix/room/Room.js b/src/matrix/room/Room.js index aaf66be1..46c06af0 100644 --- a/src/matrix/room/Room.js +++ b/src/matrix/room/Room.js @@ -231,6 +231,7 @@ export class Room extends BaseRoom { } } let emitChange = false; + console.log("Room summaryChanges", this.id, summaryChanges); if (summaryChanges) { this._summary.applyChanges(summaryChanges); if (!this._summary.data.needsHeroes) { diff --git a/src/matrix/room/RoomStatus.js b/src/matrix/room/RoomStatus.js deleted file mode 100644 index 884103e2..00000000 --- a/src/matrix/room/RoomStatus.js +++ /dev/null @@ -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); diff --git a/src/matrix/room/RoomStatus.ts b/src/matrix/room/RoomStatus.ts new file mode 100644 index 00000000..f66f59d7 --- /dev/null +++ b/src/matrix/room/RoomStatus.ts @@ -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, +} diff --git a/src/matrix/room/create.ts b/src/matrix/room/create.ts index 934650c0..24adfcf1 100644 --- a/src/matrix/room/create.ts +++ b/src/matrix/room/create.ts @@ -16,6 +16,7 @@ limitations under the License. import {calculateRoomName} from "./members/Heroes"; import {createRoomEncryptionEvent} from "../e2ee/common"; +import {MediaRepository} from "../net/MediaRepository"; import {EventEmitter} from "../../utils/EventEmitter"; 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 profiles: Profile[] = []; public readonly isEncrypted: boolean; - public readonly name: string; + private _name: string; constructor( - private readonly localId: string, + public readonly localId: string, private readonly type: RoomType, isEncrypted: boolean | undefined, private readonly explicitName: string | undefined, private readonly topic: string | undefined, private readonly inviteUserIds: string[] | undefined, + private readonly updateCallback, + public readonly mediaRepository: MediaRepository, log: ILogItem ) { super(); this.isEncrypted = isEncrypted === undefined ? defaultE2EEStatusForType(this.type) : isEncrypted; if (explicitName) { - this.name = explicitName; + this._name = explicitName; } else { const summaryData = { joinCount: 1, // ourselves 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) { options.initial_state = [createRoomEncryptionEvent()]; } - + console.log("going to create the room now"); const response = await hsApi.createRoom(options, {log}).response(); 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 { // only load profiles if we need it for the room name and avatar if (!this.explicitName && this.inviteUserIds) { 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() { - this.emit("joined", this._roomId); + private emitChange() { + this.updateCallback(this); + this.emit("change"); + } + + get avatarColorId(): string { + return this.inviteUserIds?.[0] ?? this._roomId ?? this.localId; } 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 { return this._roomId; } + + get name() { return this._name; } + + get isBeingCreated(): boolean { return true; } } export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: ILogItem): Promise { @@ -149,8 +174,8 @@ export async function loadProfiles(userIds: string[], hsApi: HomeServerApi, log: interface IProfile { get userId(): string; - get displayName(): string; - get avatarUrl(): string; + get displayName(): string | undefined; + get avatarUrl(): string | undefined; get name(): string; } @@ -158,8 +183,16 @@ export class Profile implements IProfile { constructor( public readonly userId: string, public readonly displayName: string, - public readonly avatarUrl: string + public readonly avatarUrl: string | undefined ) {} 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; } + +} diff --git a/src/platform/web/ui/session/RoomGridView.js b/src/platform/web/ui/session/RoomGridView.js index fc6497da..79fc3d21 100644 --- a/src/platform/web/ui/session/RoomGridView.js +++ b/src/platform/web/ui/session/RoomGridView.js @@ -15,6 +15,7 @@ limitations under the License. */ import {RoomView} from "./room/RoomView.js"; +import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js"; import {InviteView} from "./room/InviteView.js"; import {TemplateView} from "../general/TemplateView"; import {StaticView} from "../general/StaticView.js"; @@ -33,7 +34,9 @@ export class RoomGridView extends TemplateView { }, }, t.mapView(vm => vm.roomViewModelAt(i), roomVM => { if (roomVM) { - if (roomVM.kind === "invite") { + if (roomVM.kind === "roomBeingCreated") { + return new RoomBeingCreatedView(roomVM); + } else if (roomVM.kind === "invite") { return new InviteView(roomVM); } else { return new RoomView(roomVM); diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index f2ac2971..3740ffca 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -18,6 +18,7 @@ limitations under the License. import {LeftPanelView} from "./leftpanel/LeftPanelView.js"; import {RoomView} from "./room/RoomView.js"; import {UnknownRoomView} from "./room/UnknownRoomView.js"; +import {RoomBeingCreatedView} from "./room/RoomBeingCreatedView.js"; import {InviteView} from "./room/InviteView.js"; import {LightboxView} from "./room/LightboxView.js"; import {TemplateView} from "../general/TemplateView"; @@ -48,6 +49,8 @@ export class SessionView extends TemplateView { return new InviteView(vm.currentRoomViewModel); } else if (vm.currentRoomViewModel.kind === "room") { return new RoomView(vm.currentRoomViewModel); + } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { + return new RoomBeingCreatedView(vm.currentRoomViewModel); } else { return new UnknownRoomView(vm.currentRoomViewModel); } diff --git a/src/platform/web/ui/session/leftpanel/InviteTileView.js b/src/platform/web/ui/session/leftpanel/InviteTileView.js index b99ab1c6..de064bed 100644 --- a/src/platform/web/ui/session/leftpanel/InviteTileView.js +++ b/src/platform/web/ui/session/leftpanel/InviteTileView.js @@ -16,7 +16,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; -import {renderStaticAvatar} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView"; import {spinner} from "../../common.js"; export class InviteTileView extends TemplateView { @@ -27,9 +27,9 @@ export class InviteTileView extends TemplateView { }; return t.li({"className": classes}, [ t.a({href: vm.url}, [ - renderStaticAvatar(vm, 32), + t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}), t.div({className: "description"}, [ - t.div({className: "name"}, vm.name), + t.div({className: "name"}, vm => vm.name), t.map(vm => vm.busy, busy => { if (busy) { 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); + } } diff --git a/src/platform/web/ui/session/leftpanel/LeftPanelView.js b/src/platform/web/ui/session/leftpanel/LeftPanelView.js index 481c5362..bc14fa1e 100644 --- a/src/platform/web/ui/session/leftpanel/LeftPanelView.js +++ b/src/platform/web/ui/session/leftpanel/LeftPanelView.js @@ -64,7 +64,7 @@ export class LeftPanelView extends TemplateView { list: vm.tileViewModels, }, tileVM => { - if (tileVM.kind === "invite") { + if (tileVM.kind === "invite" || tileVM.kind === "roomBeingCreated") { return new InviteTileView(tileVM); } else { return new RoomTileView(tileVM); diff --git a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js index c249515a..5d2f9387 100644 --- a/src/platform/web/ui/session/rightpanel/MemberDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/MemberDetailsView.js @@ -46,7 +46,8 @@ export class MemberDetailsView extends TemplateView { t.div({className: "MemberDetailsView_label"}, vm.i18n`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`) ]) ]); } diff --git a/src/platform/web/ui/session/room/MessageComposer.js b/src/platform/web/ui/session/room/MessageComposer.js index f92c4403..9c67fa9f 100644 --- a/src/platform/web/ui/session/room/MessageComposer.js +++ b/src/platform/web/ui/session/room/MessageComposer.js @@ -39,7 +39,7 @@ export class MessageComposer extends TemplateView { 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" }); this._focusInput = () => this._input.focus(); diff --git a/src/platform/web/ui/session/room/RoomBeingCreatedView.js b/src/platform/web/ui/session/room/RoomBeingCreatedView.js new file mode 100644 index 00000000..df26a43d --- /dev/null +++ b/src/platform/web/ui/session/room/RoomBeingCreatedView.js @@ -0,0 +1,25 @@ +/* +Copyright 2020 Bruno Windels +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]); + } +}