From d468abd38780fb2a9794dae4d5ffb64f81e953aa Mon Sep 17 00:00:00 2001 From: Ashfame Date: Mon, 9 Jan 2023 19:14:25 +0400 Subject: [PATCH 1/9] define PeekableRoomView and make UnknownRoomViewModel handle it --- src/domain/session/SessionViewModel.js | 12 +++++++++++- .../session/room/UnknownRoomViewModel.js | 5 +++-- src/matrix/Session.js | 7 +++++++ src/matrix/net/HomeServerApi.ts | 4 ++++ src/platform/web/ui/session/SessionView.js | 3 +++ .../web/ui/session/room/PeekableRoomView.js | 19 +++++++++++++++++++ 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/platform/web/ui/session/room/PeekableRoomView.js diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 7d1dac3ce6..c2f276769d 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -231,10 +231,20 @@ export class SessionViewModel extends ViewModel { return null; } - _createUnknownRoomViewModel(roomIdOrAlias) { + async _createUnknownRoomViewModel(roomIdOrAlias) { + let roomId; + if ( roomIdOrAlias[0] !== "!" ) { + let response = await this._client._requestScheduler.hsApi.resolveRoomAlias(roomIdOrAlias).response(); + roomId = response.room_id; + } else { + roomId = roomIdOrAlias; + } + const peekable = await this._client.session.canPeekInRoom(roomId); + return new UnknownRoomViewModel(this.childOptions({ roomIdOrAlias, session: this._client.session, + peekable: peekable })); } diff --git a/src/domain/session/room/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index 8bb5fb0af9..2e8cb50853 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -19,9 +19,10 @@ import {ViewModel} from "../../ViewModel"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { super(options); - const {roomIdOrAlias, session} = options; + const {roomIdOrAlias, session, peekable} = options; this._session = session; this.roomIdOrAlias = roomIdOrAlias; + this._peekable = peekable; this._error = null; this._busy = false; } @@ -53,6 +54,6 @@ export class UnknownRoomViewModel extends ViewModel { } get kind() { - return "unknown"; + return this._peekable ? "peekable" : "unknown"; } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 35f713f658..195c974e51 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1037,6 +1037,13 @@ export class Session { return body.room_id; }); } + + canPeekInRoom(roomId, log = null) { + return this._platform.logger.wrapOrRun(log, "canPeekInRoom", async log => { + const body = await this._hsApi.state(roomId, 'm.room.history_visibility', '', {log}).response(); + return body.history_visibility === 'world_readable'; + }); + } } import {FeatureSet} from "../features"; diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index c5f9055504..6a8a13b10d 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -129,6 +129,10 @@ export class HomeServerApi { return this._get("/sync", {since, timeout, filter}, undefined, options); } + resolveRoomAlias(roomAlias: string): IHomeServerRequest { + return this._unauthedRequest( "GET", this._url( `/directory/room/${encodeURIComponent(roomAlias)}`, CS_V3_PREFIX ) ); + } + context(roomId: string, eventId: string, limit: number, filter: string): IHomeServerRequest { return this._get(`/rooms/${encodeURIComponent(roomId)}/context/${encodeURIComponent(eventId)}`, {filter, limit}); } diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 9f84e872ad..94c0ff0e50 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -31,6 +31,7 @@ import {RightPanelView} from "./rightpanel/RightPanelView.js"; import {viewClassForTile} from "./room/common"; import {JoinRoomView} from "./JoinRoomView"; import {ToastCollectionView} from "./toast/ToastCollectionView"; +import {PeekableRoomView} from "./room/PeekableRoomView"; export class SessionView extends TemplateView { render(t, vm) { @@ -60,6 +61,8 @@ export class SessionView extends TemplateView { return new RoomView(vm.currentRoomViewModel, viewClassForTile); } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { return new RoomBeingCreatedView(vm.currentRoomViewModel); + } else if (vm.currentRoomViewModel.kind === "peekable") { + return new PeekableRoomView(vm.currentRoomViewModel, viewClassForTile); } else { return new UnknownRoomView(vm.currentRoomViewModel); } diff --git a/src/platform/web/ui/session/room/PeekableRoomView.js b/src/platform/web/ui/session/room/PeekableRoomView.js new file mode 100644 index 0000000000..6e3fdbf437 --- /dev/null +++ b/src/platform/web/ui/session/room/PeekableRoomView.js @@ -0,0 +1,19 @@ +import {TemplateView} from "../../general/TemplateView"; +import {spinner} from "../../common"; + +export class PeekableRoomView extends TemplateView { + constructor(vm, viewClassForTile) { + super(vm); + } + + render(t, vm) { + return t.main({className: "UnknownRoomView middle"}, t.div([ + t.h2([ + vm.i18n`Peeking in room: ${vm.roomIdOrAlias}.`, + t.br(), + spinner(t), + vm.i18n`Loading messages..` + ]) + ])); + } +} From 8b56a0deaff0fa88fc55294b145e4230357716a0 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Thu, 9 Feb 2023 16:35:39 +0400 Subject: [PATCH 2/9] setup timelineViewModel on unknownRoomViewModel to render the timeline by fetching messages and storing in the indexed db tables --- src/domain/session/SessionViewModel.js | 6 +- .../session/room/UnknownRoomViewModel.js | 39 +++++++ src/matrix/Session.js | 109 ++++++++++++++++++ src/matrix/net/HomeServerApi.ts | 4 + .../web/ui/css/themes/element/theme.css | 13 ++- .../web/ui/session/room/PeekableRoomView.js | 38 ++++-- .../session/room/timeline/BaseMessageView.js | 2 +- 7 files changed, 200 insertions(+), 11 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index c2f276769d..870e2f7cb3 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -241,11 +241,15 @@ export class SessionViewModel extends ViewModel { } const peekable = await this._client.session.canPeekInRoom(roomId); - return new UnknownRoomViewModel(this.childOptions({ + let vm = new UnknownRoomViewModel(this.childOptions({ roomIdOrAlias, session: this._client.session, peekable: peekable })); + if ( peekable ) { + await vm.peek(); + } + return vm; } async _createArchivedRoomViewModel(roomId) { diff --git a/src/domain/session/room/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index 2e8cb50853..7bc98177f6 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -15,6 +15,9 @@ limitations under the License. */ import {ViewModel} from "../../ViewModel"; +import {TimelineViewModel} from "./timeline/TimelineViewModel"; +import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index"; +import {getAvatarHttpUrl} from "../../avatar"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { @@ -27,6 +30,10 @@ export class UnknownRoomViewModel extends ViewModel { this._busy = false; } + get room() { + return this._room; + } + get error() { return this._error?.message; } @@ -56,4 +63,36 @@ export class UnknownRoomViewModel extends ViewModel { get kind() { return this._peekable ? "peekable" : "unknown"; } + + get timelineViewModel() { + return this._timelineVM; + } + + avatarUrl(size) { + return getAvatarHttpUrl(this._room.avatarUrl, size, this.platform, this._room.mediaRepository); + } + + async peek() { + if ( !this._peekable ) { + return; + } + try { + this._room = await this._session.loadPeekableRoom(this.roomIdOrAlias); + const timeline = await this._room.openTimeline(); + this._tileOptions = this.childOptions({ + roomVM: this, + timeline, + tileClassForEntry: defaultTileClassForEntry, + }); + this._timelineVM = this.track(new TimelineViewModel(this.childOptions({ + tileOptions: this._tileOptions, + timeline, + }))); + this.emitChange("timelineViewModel"); + } catch (err) { + console.error(`room.openTimeline(): ${err.message}:\n${err.stack}`); + this._timelineError = err; + this.emitChange("error"); + } + } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 195c974e51..3a3fc57a63 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -49,6 +49,8 @@ import {SecretStorage} from "./ssss/SecretStorage"; import {ObservableValue, RetainedObservableValue} from "../observable/value"; import {CallHandler} from "./calls/CallHandler"; import {RoomStateHandlerSet} from "./room/state/RoomStateHandlerSet"; +import {EventKey} from "./room/timeline/EventKey"; +import {createEventEntry} from "./room/timeline/persistence/common"; const PICKLE_KEY = "DEFAULT_KEY"; const PUSHER_KEY = "pusher"; @@ -655,6 +657,21 @@ export class Session { return room; } + /** @internal */ + _createPeekableRoom(roomId) { + return new Room({ + roomId, + getSyncToken: this._getSyncToken, + storage: this._storage, + emitCollectionChange: this._roomUpdateCallback, + hsApi: this._hsApi, + mediaRepository: this._mediaRepository, + pendingEvents: [], + user: this._user, + platform: this._platform + }); + } + get invites() { return this._invites; } @@ -1031,6 +1048,98 @@ export class Session { }); } + loadPeekableRoom(roomId, log = null) { + return this._platform.logger.wrapOrRun(log, "loadPeekableRoom", async log => { + log.set("id", roomId); + + const room = this._createPeekableRoom(roomId); + let response = await this._loadEventsPeekableRoom(roomId, 100, 'b', null, log); + // Note: response.end to be used in the next call for sync functionality + + let summary = await this._preparePeekableRoomSummary(roomId, log); + const txn = await this._storage.readTxn([ + this._storage.storeNames.timelineFragments, + this._storage.storeNames.timelineEvents, + this._storage.storeNames.roomMembers, + ]); + await room.load(summary, txn, log); + + return room; + }); + } + + async _preparePeekableRoomSummary(roomId, log = null) { + return this._platform.logger.wrapOrRun(log, "preparePeekableRoomSummary", async log => { + log.set("id", roomId); + + let summary = {}; + const resp = await this._hsApi.currentState(roomId).response(); + for ( let i=0; i { + log.set("id", roomId); + let options = { + limit: limit, + dir: 'b', + filter: { + lazy_load_members: true, + include_redundant_members: true, + } + } + if (end !== null) { + options['from'] = end; + } + + const response = await this._hsApi.messages(roomId, options, {log}).response(); + log.set("/messages endpoint response", response); + + const txn = await this._storage.readWriteTxn([ + this._storage.storeNames.timelineFragments, + this._storage.storeNames.timelineEvents, + ]); + + // clear old records for this room + txn.timelineFragments.removeAllForRoom(roomId); + txn.timelineEvents.removeAllForRoom(roomId); + + // insert fragment and event records for this room + const fragment = { + roomId: roomId, + id: 0, + previousId: null, + nextId: null, + previousToken: response.start, + nextToken: null, + }; + txn.timelineFragments.add(fragment); + + let eventKey = EventKey.defaultLiveKey; + for (let i = 0; i < response.chunk.length; i++) { + if (i) { + eventKey = eventKey.previousKey(); + } + let txn = await this._storage.readWriteTxn([this._storage.storeNames.timelineEvents]); + let eventEntry = createEventEntry(eventKey, roomId, response.chunk[i]); + await txn.timelineEvents.tryInsert(eventEntry, log); + } + + return response; + }); + } + joinRoom(roomIdOrAlias, log = null) { return this._platform.logger.wrapOrRun(log, "joinRoom", async log => { const body = await this._hsApi.joinIdOrAlias(roomIdOrAlias, {log}).response(); diff --git a/src/matrix/net/HomeServerApi.ts b/src/matrix/net/HomeServerApi.ts index 6a8a13b10d..8f168e6693 100644 --- a/src/matrix/net/HomeServerApi.ts +++ b/src/matrix/net/HomeServerApi.ts @@ -168,6 +168,10 @@ export class HomeServerApi { return this._put(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, content, options); } + currentState(roomId: string): IHomeServerRequest { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state`, {}, undefined); + } + getLoginFlows(): IHomeServerRequest { return this._unauthedRequest("GET", this._url("/login")); } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 4c617386fa..dbdec97f02 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -975,7 +975,7 @@ button.link { width: 100%; } -.DisabledComposerView { +.DisabledComposerView, .PeekableRoomComposerView { padding: 12px; background-color: var(--background-color-secondary); } @@ -1002,6 +1002,17 @@ button.link { width: 100%; } +.PeekableRoomView .Timeline_message:hover > .Timeline_messageOptions{ + display: none; +} +.PeekableRoomComposerView h3 { + display: inline-block; + margin: 0; +} +.PeekableRoomComposerView .joinRoomButton { + float: right; +} + .LoadingView { height: 100%; width: 100%; diff --git a/src/platform/web/ui/session/room/PeekableRoomView.js b/src/platform/web/ui/session/room/PeekableRoomView.js index 6e3fdbf437..e1c5c5ec60 100644 --- a/src/platform/web/ui/session/room/PeekableRoomView.js +++ b/src/platform/web/ui/session/room/PeekableRoomView.js @@ -1,19 +1,41 @@ import {TemplateView} from "../../general/TemplateView"; -import {spinner} from "../../common"; +import {TimelineView} from "./TimelineView"; +import {TimelineLoadingView} from "./TimelineLoadingView"; +import {AvatarView} from "../../AvatarView"; export class PeekableRoomView extends TemplateView { + constructor(vm, viewClassForTile) { super(vm); + this._viewClassForTile = viewClassForTile; } render(t, vm) { - return t.main({className: "UnknownRoomView middle"}, t.div([ - t.h2([ - vm.i18n`Peeking in room: ${vm.roomIdOrAlias}.`, - t.br(), - spinner(t), - vm.i18n`Loading messages..` + return t.main({className: "RoomView PeekableRoomView middle"}, [ + t.div({className: "RoomHeader middle-header"}, [ + t.view(new AvatarView(vm, 32)), + t.div({className: "room-description"}, [ + t.h2(vm => vm.room.name), + ]), + ]), + t.div({className: "RoomView_body"}, [ + t.div({className: "RoomView_error"}, [ + t.if(vm => vm.error, t => t.div( + [ + t.p({}, vm => vm.error), + t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) }) + ]) + )]), + t.mapView(vm => vm.timelineViewModel, timelineViewModel => { + return timelineViewModel ? + new TimelineView(timelineViewModel, this._viewClassForTile) : + new TimelineLoadingView(vm); // vm is just needed for i18n + }), + t.div({className: "PeekableRoomComposerView"}, [ + t.h3(vm => vm.i18n`Join the room to participate`), + t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) + ]) ]) - ])); + ]); } } diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index d998e8269b..9e3d9dbce7 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -62,7 +62,7 @@ export class BaseMessageView extends TemplateView { li.removeChild(li.querySelector(".Timeline_messageAvatar")); li.removeChild(li.querySelector(".Timeline_messageSender")); } else if (!isContinuation && !this._isReplyPreview) { - const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); + const avatar = vm.options.peekable ? tag.div({className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]) : tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); const sender = tag.div( { className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`, From 9fa2f71a218e0b6f4bd6b7fbf7bf6b2b33917377 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Sat, 11 Feb 2023 12:12:55 +0400 Subject: [PATCH 3/9] move room alias resolution to canPeekInRoom() instead --- src/domain/session/SessionViewModel.js | 10 +--------- src/matrix/Session.js | 9 ++++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 870e2f7cb3..3966b2b262 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -232,15 +232,7 @@ export class SessionViewModel extends ViewModel { } async _createUnknownRoomViewModel(roomIdOrAlias) { - let roomId; - if ( roomIdOrAlias[0] !== "!" ) { - let response = await this._client._requestScheduler.hsApi.resolveRoomAlias(roomIdOrAlias).response(); - roomId = response.room_id; - } else { - roomId = roomIdOrAlias; - } - const peekable = await this._client.session.canPeekInRoom(roomId); - + const peekable = await this._client.session.canPeekInRoom(roomIdOrAlias); let vm = new UnknownRoomViewModel(this.childOptions({ roomIdOrAlias, session: this._client.session, diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 3a3fc57a63..45e7e27aac 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1147,8 +1147,15 @@ export class Session { }); } - canPeekInRoom(roomId, log = null) { + canPeekInRoom(roomIdOrAlias, log = null) { return this._platform.logger.wrapOrRun(log, "canPeekInRoom", async log => { + let roomId; + if (roomIdOrAlias[0] !== "!") { + let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response(); + roomId = response.room_id; + } else { + roomId = roomIdOrAlias; + } const body = await this._hsApi.state(roomId, 'm.room.history_visibility', '', {log}).response(); return body.history_visibility === 'world_readable'; }); From c76e45da8a30e794bf13a3655e758ea33752ea12 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Sat, 11 Feb 2023 13:26:18 +0400 Subject: [PATCH 4/9] use css to hide the right panel triggered when clicked on user avatar in a message --- src/platform/web/ui/css/themes/element/theme.css | 3 +++ src/platform/web/ui/session/room/timeline/BaseMessageView.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index dbdec97f02..4867e77879 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1005,6 +1005,9 @@ button.link { .PeekableRoomView .Timeline_message:hover > .Timeline_messageOptions{ display: none; } +.PeekableRoomView .Timeline_messageAvatar { + pointer-events: none; /* Prevent user panel from opening when clicking on avatars in the timeline. */ +} .PeekableRoomComposerView h3 { display: inline-block; margin: 0; diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 9e3d9dbce7..d998e8269b 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -62,7 +62,7 @@ export class BaseMessageView extends TemplateView { li.removeChild(li.querySelector(".Timeline_messageAvatar")); li.removeChild(li.querySelector(".Timeline_messageSender")); } else if (!isContinuation && !this._isReplyPreview) { - const avatar = vm.options.peekable ? tag.div({className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]) : tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); + const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); const sender = tag.div( { className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`, From 5bc5ca0f36fbbccb8d111d107198afbc8b3bf887 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Sat, 11 Feb 2023 14:31:16 +0400 Subject: [PATCH 5/9] remove PeekableRoomView and bring the code for it within UnknownRoomView only --- src/platform/web/ui/session/SessionView.js | 5 +- .../web/ui/session/room/PeekableRoomView.js | 41 ------------ .../web/ui/session/room/UnknownRoomView.js | 64 +++++++++++++++---- 3 files changed, 52 insertions(+), 58 deletions(-) delete mode 100644 src/platform/web/ui/session/room/PeekableRoomView.js diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 94c0ff0e50..b5938b56a7 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -31,7 +31,6 @@ import {RightPanelView} from "./rightpanel/RightPanelView.js"; import {viewClassForTile} from "./room/common"; import {JoinRoomView} from "./JoinRoomView"; import {ToastCollectionView} from "./toast/ToastCollectionView"; -import {PeekableRoomView} from "./room/PeekableRoomView"; export class SessionView extends TemplateView { render(t, vm) { @@ -61,10 +60,8 @@ export class SessionView extends TemplateView { return new RoomView(vm.currentRoomViewModel, viewClassForTile); } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { return new RoomBeingCreatedView(vm.currentRoomViewModel); - } else if (vm.currentRoomViewModel.kind === "peekable") { - return new PeekableRoomView(vm.currentRoomViewModel, viewClassForTile); } else { - return new UnknownRoomView(vm.currentRoomViewModel); + return new UnknownRoomView(vm.currentRoomViewModel, viewClassForTile); } } else { return new StaticView(t => t.div({className: "room-placeholder"}, t.h2(vm.i18n`Choose a room on the left side.`))); diff --git a/src/platform/web/ui/session/room/PeekableRoomView.js b/src/platform/web/ui/session/room/PeekableRoomView.js deleted file mode 100644 index e1c5c5ec60..0000000000 --- a/src/platform/web/ui/session/room/PeekableRoomView.js +++ /dev/null @@ -1,41 +0,0 @@ -import {TemplateView} from "../../general/TemplateView"; -import {TimelineView} from "./TimelineView"; -import {TimelineLoadingView} from "./TimelineLoadingView"; -import {AvatarView} from "../../AvatarView"; - -export class PeekableRoomView extends TemplateView { - - constructor(vm, viewClassForTile) { - super(vm); - this._viewClassForTile = viewClassForTile; - } - - render(t, vm) { - return t.main({className: "RoomView PeekableRoomView middle"}, [ - t.div({className: "RoomHeader middle-header"}, [ - t.view(new AvatarView(vm, 32)), - t.div({className: "room-description"}, [ - t.h2(vm => vm.room.name), - ]), - ]), - t.div({className: "RoomView_body"}, [ - t.div({className: "RoomView_error"}, [ - t.if(vm => vm.error, t => t.div( - [ - t.p({}, vm => vm.error), - t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) }) - ]) - )]), - t.mapView(vm => vm.timelineViewModel, timelineViewModel => { - return timelineViewModel ? - new TimelineView(timelineViewModel, this._viewClassForTile) : - new TimelineLoadingView(vm); // vm is just needed for i18n - }), - t.div({className: "PeekableRoomComposerView"}, [ - t.h3(vm => vm.i18n`Join the room to participate`), - t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) - ]) - ]) - ]); - } -} diff --git a/src/platform/web/ui/session/room/UnknownRoomView.js b/src/platform/web/ui/session/room/UnknownRoomView.js index 80d857d801..54fccf2194 100644 --- a/src/platform/web/ui/session/room/UnknownRoomView.js +++ b/src/platform/web/ui/session/room/UnknownRoomView.js @@ -15,21 +15,59 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView"; +import {AvatarView} from "../../AvatarView"; +import {TimelineView} from "./TimelineView"; +import {TimelineLoadingView} from "./TimelineLoadingView"; export class UnknownRoomView extends TemplateView { + + constructor(vm, viewClassForTile) { + super(vm); + this._viewClassForTile = viewClassForTile; + } + render(t, vm) { - return t.main({className: "UnknownRoomView middle"}, t.div([ - t.h2([ - vm.i18n`You are currently not in ${vm.roomIdOrAlias}.`, - t.br(), - vm.i18n`Want to join it?` - ]), - t.button({ - className: "button-action primary", - onClick: () => vm.join(), - disabled: vm => vm.busy, - }, vm.i18n`Join room`), - t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) - ])); + if (vm.kind === 'peekable') { + return t.main({className: "RoomView PeekableRoomView middle"}, [ + t.div({className: "RoomHeader middle-header"}, [ + t.view(new AvatarView(vm, 32)), + t.div({className: "room-description"}, [ + t.h2(vm => vm.room.name), + ]), + ]), + t.div({className: "RoomView_body"}, [ + t.div({className: "RoomView_error"}, [ + t.if(vm => vm.error, t => t.div( + [ + t.p({}, vm => vm.error), + t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) }) + ]) + )]), + t.mapView(vm => vm.timelineViewModel, timelineViewModel => { + return timelineViewModel ? + new TimelineView(timelineViewModel, this._viewClassForTile) : + new TimelineLoadingView(vm); // vm is just needed for i18n + }), + t.div({className: "PeekableRoomComposerView"}, [ + t.h3(vm => vm.i18n`Join the room to participate`), + t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) + ]) + ]) + ]); + } else { + return t.main({className: "UnknownRoomView middle"}, t.div([ + t.h2([ + vm.i18n`You are currently not in ${vm.roomIdOrAlias}.`, + t.br(), + vm.i18n`Want to join it?` + ]), + t.button({ + className: "button-action primary", + onClick: () => vm.join(), + disabled: vm => vm.busy, + }, vm.i18n`Join room`), + t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) + ])); + } } } From 4439fb381373142feba0af7a98511ed863a4a28d Mon Sep 17 00:00:00 2001 From: Ashfame Date: Sat, 11 Feb 2023 15:06:24 +0400 Subject: [PATCH 6/9] change code to refer peekable room as WorldReadable room to be more aligned with Matrix spec --- src/domain/session/SessionViewModel.js | 8 +++---- .../session/room/UnknownRoomViewModel.js | 12 +++++----- src/matrix/Session.js | 24 +++++++++---------- .../web/ui/css/themes/element/theme.css | 10 ++++---- .../web/ui/session/room/UnknownRoomView.js | 6 ++--- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 3966b2b262..e53dcf802b 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -232,14 +232,14 @@ export class SessionViewModel extends ViewModel { } async _createUnknownRoomViewModel(roomIdOrAlias) { - const peekable = await this._client.session.canPeekInRoom(roomIdOrAlias); + const worldReadable = await this._client.session.isWorldReadableRoom(roomIdOrAlias); let vm = new UnknownRoomViewModel(this.childOptions({ roomIdOrAlias, session: this._client.session, - peekable: peekable + worldReadable: worldReadable })); - if ( peekable ) { - await vm.peek(); + if (worldReadable) { + await vm.load(); } return vm; } diff --git a/src/domain/session/room/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index 7bc98177f6..ba0b5c6eb9 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -22,10 +22,10 @@ import {getAvatarHttpUrl} from "../../avatar"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { super(options); - const {roomIdOrAlias, session, peekable} = options; + const {roomIdOrAlias, session, worldReadable} = options; this._session = session; this.roomIdOrAlias = roomIdOrAlias; - this._peekable = peekable; + this._worldReadable = worldReadable; this._error = null; this._busy = false; } @@ -61,7 +61,7 @@ export class UnknownRoomViewModel extends ViewModel { } get kind() { - return this._peekable ? "peekable" : "unknown"; + return this._worldReadable ? "worldReadableRoom" : "unknown"; } get timelineViewModel() { @@ -72,12 +72,12 @@ export class UnknownRoomViewModel extends ViewModel { return getAvatarHttpUrl(this._room.avatarUrl, size, this.platform, this._room.mediaRepository); } - async peek() { - if ( !this._peekable ) { + async load() { + if (!this._worldReadable) { return; } try { - this._room = await this._session.loadPeekableRoom(this.roomIdOrAlias); + this._room = await this._session.loadWorldReadableRoom(this.roomIdOrAlias); const timeline = await this._room.openTimeline(); this._tileOptions = this.childOptions({ roomVM: this, diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 45e7e27aac..96177ba582 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -658,7 +658,7 @@ export class Session { } /** @internal */ - _createPeekableRoom(roomId) { + _createWorldReadableRoom(roomId) { return new Room({ roomId, getSyncToken: this._getSyncToken, @@ -1048,15 +1048,15 @@ export class Session { }); } - loadPeekableRoom(roomId, log = null) { - return this._platform.logger.wrapOrRun(log, "loadPeekableRoom", async log => { + loadWorldReadableRoom(roomId, log = null) { + return this._platform.logger.wrapOrRun(log, "loadWorldReadableRoom", async log => { log.set("id", roomId); - const room = this._createPeekableRoom(roomId); - let response = await this._loadEventsPeekableRoom(roomId, 100, 'b', null, log); + const room = this._createWorldReadableRoom(roomId); + let response = await this._fetchWorldReadableRoomEvents(roomId, 100, 'b', null, log); // Note: response.end to be used in the next call for sync functionality - let summary = await this._preparePeekableRoomSummary(roomId, log); + let summary = await this._prepareWorldReadableRoomSummary(roomId, log); const txn = await this._storage.readTxn([ this._storage.storeNames.timelineFragments, this._storage.storeNames.timelineEvents, @@ -1068,8 +1068,8 @@ export class Session { }); } - async _preparePeekableRoomSummary(roomId, log = null) { - return this._platform.logger.wrapOrRun(log, "preparePeekableRoomSummary", async log => { + async _prepareWorldReadableRoomSummary(roomId, log = null) { + return this._platform.logger.wrapOrRun(log, "prepareWorldReadableRoomSummary", async log => { log.set("id", roomId); let summary = {}; @@ -1088,8 +1088,8 @@ export class Session { }); } - async _loadEventsPeekableRoom(roomId, limit = 30, dir = 'b', end = null, log = null) { - return this._platform.logger.wrapOrRun(log, "loadEventsPeekableRoom", async log => { + async _fetchWorldReadableRoomEvents(roomId, limit = 30, dir = 'b', end = null, log = null) { + return this._platform.logger.wrapOrRun(log, "fetchWorldReadableRoomEvents", async log => { log.set("id", roomId); let options = { limit: limit, @@ -1147,8 +1147,8 @@ export class Session { }); } - canPeekInRoom(roomIdOrAlias, log = null) { - return this._platform.logger.wrapOrRun(log, "canPeekInRoom", async log => { + isWorldReadableRoom(roomIdOrAlias, log = null) { + return this._platform.logger.wrapOrRun(log, "isWorldReadableRoom", async log => { let roomId; if (roomIdOrAlias[0] !== "!") { let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response(); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 4867e77879..d328d98024 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -975,7 +975,7 @@ button.link { width: 100%; } -.DisabledComposerView, .PeekableRoomComposerView { +.DisabledComposerView, .WorldReadableRoomComposerView { padding: 12px; background-color: var(--background-color-secondary); } @@ -1002,17 +1002,17 @@ button.link { width: 100%; } -.PeekableRoomView .Timeline_message:hover > .Timeline_messageOptions{ +.WorldReadableRoomView .Timeline_message:hover > .Timeline_messageOptions{ display: none; } -.PeekableRoomView .Timeline_messageAvatar { +.WorldReadableRoomView .Timeline_messageAvatar { pointer-events: none; /* Prevent user panel from opening when clicking on avatars in the timeline. */ } -.PeekableRoomComposerView h3 { +.WorldReadableRoomComposerView h3 { display: inline-block; margin: 0; } -.PeekableRoomComposerView .joinRoomButton { +.WorldReadableRoomComposerView .joinRoomButton { float: right; } diff --git a/src/platform/web/ui/session/room/UnknownRoomView.js b/src/platform/web/ui/session/room/UnknownRoomView.js index 54fccf2194..945ecf0487 100644 --- a/src/platform/web/ui/session/room/UnknownRoomView.js +++ b/src/platform/web/ui/session/room/UnknownRoomView.js @@ -27,8 +27,8 @@ export class UnknownRoomView extends TemplateView { } render(t, vm) { - if (vm.kind === 'peekable') { - return t.main({className: "RoomView PeekableRoomView middle"}, [ + if (vm.kind === 'worldReadableRoom') { + return t.main({className: "RoomView WorldReadableRoomView middle"}, [ t.div({className: "RoomHeader middle-header"}, [ t.view(new AvatarView(vm, 32)), t.div({className: "room-description"}, [ @@ -48,7 +48,7 @@ export class UnknownRoomView extends TemplateView { new TimelineView(timelineViewModel, this._viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), - t.div({className: "PeekableRoomComposerView"}, [ + t.div({className: "WorldReadableRoomComposerView"}, [ t.h3(vm => vm.i18n`Join the room to participate`), t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) ]) From 220fe383dc6289dd3e1b1f6db70ec9718c0ae067 Mon Sep 17 00:00:00 2001 From: Ashish Kumar Date: Mon, 13 Feb 2023 18:54:57 +0400 Subject: [PATCH 7/9] use string's startsWith() instead Co-authored-by: Paulo Pinto --- src/matrix/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 96177ba582..f819e9dc57 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1150,7 +1150,7 @@ export class Session { isWorldReadableRoom(roomIdOrAlias, log = null) { return this._platform.logger.wrapOrRun(log, "isWorldReadableRoom", async log => { let roomId; - if (roomIdOrAlias[0] !== "!") { + if (!roomIdOrAlias.startsWith("!")) { let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response(); roomId = response.room_id; } else { From 2fb9cb658a27e64a510f64f7974a22eb221ef165 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Wed, 15 Feb 2023 19:04:52 +0400 Subject: [PATCH 8/9] ensure a boolean is returned despite of failure/error in HomeserverApi HTTP request --- src/matrix/Session.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/matrix/Session.js b/src/matrix/Session.js index f819e9dc57..a0397567c0 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -1149,15 +1149,19 @@ export class Session { isWorldReadableRoom(roomIdOrAlias, log = null) { return this._platform.logger.wrapOrRun(log, "isWorldReadableRoom", async log => { - let roomId; - if (!roomIdOrAlias.startsWith("!")) { - let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response(); - roomId = response.room_id; - } else { - roomId = roomIdOrAlias; + try { + let roomId; + if (!roomIdOrAlias.startsWith("!")) { + let response = await this._hsApi.resolveRoomAlias(roomIdOrAlias).response(); + roomId = response.room_id; + } else { + roomId = roomIdOrAlias; + } + const body = await this._hsApi.state(roomId, 'm.room.history_visibility', '', {log}).response(); + return body.history_visibility === 'world_readable'; + } catch { + return false; } - const body = await this._hsApi.state(roomId, 'm.room.history_visibility', '', {log}).response(); - return body.history_visibility === 'world_readable'; }); } } From 654cdf03178ab514a40dca23613b6d6e5957a8d1 Mon Sep 17 00:00:00 2001 From: Ashfame Date: Wed, 15 Feb 2023 19:20:56 +0400 Subject: [PATCH 9/9] improve UX when joining big rooms to render unknown room view early and loading things async and updating view as we do This shows a spinner on UnknownRoomView stating "checking preview capability" and goes away when done and if room is world_readable, then renders the timeline with the last 100 messages --- src/domain/session/SessionViewModel.js | 10 +- .../session/room/UnknownRoomViewModel.js | 15 ++- .../web/ui/css/themes/element/theme.css | 12 +++ src/platform/web/ui/session/SessionView.js | 2 +- .../web/ui/session/room/UnknownRoomView.js | 102 ++++++++++-------- 5 files changed, 89 insertions(+), 52 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index e53dcf802b..5b762c17cb 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -232,16 +232,12 @@ export class SessionViewModel extends ViewModel { } async _createUnknownRoomViewModel(roomIdOrAlias) { - const worldReadable = await this._client.session.isWorldReadableRoom(roomIdOrAlias); - let vm = new UnknownRoomViewModel(this.childOptions({ + const roomVM = new UnknownRoomViewModel(this.childOptions({ roomIdOrAlias, session: this._client.session, - worldReadable: worldReadable })); - if (worldReadable) { - await vm.load(); - } - return vm; + void roomVM.load(); + return roomVM; } async _createArchivedRoomViewModel(roomId) { diff --git a/src/domain/session/room/UnknownRoomViewModel.js b/src/domain/session/room/UnknownRoomViewModel.js index ba0b5c6eb9..7019277431 100644 --- a/src/domain/session/room/UnknownRoomViewModel.js +++ b/src/domain/session/room/UnknownRoomViewModel.js @@ -22,12 +22,13 @@ import {getAvatarHttpUrl} from "../../avatar"; export class UnknownRoomViewModel extends ViewModel { constructor(options) { super(options); - const {roomIdOrAlias, session, worldReadable} = options; + const {roomIdOrAlias, session} = options; this._session = session; this.roomIdOrAlias = roomIdOrAlias; - this._worldReadable = worldReadable; this._error = null; this._busy = false; + this._worldReadable = false; // won't know until load() finishes with isWorldReadableRoom() call + this._checkingPreviewCapability = false; // won't know until load() finishes with isWorldReadableRoom() call } get room() { @@ -60,6 +61,10 @@ export class UnknownRoomViewModel extends ViewModel { return this._busy; } + get checkingPreviewCapability() { + return this._checkingPreviewCapability; + } + get kind() { return this._worldReadable ? "worldReadableRoom" : "unknown"; } @@ -73,9 +78,15 @@ export class UnknownRoomViewModel extends ViewModel { } async load() { + this._checkingPreviewCapability = true; + this._worldReadable = await this._session.isWorldReadableRoom(this.roomIdOrAlias); + this._checkingPreviewCapability = false; + if (!this._worldReadable) { + this.emitChange("checkingPreviewCapability"); return; } + try { this._room = await this._session.loadWorldReadableRoom(this.roomIdOrAlias); const timeline = await this._room.openTimeline(); diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index d328d98024..0e7dff12db 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -1002,6 +1002,18 @@ button.link { width: 100%; } +.UnknownRoomView .checkingPreviewCapability { + display: flex; + flex-direction: row; /* make main axis vertical */ + justify-content: center; /* center items vertically, in this case */ + align-items: center; /* center items horizontally, in this case */ + margin-top: 5px; +} + +.UnknownRoomView .checkingPreviewCapability p { + margin-left: 5px; +} + .WorldReadableRoomView .Timeline_message:hover > .Timeline_messageOptions{ display: none; } diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index b5938b56a7..9f84e872ad 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -61,7 +61,7 @@ export class SessionView extends TemplateView { } else if (vm.currentRoomViewModel.kind === "roomBeingCreated") { return new RoomBeingCreatedView(vm.currentRoomViewModel); } else { - return new UnknownRoomView(vm.currentRoomViewModel, viewClassForTile); + return new UnknownRoomView(vm.currentRoomViewModel); } } else { return new StaticView(t => t.div({className: "room-placeholder"}, t.h2(vm.i18n`Choose a room on the left side.`))); diff --git a/src/platform/web/ui/session/room/UnknownRoomView.js b/src/platform/web/ui/session/room/UnknownRoomView.js index 945ecf0487..cd25300d6b 100644 --- a/src/platform/web/ui/session/room/UnknownRoomView.js +++ b/src/platform/web/ui/session/room/UnknownRoomView.js @@ -14,60 +14,78 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../../general/TemplateView"; +import {InlineTemplateView, TemplateView} from "../../general/TemplateView"; import {AvatarView} from "../../AvatarView"; import {TimelineView} from "./TimelineView"; import {TimelineLoadingView} from "./TimelineLoadingView"; +import {spinner} from "../../common.js"; +import {viewClassForTile} from "./common"; export class UnknownRoomView extends TemplateView { - constructor(vm, viewClassForTile) { + constructor(vm) { super(vm); - this._viewClassForTile = viewClassForTile; } render(t, vm) { - if (vm.kind === 'worldReadableRoom') { - return t.main({className: "RoomView WorldReadableRoomView middle"}, [ - t.div({className: "RoomHeader middle-header"}, [ - t.view(new AvatarView(vm, 32)), - t.div({className: "room-description"}, [ - t.h2(vm => vm.room.name), + return t.mapView(vm => vm.kind, kind => { + const unknownRoomView = new InlineTemplateView(vm, (t, m) => { + return t.main({className: "UnknownRoomView middle"}, t.div([ + t.h2([ + vm.i18n`You are currently not in ${vm.roomIdOrAlias}.`, + t.br(), + vm.i18n`Want to join it?` ]), - ]), - t.div({className: "RoomView_body"}, [ - t.div({className: "RoomView_error"}, [ - t.if(vm => vm.error, t => t.div( - [ - t.p({}, vm => vm.error), - t.button({ className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt) }) - ]) - )]), - t.mapView(vm => vm.timelineViewModel, timelineViewModel => { - return timelineViewModel ? - new TimelineView(timelineViewModel, this._viewClassForTile) : - new TimelineLoadingView(vm); // vm is just needed for i18n - }), - t.div({className: "WorldReadableRoomComposerView"}, [ - t.h3(vm => vm.i18n`Join the room to participate`), - t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) - ]) - ]) - ]); - } else { - return t.main({className: "UnknownRoomView middle"}, t.div([ - t.h2([ - vm.i18n`You are currently not in ${vm.roomIdOrAlias}.`, + t.button({ + className: "button-action primary", + onClick: () => vm.join(), + disabled: vm => vm.busy, + }, vm.i18n`Join room`), t.br(), - vm.i18n`Want to join it?` + t.if(vm => vm.checkingPreviewCapability, t => t.div({className: "checkingPreviewCapability"}, [ + spinner(t), + t.p(vm.i18n`Checking preview capability...`) + ])), + t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) + ])); + }); + return kind === 'worldReadableRoom' ? new WorldReadableRoomView(vm) : unknownRoomView; + }); + } +} + +class WorldReadableRoomView extends InlineTemplateView { + + constructor(value, render) { + super(value, render); + } + + render(t, vm) { + return t.main({className: "RoomView WorldReadableRoomView middle"}, [ + t.div({className: "RoomHeader middle-header"}, [ + t.view(new AvatarView(vm, 32)), + t.div({className: "room-description"}, [ + t.h2(vm => vm.room.name), ]), - t.button({ - className: "button-action primary", - onClick: () => vm.join(), - disabled: vm => vm.busy, - }, vm.i18n`Join room`), - t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) - ])); - } + ]), + t.div({className: "RoomView_body"}, [ + t.div({className: "RoomView_error"}, [ + t.if(vm => vm.error, t => t.div( + [ + t.p({}, vm => vm.error), + t.button({className: "RoomView_error_closerButton", onClick: evt => vm.dismissError(evt)}) + ]) + )]), + t.mapView(vm => vm.timelineViewModel, timelineViewModel => { + return timelineViewModel ? + new TimelineView(timelineViewModel, viewClassForTile) : + new TimelineLoadingView(vm); // vm is just needed for i18n + }), + t.div({className: "WorldReadableRoomComposerView"}, [ + t.h3(vm => vm.i18n`Join the room to participate`), + t.button({className: "joinRoomButton", onClick: () => vm.join()}, vm.i18n`Join Room`) + ]) + ]) + ]); } }