From 5f1346568de7ff814799ac43b1981e0ff3417b5f Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 28 Jun 2021 23:18:07 +0530 Subject: [PATCH 001/118] Handle avatar error Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 8845f887..715a4890 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -44,6 +44,11 @@ export class AvatarView extends BaseUpdateView { return false; } + setAvatarError() { + this._avatarError = true; + this.update(this.value); + } + _avatarTitleChanged() { if (this.value.avatarTitle !== this._avatarTitle) { this._avatarTitle = this.value.avatarTitle; @@ -65,6 +70,8 @@ export class AvatarView extends BaseUpdateView { this._avatarLetterChanged(); this._avatarTitleChanged(); this._root = renderStaticAvatar(this.value, this._size); + const image = this._root.firstChild; + image?.addEventListener("error", () => this.setAvatarError()); // takes care of update being called when needed super.mount(options); return this._root; @@ -76,10 +83,10 @@ export class AvatarView extends BaseUpdateView { update(vm) { // important to always call _...changed for every prop - if (this._avatarUrlChanged()) { + if (this._avatarUrlChanged() || this._avatarError) { // avatarColorNumber won't change, it's based on room/user id const bgColorClass = `usercolor${vm.avatarColorNumber}`; - if (vm.avatarUrl(this._size)) { + if (vm.avatarUrl(this._size) && !this._avatarError) { this._root.replaceChild(renderImg(vm, this._size), this._root.firstChild); this._root.classList.remove(bgColorClass); } else { @@ -87,7 +94,7 @@ export class AvatarView extends BaseUpdateView { this._root.classList.add(bgColorClass); } } - const hasAvatar = !!vm.avatarUrl(this._size); + const hasAvatar = !!(vm.avatarUrl(this._size) && !vm._avatarError); if (this._avatarTitleChanged() && hasAvatar) { const img = this._root.firstChild; img.setAttribute("title", vm.avatarTitle); @@ -95,6 +102,7 @@ export class AvatarView extends BaseUpdateView { if (this._avatarLetterChanged() && !hasAvatar) { this._root.firstChild.textContent = vm.avatarLetter; } + if (this._avatarError) { this._avatarError = false;} } } @@ -104,7 +112,7 @@ export class AvatarView extends BaseUpdateView { * @return {Element} */ export function renderStaticAvatar(vm, size, extraClasses = undefined) { - const hasAvatar = !!vm.avatarUrl(size); + const hasAvatar = !!(vm.avatarUrl(size) && !vm.avatarError); let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, From 8b6ff533e8ed8604619c7cdc8ef41952fb0c9f14 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 29 Jun 2021 15:38:58 +0530 Subject: [PATCH 002/118] Add and remove opposing event listeners Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 715a4890..0a865963 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -44,7 +44,7 @@ export class AvatarView extends BaseUpdateView { return false; } - setAvatarError() { + _setAvatarError() { this._avatarError = true; this.update(this.value); } @@ -57,6 +57,20 @@ export class AvatarView extends BaseUpdateView { return false; } + _addListenersToAvatar(image) { + const handleAvatarError = (e) => { + const image = e.target; + image.removeEventListener("load", removeErrorHandler); + this._setAvatarError(); + }; + const removeErrorHandler = (e) => { + const image = e.target; + image.removeEventListener("error", handleAvatarError); + }; + image?.addEventListener("error", handleAvatarError); + image?.addEventListener("load", removeErrorHandler); + } + _avatarLetterChanged() { if (this.value.avatarLetter !== this._avatarLetter) { this._avatarLetter = this.value.avatarLetter; @@ -71,7 +85,7 @@ export class AvatarView extends BaseUpdateView { this._avatarTitleChanged(); this._root = renderStaticAvatar(this.value, this._size); const image = this._root.firstChild; - image?.addEventListener("error", () => this.setAvatarError()); + this._addListenersToAvatar(image); // takes care of update being called when needed super.mount(options); return this._root; From b42f7e1a361727c9fc6682efcf9d1f5f9ce0614d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 29 Jun 2021 19:48:36 +0530 Subject: [PATCH 003/118] remove both handlers Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 0a865963..d01a026e 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -58,17 +58,14 @@ export class AvatarView extends BaseUpdateView { } _addListenersToAvatar(image) { - const handleAvatarError = (e) => { + const imageEventHandler = (e) => { + if(e.type === "error") { this._setAvatarError(); } const image = e.target; - image.removeEventListener("load", removeErrorHandler); - this._setAvatarError(); + image.removeEventListener("error", imageEventHandler); + image.removeEventListener("load", imageEventHandler); }; - const removeErrorHandler = (e) => { - const image = e.target; - image.removeEventListener("error", handleAvatarError); - }; - image?.addEventListener("error", handleAvatarError); - image?.addEventListener("load", removeErrorHandler); + image?.addEventListener("error", imageEventHandler); + image?.addEventListener("load", imageEventHandler); } _avatarLetterChanged() { From bcaf84e54587c77afab254124600209e93652b5d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 23:27:46 +0530 Subject: [PATCH 004/118] Revert commits This reverts commit 5f1346568de7ff814799ac43b1981e0ff3417b5f. Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index d01a026e..8845f887 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -44,11 +44,6 @@ export class AvatarView extends BaseUpdateView { return false; } - _setAvatarError() { - this._avatarError = true; - this.update(this.value); - } - _avatarTitleChanged() { if (this.value.avatarTitle !== this._avatarTitle) { this._avatarTitle = this.value.avatarTitle; @@ -57,17 +52,6 @@ export class AvatarView extends BaseUpdateView { return false; } - _addListenersToAvatar(image) { - const imageEventHandler = (e) => { - if(e.type === "error") { this._setAvatarError(); } - const image = e.target; - image.removeEventListener("error", imageEventHandler); - image.removeEventListener("load", imageEventHandler); - }; - image?.addEventListener("error", imageEventHandler); - image?.addEventListener("load", imageEventHandler); - } - _avatarLetterChanged() { if (this.value.avatarLetter !== this._avatarLetter) { this._avatarLetter = this.value.avatarLetter; @@ -81,8 +65,6 @@ export class AvatarView extends BaseUpdateView { this._avatarLetterChanged(); this._avatarTitleChanged(); this._root = renderStaticAvatar(this.value, this._size); - const image = this._root.firstChild; - this._addListenersToAvatar(image); // takes care of update being called when needed super.mount(options); return this._root; @@ -94,10 +76,10 @@ export class AvatarView extends BaseUpdateView { update(vm) { // important to always call _...changed for every prop - if (this._avatarUrlChanged() || this._avatarError) { + if (this._avatarUrlChanged()) { // avatarColorNumber won't change, it's based on room/user id const bgColorClass = `usercolor${vm.avatarColorNumber}`; - if (vm.avatarUrl(this._size) && !this._avatarError) { + if (vm.avatarUrl(this._size)) { this._root.replaceChild(renderImg(vm, this._size), this._root.firstChild); this._root.classList.remove(bgColorClass); } else { @@ -105,7 +87,7 @@ export class AvatarView extends BaseUpdateView { this._root.classList.add(bgColorClass); } } - const hasAvatar = !!(vm.avatarUrl(this._size) && !vm._avatarError); + const hasAvatar = !!vm.avatarUrl(this._size); if (this._avatarTitleChanged() && hasAvatar) { const img = this._root.firstChild; img.setAttribute("title", vm.avatarTitle); @@ -113,7 +95,6 @@ export class AvatarView extends BaseUpdateView { if (this._avatarLetterChanged() && !hasAvatar) { this._root.firstChild.textContent = vm.avatarLetter; } - if (this._avatarError) { this._avatarError = false;} } } @@ -123,7 +104,7 @@ export class AvatarView extends BaseUpdateView { * @return {Element} */ export function renderStaticAvatar(vm, size, extraClasses = undefined) { - const hasAvatar = !!(vm.avatarUrl(size) && !vm.avatarError); + const hasAvatar = !!vm.avatarUrl(size); let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, From b469c4299f0ac60f7c70c4245f416738b25183bd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 23:30:44 +0530 Subject: [PATCH 005/118] implement new approach Signed-off-by: RMidhunSuresh --- src/platform/web/Platform.js | 2 ++ src/platform/web/ui/avatar.js | 20 ++++++++++++++++++-- src/platform/web/ui/css/avatar.css | 4 ++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index fea17ba1..3d89be9a 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -36,6 +36,7 @@ import {BlobHandle} from "./dom/BlobHandle.js"; import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js"; import {downloadInIframe} from "./dom/download.js"; import {Disposables} from "../../utils/Disposables.js"; +import {handleAvatarError} from "./ui/avatar.js"; function addScript(src) { return new Promise(function (resolve, reject) { @@ -189,6 +190,7 @@ export class Platform { this._disposables.track(disposable); } } + this._container.addEventListener("error", handleAvatarError, true); window.__hydrogenViewModel = vm; const view = new RootView(vm); this._container.appendChild(view.mount()); diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 8845f887..093cfd69 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -108,16 +108,32 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, - [`usercolor${vm.avatarColorNumber}`]: !hasAvatar, + [`usercolor${vm.avatarColorNumber}`]: true, + ['has-image']: true }); if (extraClasses) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - return tag.div({className: avatarClasses}, [avatarContent]); + return tag.div({className: avatarClasses, 'data-avatar-letter': vm.avatarLetter}, [avatarContent]); } function renderImg(vm, size) { const sizeStr = size.toString(); return tag.img({src: vm.avatarUrl(size), width: sizeStr, height: sizeStr, title: vm.avatarTitle}); } + +function isAvatarEvent(e) { + const element = e.target; + const parent = element.parentElement; + return element.tagName === "IMG" && parent.classList.contains("avatar"); +} + +export function handleAvatarError(e) { + if (!isAvatarEvent(e)) { return; } + const parent = e.target.parentElement; + const avatarLetter = parent.getAttribute("data-avatar-letter"); + const letterNode = document.createTextNode(avatarLetter); + parent.appendChild(letterNode); + parent.classList.remove("has-image"); +} diff --git a/src/platform/web/ui/css/avatar.css b/src/platform/web/ui/css/avatar.css index d369f85f..85d370b7 100644 --- a/src/platform/web/ui/css/avatar.css +++ b/src/platform/web/ui/css/avatar.css @@ -31,6 +31,10 @@ limitations under the License. speak: none; } +.hydrogen .avatar:not(.has-image) img{ + display: none; +} + .hydrogen .avatar img { width: 100%; height: 100%; From 168b1d6247ab57e2f65b8f80aaaaf0bd0d511526 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 23:34:23 +0530 Subject: [PATCH 006/118] Move AvatarView to separate file Signed-off-by: RMidhunSuresh --- src/platform/web/ui/AvatarView.js | 84 ++++++++++++++++++ src/platform/web/ui/avatar.js | 85 +------------------ .../web/ui/session/leftpanel/RoomTileView.js | 2 +- .../ui/session/rightpanel/RoomDetailsView.js | 2 +- src/platform/web/ui/session/room/RoomView.js | 2 +- 5 files changed, 88 insertions(+), 87 deletions(-) create mode 100644 src/platform/web/ui/AvatarView.js diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js new file mode 100644 index 00000000..22381838 --- /dev/null +++ b/src/platform/web/ui/AvatarView.js @@ -0,0 +1,84 @@ +import {BaseUpdateView} from "./general/BaseUpdateView.js"; +import {renderStaticAvatar, renderImg} from "./avatar.js"; +import {text} from "./general/html.js"; + +/* +optimization to not use a sub view when changing between img and text +because there can be many many instances of this view +*/ + +export class AvatarView extends BaseUpdateView { + /** + * @param {ViewModel} value view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} + * @param {Number} size + */ + constructor(value, size) { + super(value); + this._root = null; + this._avatarUrl = null; + this._avatarTitle = null; + this._avatarLetter = null; + this._size = size; + } + + _avatarUrlChanged() { + if (this.value.avatarUrl(this._size) !== this._avatarUrl) { + this._avatarUrl = this.value.avatarUrl(this._size); + return true; + } + return false; + } + + _avatarTitleChanged() { + if (this.value.avatarTitle !== this._avatarTitle) { + this._avatarTitle = this.value.avatarTitle; + return true; + } + return false; + } + + _avatarLetterChanged() { + if (this.value.avatarLetter !== this._avatarLetter) { + this._avatarLetter = this.value.avatarLetter; + return true; + } + return false; + } + + mount(options) { + this._avatarUrlChanged(); + this._avatarLetterChanged(); + this._avatarTitleChanged(); + this._root = renderStaticAvatar(this.value, this._size); + // takes care of update being called when needed + super.mount(options); + return this._root; + } + + root() { + return this._root; + } + + update(vm) { + // important to always call _...changed for every prop + if (this._avatarUrlChanged()) { + // avatarColorNumber won't change, it's based on room/user id + const bgColorClass = `usercolor${vm.avatarColorNumber}`; + if (vm.avatarUrl(this._size)) { + this._root.replaceChild(renderImg(vm, this._size), this._root.firstChild); + this._root.classList.remove(bgColorClass); + } else { + this._root.replaceChild(text(vm.avatarLetter), this._root.firstChild); + this._root.classList.add(bgColorClass); + } + } + const hasAvatar = !!vm.avatarUrl(this._size); + if (this._avatarTitleChanged() && hasAvatar) { + const img = this._root.firstChild; + img.setAttribute("title", vm.avatarTitle); + } + if (this._avatarLetterChanged() && !hasAvatar) { + this._root.firstChild.textContent = vm.avatarLetter; + } + } +} diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 093cfd69..bda44db0 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -15,89 +15,6 @@ limitations under the License. */ import {tag, text, classNames} from "./general/html.js"; -import {BaseUpdateView} from "./general/BaseUpdateView.js"; - -/* -optimization to not use a sub view when changing between img and text -because there can be many many instances of this view -*/ - -export class AvatarView extends BaseUpdateView { - /** - * @param {ViewModel} value view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} - * @param {Number} size - */ - constructor(value, size) { - super(value); - this._root = null; - this._avatarUrl = null; - this._avatarTitle = null; - this._avatarLetter = null; - this._size = size; - } - - _avatarUrlChanged() { - if (this.value.avatarUrl(this._size) !== this._avatarUrl) { - this._avatarUrl = this.value.avatarUrl(this._size); - return true; - } - return false; - } - - _avatarTitleChanged() { - if (this.value.avatarTitle !== this._avatarTitle) { - this._avatarTitle = this.value.avatarTitle; - return true; - } - return false; - } - - _avatarLetterChanged() { - if (this.value.avatarLetter !== this._avatarLetter) { - this._avatarLetter = this.value.avatarLetter; - return true; - } - return false; - } - - mount(options) { - this._avatarUrlChanged(); - this._avatarLetterChanged(); - this._avatarTitleChanged(); - this._root = renderStaticAvatar(this.value, this._size); - // takes care of update being called when needed - super.mount(options); - return this._root; - } - - root() { - return this._root; - } - - update(vm) { - // important to always call _...changed for every prop - if (this._avatarUrlChanged()) { - // avatarColorNumber won't change, it's based on room/user id - const bgColorClass = `usercolor${vm.avatarColorNumber}`; - if (vm.avatarUrl(this._size)) { - this._root.replaceChild(renderImg(vm, this._size), this._root.firstChild); - this._root.classList.remove(bgColorClass); - } else { - this._root.replaceChild(text(vm.avatarLetter), this._root.firstChild); - this._root.classList.add(bgColorClass); - } - } - const hasAvatar = !!vm.avatarUrl(this._size); - if (this._avatarTitleChanged() && hasAvatar) { - const img = this._root.firstChild; - img.setAttribute("title", vm.avatarTitle); - } - if (this._avatarLetterChanged() && !hasAvatar) { - this._root.firstChild.textContent = vm.avatarLetter; - } - } -} - /** * @param {Object} vm view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} * @param {Number} size @@ -118,7 +35,7 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { return tag.div({className: avatarClasses, 'data-avatar-letter': vm.avatarLetter}, [avatarContent]); } -function renderImg(vm, size) { +export function renderImg(vm, size) { const sizeStr = size.toString(); return tag.img({src: vm.avatarUrl(size), width: sizeStr, height: sizeStr, title: vm.avatarTitle}); } diff --git a/src/platform/web/ui/session/leftpanel/RoomTileView.js b/src/platform/web/ui/session/leftpanel/RoomTileView.js index 84b38b62..228addba 100644 --- a/src/platform/web/ui/session/leftpanel/RoomTileView.js +++ b/src/platform/web/ui/session/leftpanel/RoomTileView.js @@ -16,7 +16,7 @@ limitations under the License. */ import {TemplateView} from "../../general/TemplateView.js"; -import {AvatarView} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView.js"; export class RoomTileView extends TemplateView { render(t, vm) { diff --git a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js index a6d1a81f..8357b722 100644 --- a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js @@ -1,6 +1,6 @@ import {TemplateView} from "../../general/TemplateView.js"; import {classNames, tag} from "../../general/html.js"; -import {AvatarView} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView.js"; export class RoomDetailsView extends TemplateView { render(t, vm) { diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index f8e84f87..ccad448f 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -22,7 +22,7 @@ import {TimelineList} from "./TimelineList.js"; import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; -import {AvatarView} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView.js"; export class RoomView extends TemplateView { constructor(options) { From 9ed6cd57f3a4c7a845746fc969f47a9bfbd92ae4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 1 Jul 2021 00:01:38 +0530 Subject: [PATCH 007/118] use textContent Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 7 ++----- src/platform/web/ui/css/avatar.css | 4 ---- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index bda44db0..36d9b172 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -25,8 +25,7 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, - [`usercolor${vm.avatarColorNumber}`]: true, - ['has-image']: true + [`usercolor${vm.avatarColorNumber}`]: true }); if (extraClasses) { avatarClasses += ` ${extraClasses}`; @@ -50,7 +49,5 @@ export function handleAvatarError(e) { if (!isAvatarEvent(e)) { return; } const parent = e.target.parentElement; const avatarLetter = parent.getAttribute("data-avatar-letter"); - const letterNode = document.createTextNode(avatarLetter); - parent.appendChild(letterNode); - parent.classList.remove("has-image"); + parent.textContent = avatarLetter; } diff --git a/src/platform/web/ui/css/avatar.css b/src/platform/web/ui/css/avatar.css index 85d370b7..d369f85f 100644 --- a/src/platform/web/ui/css/avatar.css +++ b/src/platform/web/ui/css/avatar.css @@ -31,10 +31,6 @@ limitations under the License. speak: none; } -.hydrogen .avatar:not(.has-image) img{ - display: none; -} - .hydrogen .avatar img { width: 100%; height: 100%; From 03a913629f0ef44ad120a5abc3721322c7dc237e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 1 Jul 2021 15:25:28 +0530 Subject: [PATCH 008/118] Pass color as data attribute Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 36d9b172..c1660e70 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -25,13 +25,13 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, - [`usercolor${vm.avatarColorNumber}`]: true + [`usercolor${vm.avatarColorNumber}`]: !hasAvatar }); if (extraClasses) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - return tag.div({className: avatarClasses, 'data-avatar-letter': vm.avatarLetter}, [avatarContent]); + return tag.div({className: avatarClasses, 'data-avatar-letter': vm.avatarLetter, 'data-avatar-color': vm.avatarColorNumber}, [avatarContent]); } export function renderImg(vm, size) { @@ -48,6 +48,8 @@ function isAvatarEvent(e) { export function handleAvatarError(e) { if (!isAvatarEvent(e)) { return; } const parent = e.target.parentElement; + const avatarColorNumber = parent.getAttribute("data-avatar-color"); + parent.classList.add(`usercolor${avatarColorNumber}`); const avatarLetter = parent.getAttribute("data-avatar-letter"); parent.textContent = avatarLetter; } From 93e77a3fcd313d360f0258201645c3c923a96c89 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 1 Jul 2021 15:41:40 +0530 Subject: [PATCH 009/118] Only add attribute if we have avatar Signed-off-by: RMidhunSuresh --- src/platform/web/ui/avatar.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index c1660e70..2e2b0142 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {tag, text, classNames} from "./general/html.js"; +import {tag, text, classNames, setAttribute} from "./general/html.js"; /** * @param {Object} vm view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} * @param {Number} size @@ -31,7 +31,12 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - return tag.div({className: avatarClasses, 'data-avatar-letter': vm.avatarLetter, 'data-avatar-color': vm.avatarColorNumber}, [avatarContent]); + const avatar = tag.div({className: avatarClasses}, [avatarContent]); + if (hasAvatar) { + setAttribute(avatar, "data-avatar-letter", vm.avatarLetter); + setAttribute(avatar, "data-avatar-color", vm.avatarColorNumber); + } + return avatar; } export function renderImg(vm, size) { From b8c01272f4ca3a679dd6e4084d78cf343a637442 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 1 Jul 2021 15:42:07 +0530 Subject: [PATCH 010/118] remove listener on dispose Signed-off-by: RMidhunSuresh --- src/platform/web/Platform.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 3d89be9a..49a90dd5 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -191,6 +191,7 @@ export class Platform { } } this._container.addEventListener("error", handleAvatarError, true); + this._disposables.track(() => this._container.removeEventListener("error", handleAvatarError, true)); window.__hydrogenViewModel = vm; const view = new RootView(vm); this._container.appendChild(view.mount()); From 191613adbecfcd89e667970ebd4c28435267a83c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 1 Jul 2021 19:21:54 +0530 Subject: [PATCH 011/118] Make changes - use textContent where possible - make sure we have an image before adding title Signed-off-by: RMidhunSuresh --- src/platform/web/ui/AvatarView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js index 22381838..1f6f2736 100644 --- a/src/platform/web/ui/AvatarView.js +++ b/src/platform/web/ui/AvatarView.js @@ -68,17 +68,19 @@ export class AvatarView extends BaseUpdateView { this._root.replaceChild(renderImg(vm, this._size), this._root.firstChild); this._root.classList.remove(bgColorClass); } else { - this._root.replaceChild(text(vm.avatarLetter), this._root.firstChild); + this._root.textContent = vm.avatarLetter; this._root.classList.add(bgColorClass); } } const hasAvatar = !!vm.avatarUrl(this._size); if (this._avatarTitleChanged() && hasAvatar) { - const img = this._root.firstChild; - img.setAttribute("title", vm.avatarTitle); + const element = this._root.firstChild; + if (element.tagName === "IMG") { + element.setAttribute("title", vm.avatarTitle); + } } if (this._avatarLetterChanged() && !hasAvatar) { - this._root.firstChild.textContent = vm.avatarLetter; + this._root.textContent = vm.avatarLetter; } } } From fb3c090de20a379c34f89e082c52766119e01407 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 15 Jul 2021 14:30:16 +0200 Subject: [PATCH 012/118] confirm the leave room --- src/platform/web/ui/session/room/RoomView.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index ccad448f..71eb1e13 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -70,7 +70,7 @@ export class RoomView extends TemplateView { const options = []; options.push(Menu.option(vm.i18n`Room details`, () => vm.openDetailsPanel())) if (vm.canLeave) { - options.push(Menu.option(vm.i18n`Leave room`, () => vm.leaveRoom()).setDestructive()); + options.push(Menu.option(vm.i18n`Leave room`, () => this._confirmToLeaveRoom()).setDestructive()); } if (vm.canForget) { options.push(Menu.option(vm.i18n`Forget room`, () => vm.forgetRoom()).setDestructive()); @@ -97,4 +97,10 @@ export class RoomView extends TemplateView { }); } } + + _confirmToLeaveRoom() { + if (confirm(this.value.i18n`Are you sure you want to leave "${this.value.name}"?`)) { + this.value.leaveRoom(); + } + } } From 69237fce85282ad44333c00b70b0315279fb4f47 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 11 Jun 2021 23:51:55 +0530 Subject: [PATCH 013/118] Basic barebones of memberlist view Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 24 +++++++++++++++++++ .../session/rightpanel/MemberTileViewModel.js | 12 ++++++++++ .../ui/session/rightpanel/MemberListView.js | 9 +++++++ .../ui/session/rightpanel/MemberTileView.js | 7 ++++++ 4 files changed, 52 insertions(+) create mode 100644 src/domain/session/rightpanel/MemberListViewModel.js create mode 100644 src/domain/session/rightpanel/MemberTileViewModel.js create mode 100644 src/platform/web/ui/session/rightpanel/MemberListView.js create mode 100644 src/platform/web/ui/session/rightpanel/MemberTileView.js diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js new file mode 100644 index 00000000..691efd0d --- /dev/null +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -0,0 +1,24 @@ +import {ViewModel} from "../../ViewModel.js"; +import {MemberTileViewModel} from "./MemberTileViewModel.js"; + +function comparator(member, otherMember) { + return member.displayName?.localeCompare(otherMember.displayName); +} + +export class MemberListViewModel extends ViewModel { + constructor(options) { + super(options); + this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) + .sortValues(comparator); + } + + _filterJoinedMembers(members) { + return members.filterValues(member => member.membership === "join"); + } + + _mapTileViewModels(members) { + return members.mapValues((member, emitChange) => { + return new MemberTileViewModel(this.childOptions({member, emitChange})); + }); + } +} diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js new file mode 100644 index 00000000..9ce14d78 --- /dev/null +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -0,0 +1,12 @@ +import {ViewModel} from "../../ViewModel.js"; + +export class MemberTileViewModel extends ViewModel { + constructor(options) { + super(options); + this.member = options.member; + } + + get displayName() { + return this.member.displayName; + } +} diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js new file mode 100644 index 00000000..b441f319 --- /dev/null +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -0,0 +1,9 @@ +import {TemplateView} from "../../general/TemplateView.js"; +import {ListView} from "../../general/ListView.js"; +import {MemberTileView} from "./MemberTileView.js"; + +export class MemberListView extends TemplateView { + render(t, vm) { + return t.view(new ListView({list: vm.memberTileViewModels}, tileViewModel => new MemberTileView(tileViewModel))); + } +} diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js new file mode 100644 index 00000000..e4b010a4 --- /dev/null +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -0,0 +1,7 @@ +import {TemplateView} from "../../general/TemplateView.js"; + +export class MemberTileView extends TemplateView { + render(t, vm) { + return t.div(vm.displayName); + } +} From 7e6e4ecfab5094cb3e10355f649506ea6a7b5ccb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 11 Jun 2021 23:56:07 +0530 Subject: [PATCH 014/118] Duplicate navigation from room details - TODO: Remove duplication Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 2 +- src/domain/session/SessionViewModel.js | 22 ++++++++++++++++++++++ src/platform/web/ui/session/SessionView.js | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index fab91c11..5dbb790f 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -37,7 +37,7 @@ function allowsChild(parent, child) { // downside of the approach: both of these will control which tile is selected return type === "room" || type === "empty-grid-tile"; case "room": - return type === "lightbox" || type === "details"; + return type === "lightbox" || type === "details" || type === "members"; default: return false; } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 087b9315..6fb8a11b 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -26,6 +26,7 @@ import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {ViewModel} from "../ViewModel.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; +import {MemberListViewModel} from "./rightpanel/MemberListViewModel.js"; export class SessionViewModel extends ViewModel { constructor(options) { @@ -84,6 +85,10 @@ export class SessionViewModel extends ViewModel { const details = this.navigation.observe("details"); this.track(details.subscribe(() => this._updateRoomDetails())); this._updateRoomDetails(); + + const members = this.navigation.observe("members"); + this.track(members.subscribe(() => this._toggleMemberListPanel())); + this._toggleMemberListPanel(); } get id() { @@ -122,6 +127,10 @@ export class SessionViewModel extends ViewModel { return this._roomDetailsViewModel; } + get memberListViewModel() { + return this._memberListViewModel; + } + _updateGrid(roomIds) { const changed = !(this._gridViewModel && roomIds); const currentRoomId = this.navigation.path.get("room"); @@ -267,4 +276,17 @@ export class SessionViewModel extends ViewModel { this.emitChange("roomDetailsViewModel"); } + async _toggleMemberListPanel() { + this._memberListViewModel = this.disposeTracked(this._memberListViewModel); + const enable = !!this.navigation.path.get("members")?.value; + if (enable) { + const room = this._roomFromNavigation(); + const list = await room.loadMemberList(); + const members = list.members; + this._memberListViewModel = this.track( + new MemberListViewModel(this.childOptions({members})) + ); + } + this.emitChange("memberListViewModel"); + } } diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 877cc67c..4323a07c 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -26,6 +26,7 @@ import {SessionStatusView} from "./SessionStatusView.js"; import {RoomGridView} from "./RoomGridView.js"; import {SettingsView} from "./settings/SettingsView.js"; import {RoomDetailsView} from "./rightpanel/RoomDetailsView.js"; +import {MemberListView} from "./rightpanel/MemberListView.js"; export class SessionView extends TemplateView { render(t, vm) { @@ -56,6 +57,7 @@ export class SessionView extends TemplateView { } }), t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), + t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : null), t.mapView(vm => vm.lightboxViewModel, lightboxViewModel => lightboxViewModel ? new LightboxView(lightboxViewModel) : null) ]); } From b74e4452ddf82564f6991bed6ad613cab7aeb6dd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 14:12:43 +0530 Subject: [PATCH 015/118] Move rightpanel to separate view and vm Signed-off-by: RMidhunSuresh --- .../session/rightpanel/RightPanelViewModel.js | 28 +++++++++++++++++++ .../ui/session/rightpanel/RightPanelView.js | 10 +++++++ 2 files changed, 38 insertions(+) create mode 100644 src/domain/session/rightpanel/RightPanelViewModel.js create mode 100644 src/platform/web/ui/session/rightpanel/RightPanelView.js diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js new file mode 100644 index 00000000..2ab291bd --- /dev/null +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -0,0 +1,28 @@ +import {ViewModel} from "../../ViewModel.js"; +import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js"; + +export class RightPanelViewModel extends ViewModel { + constructor(options) { + super(options); + this._room = options.room; + this._setupNavigation(); + } + + get roomDetailsViewModel() { return this._roomDetailsViewModel; } + + _setupNavigation() { + const details = this.navigation.observe("details"); + this.track(details.subscribe(() => this._toggleRoomDetailsPanel())); + this._toggleRoomDetailsPanel(); + } + + _toggleRoomDetailsPanel() { + this._roomDetailsViewModel = this.disposeTracked(this._roomDetailsViewModel); + const enable = !!this.navigation.path.get("details")?.value; + if (enable) { + const room = this._room; + this._roomDetailsViewModel = this.track(new RoomDetailsViewModel(this.childOptions({room}))); + } + this.emitChange("roomDetailsViewModel"); + } +} diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js new file mode 100644 index 00000000..67a34012 --- /dev/null +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -0,0 +1,10 @@ +import {TemplateView} from "../../general/TemplateView.js"; +import {RoomDetailsView} from "./RoomDetailsView.js"; + +export class RightPanelView extends TemplateView { + render(t, vm) { + return t.div({ className: "RightPanelView"}, + t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null) + ); + } +} From 7500bbeaad5101710993cc82d216e4dc93dd3b8b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 14:16:10 +0530 Subject: [PATCH 016/118] Modify navigation to work with rightpanel segment - Ensure that rightpanel does not show in the URL. - Create an action of details to insert the rightpanel segment. - Make sure rightpanel can be a child of room. Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 5dbb790f..a8d3bccd 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -37,7 +37,9 @@ function allowsChild(parent, child) { // downside of the approach: both of these will control which tile is selected return type === "room" || type === "empty-grid-tile"; case "room": - return type === "lightbox" || type === "details" || type === "members"; + return type === "lightbox" || type === "members" || type === "rightpanel"; + case "rightpanel": + return type === "details"; default: return false; } @@ -114,6 +116,7 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { } segments.push(new Segment("room", roomId)); if (currentNavPath.get("details")?.value) { + segments.push(new Segment("rightpanel")); segments.push(new Segment("details")); } } else if (type === "last-session") { @@ -124,6 +127,9 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { if (sessionSegment) { segments.push(sessionSegment); } + } else if (type === "details") { + segments.push(new Segment("rightpanel")); + segments.push(new Segment("details")); } else { // might be undefined, which will be turned into true by Segment const value = iterator.next().value; @@ -152,6 +158,8 @@ export function stringifyPath(path) { urlPath += `/${segment.type}/${segment.value}`; } break; + case "rightpanel": + continue; default: urlPath += `/${segment.type}`; if (segment.value && segment.value !== true) { @@ -185,6 +193,18 @@ export function tests() { const urlPath = stringifyPath(path); assert.equal(urlPath, "/session/1/rooms/a,b,c/1"); }, + "stringify url with rightpanel and details segment": assert => { + const nav = new Navigation(allowsChild); + const path = nav.pathFrom([ + new Segment("session", 1), + new Segment("rooms", ["a", "b", "c"]), + new Segment("room", "b"), + new Segment("rightpanel"), + new Segment("details") + ]); + const urlPath = stringifyPath(path); + assert.equal(urlPath, "/session/1/rooms/a,b,c/1/details"); + }, "parse grid url path with focused empty tile": assert => { const segments = parseUrlPath("/session/1/rooms/a,b,c/3"); assert.equal(segments.length, 3); @@ -263,18 +283,21 @@ export function tests() { new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), new Segment("room", "b"), + new Segment("rightpanel", true), new Segment("details", true) ]); const segments = parseUrlPath("/session/1/open-room/a", path); - assert.equal(segments.length, 4); + assert.equal(segments.length, 5); assert.equal(segments[0].type, "session"); assert.equal(segments[0].value, "1"); assert.equal(segments[1].type, "rooms"); assert.deepEqual(segments[1].value, ["a", "b", "c"]); assert.equal(segments[2].type, "room"); assert.equal(segments[2].value, "a"); - assert.equal(segments[3].type, "details"); + assert.equal(segments[3].type, "rightpanel"); assert.equal(segments[3].value, true); + assert.equal(segments[4].type, "details"); + assert.equal(segments[4].value, true); }, "parse open-room action setting a room in an empty tile": assert => { const nav = new Navigation(allowsChild); From 8b01ca502e7053a312be1124144885678aef4f20 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 14:19:55 +0530 Subject: [PATCH 017/118] Create RightPanel from SessionViewModel - Also remove old methods used in RoomDetails Signed-off-by: RMidhunSuresh --- src/domain/session/SessionViewModel.js | 46 +++++++++++----------- src/platform/web/ui/session/SessionView.js | 7 ++-- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 6fb8a11b..1a80aa82 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -17,7 +17,6 @@ limitations under the License. import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; -import {RoomDetailsViewModel} from "./rightpanel/RoomDetailsViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js"; import {LightboxViewModel} from "./room/LightboxViewModel.js"; @@ -27,6 +26,7 @@ import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {ViewModel} from "../ViewModel.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; import {MemberListViewModel} from "./rightpanel/MemberListViewModel.js"; +import { RightPanelViewModel } from "./rightpanel/RightPanelViewModel.js"; export class SessionViewModel extends ViewModel { constructor(options) { @@ -64,7 +64,7 @@ export class SessionViewModel extends ViewModel { if (!this._gridViewModel) { this._updateRoom(roomId); } - this._updateRoomDetails(); + this._updateRightPanel(); })); if (!this._gridViewModel) { this._updateRoom(currentRoomId.get()); @@ -82,13 +82,13 @@ export class SessionViewModel extends ViewModel { })); this._updateLightbox(lightbox.get()); - const details = this.navigation.observe("details"); - this.track(details.subscribe(() => this._updateRoomDetails())); - this._updateRoomDetails(); + // const members = this.navigation.observe("members"); + // this.track(members.subscribe(() => this._toggleMemberListPanel())); + // this._toggleMemberListPanel(); - const members = this.navigation.observe("members"); - this.track(members.subscribe(() => this._toggleMemberListPanel())); - this._toggleMemberListPanel(); + const rightpanel = this.navigation.observe("rightpanel"); + this.track(rightpanel.subscribe(() => this._updateRightPanel())); + this._updateRightPanel(); } get id() { @@ -123,14 +123,14 @@ export class SessionViewModel extends ViewModel { return this._roomViewModelObservable?.get(); } - get roomDetailsViewModel() { - return this._roomDetailsViewModel; - } - get memberListViewModel() { return this._memberListViewModel; } + get rightPanelViewModel() { + return this._rightPanelViewModel; + } + _updateGrid(roomIds) { const changed = !(this._gridViewModel && roomIds); const currentRoomId = this.navigation.path.get("room"); @@ -265,17 +265,6 @@ export class SessionViewModel extends ViewModel { return room; } - _updateRoomDetails() { - this._roomDetailsViewModel = this.disposeTracked(this._roomDetailsViewModel); - const enable = !!this.navigation.path.get("details")?.value; - if (enable) { - const room = this._roomFromNavigation(); - if (!room) { return; } - this._roomDetailsViewModel = this.track(new RoomDetailsViewModel(this.childOptions({room}))); - } - this.emitChange("roomDetailsViewModel"); - } - async _toggleMemberListPanel() { this._memberListViewModel = this.disposeTracked(this._memberListViewModel); const enable = !!this.navigation.path.get("members")?.value; @@ -289,4 +278,15 @@ export class SessionViewModel extends ViewModel { } this.emitChange("memberListViewModel"); } + + _updateRightPanel() { + this._rightPanelViewModel = this.disposeTracked(this._rightPanelViewModel); + const enable = !!this.navigation.path.get("rightpanel")?.value; + if (enable) { + const room = this._roomFromNavigation(); + this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room}))); + } + this.emitChange("rightPanelViewModel"); + } + } diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 4323a07c..15a5d0dd 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -27,6 +27,7 @@ import {RoomGridView} from "./RoomGridView.js"; import {SettingsView} from "./settings/SettingsView.js"; import {RoomDetailsView} from "./rightpanel/RoomDetailsView.js"; import {MemberListView} from "./rightpanel/MemberListView.js"; +import {RightPanelView} from "./rightpanel/RightPanelView.js"; export class SessionView extends TemplateView { render(t, vm) { @@ -34,7 +35,7 @@ export class SessionView extends TemplateView { className: { "SessionView": true, "middle-shown": vm => !!vm.activeMiddleViewModel, - "right-shown": vm => !!vm.roomDetailsViewModel + "right-shown": vm => !!vm.rightPanelViewModel }, }, [ t.view(new SessionStatusView(vm.sessionStatusViewModel)), @@ -56,9 +57,9 @@ export class SessionView extends TemplateView { return new StaticView(t => t.div({className: "room-placeholder"}, t.h2(vm.i18n`Choose a room on the left side.`))); } }), - t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : null), - t.mapView(vm => vm.lightboxViewModel, lightboxViewModel => lightboxViewModel ? new LightboxView(lightboxViewModel) : null) + t.mapView(vm => vm.lightboxViewModel, lightboxViewModel => lightboxViewModel ? new LightboxView(lightboxViewModel) : null), + t.mapView(vm => vm.rightPanelViewModel, rightPanelViewModel => rightPanelViewModel ? new RightPanelView(rightPanelViewModel) : null) ]); } } From dfe7385611561ae19cc76ae035f70a36ec3934b7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 14:22:48 +0530 Subject: [PATCH 018/118] Accommodate "rightpanel" navigation segment Signed-off-by: RMidhunSuresh --- src/domain/session/RoomGridViewModel.js | 1 + src/domain/session/leftpanel/LeftPanelViewModel.js | 7 ++++++- src/domain/session/room/RoomViewModel.js | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index dddc603b..c0aa5a52 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -83,6 +83,7 @@ export class RoomGridViewModel extends ViewModel { let path = this.navigation.path.until("rooms"); path = path.with(this.navigation.segment("room", roomId)); if (detailsShown) { + path = path.with(this.navigation.segment("rightpanel", true)); path = path.with(this.navigation.segment("details", true)); } this.navigation.applyPath(path); diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 061c640c..45f1f02b 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -93,8 +93,13 @@ export class LeftPanelViewModel extends ViewModel { } _pathForDetails(path) { + let _path = path; const details = this.navigation.path.get("details"); - return details?.value ? path.with(details) : path; + if (details?.value) { + _path = _path.with(this.navigation.segment("rightpanel")); + _path = _path.with(details) + } + return _path; } toggleGrid() { diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 38835db3..30dae699 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -290,6 +290,7 @@ export class RoomViewModel extends ViewModel { openDetailsPanel() { let path = this.navigation.path.until("room"); + path = path.with(this.navigation.segment("rightpanel", true)); path = path.with(this.navigation.segment("details", true)); this.navigation.applyPath(path); } From 1418645e151b99f84112e6813637fa8a11271c3d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 14:24:08 +0530 Subject: [PATCH 019/118] Modify CSS to reflect changes Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index f3f34e38..aff2aad6 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -1,6 +1,10 @@ -.RoomDetailsView { +.RightPanelView{ grid-area: right; +} + +.RoomDetailsView { flex-direction: column; + height: 100%; } .RoomDetailsView_avatar { From 46a6cf6adf4c3bb4f353a4575d2af8d79f1ac58d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 15:16:04 +0530 Subject: [PATCH 020/118] Add memberlist to navigation Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index a8d3bccd..1e4a5941 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -37,9 +37,9 @@ function allowsChild(parent, child) { // downside of the approach: both of these will control which tile is selected return type === "room" || type === "empty-grid-tile"; case "room": - return type === "lightbox" || type === "members" || type === "rightpanel"; + return type === "lightbox" || type === "rightpanel"; case "rightpanel": - return type === "details"; + return type === "details"|| type === "members"; default: return false; } @@ -130,6 +130,10 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { } else if (type === "details") { segments.push(new Segment("rightpanel")); segments.push(new Segment("details")); + } else if (type === "members") { + //TODO: Fix duplication here. + segments.push(new Segment("rightpanel")); + segments.push(new Segment("members")); } else { // might be undefined, which will be turned into true by Segment const value = iterator.next().value; From f3c7ab337d57b413f9334276e2607b3bf420b372 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 15:16:34 +0530 Subject: [PATCH 021/118] Remove code from session view/vm Signed-off-by: RMidhunSuresh --- src/domain/session/SessionViewModel.js | 23 +--------------------- src/platform/web/ui/session/SessionView.js | 3 --- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 1a80aa82..a66d66a0 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -25,8 +25,7 @@ import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {ViewModel} from "../ViewModel.js"; import {RoomViewModelObservable} from "./RoomViewModelObservable.js"; -import {MemberListViewModel} from "./rightpanel/MemberListViewModel.js"; -import { RightPanelViewModel } from "./rightpanel/RightPanelViewModel.js"; +import {RightPanelViewModel} from "./rightpanel/RightPanelViewModel.js"; export class SessionViewModel extends ViewModel { constructor(options) { @@ -82,9 +81,6 @@ export class SessionViewModel extends ViewModel { })); this._updateLightbox(lightbox.get()); - // const members = this.navigation.observe("members"); - // this.track(members.subscribe(() => this._toggleMemberListPanel())); - // this._toggleMemberListPanel(); const rightpanel = this.navigation.observe("rightpanel"); this.track(rightpanel.subscribe(() => this._updateRightPanel())); @@ -123,9 +119,6 @@ export class SessionViewModel extends ViewModel { return this._roomViewModelObservable?.get(); } - get memberListViewModel() { - return this._memberListViewModel; - } get rightPanelViewModel() { return this._rightPanelViewModel; @@ -265,20 +258,6 @@ export class SessionViewModel extends ViewModel { return room; } - async _toggleMemberListPanel() { - this._memberListViewModel = this.disposeTracked(this._memberListViewModel); - const enable = !!this.navigation.path.get("members")?.value; - if (enable) { - const room = this._roomFromNavigation(); - const list = await room.loadMemberList(); - const members = list.members; - this._memberListViewModel = this.track( - new MemberListViewModel(this.childOptions({members})) - ); - } - this.emitChange("memberListViewModel"); - } - _updateRightPanel() { this._rightPanelViewModel = this.disposeTracked(this._rightPanelViewModel); const enable = !!this.navigation.path.get("rightpanel")?.value; diff --git a/src/platform/web/ui/session/SessionView.js b/src/platform/web/ui/session/SessionView.js index 15a5d0dd..0cda8428 100644 --- a/src/platform/web/ui/session/SessionView.js +++ b/src/platform/web/ui/session/SessionView.js @@ -25,8 +25,6 @@ import {StaticView} from "../general/StaticView.js"; import {SessionStatusView} from "./SessionStatusView.js"; import {RoomGridView} from "./RoomGridView.js"; import {SettingsView} from "./settings/SettingsView.js"; -import {RoomDetailsView} from "./rightpanel/RoomDetailsView.js"; -import {MemberListView} from "./rightpanel/MemberListView.js"; import {RightPanelView} from "./rightpanel/RightPanelView.js"; export class SessionView extends TemplateView { @@ -57,7 +55,6 @@ export class SessionView extends TemplateView { return new StaticView(t => t.div({className: "room-placeholder"}, t.h2(vm.i18n`Choose a room on the left side.`))); } }), - t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : null), t.mapView(vm => vm.lightboxViewModel, lightboxViewModel => lightboxViewModel ? new LightboxView(lightboxViewModel) : null), t.mapView(vm => vm.rightPanelViewModel, rightPanelViewModel => rightPanelViewModel ? new RightPanelView(rightPanelViewModel) : null) ]); From a765d7fc7dd732478d245a624d27b9de3ddc1643 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 15:17:02 +0530 Subject: [PATCH 022/118] Create memberlist view from rightpanel Signed-off-by: RMidhunSuresh --- .../session/rightpanel/RightPanelViewModel.js | 19 +++++++++++++++++++ .../ui/session/rightpanel/RightPanelView.js | 8 ++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 2ab291bd..a9cdc23b 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -1,5 +1,6 @@ import {ViewModel} from "../../ViewModel.js"; import {RoomDetailsViewModel} from "./RoomDetailsViewModel.js"; +import {MemberListViewModel} from "./MemberListViewModel.js"; export class RightPanelViewModel extends ViewModel { constructor(options) { @@ -9,11 +10,16 @@ export class RightPanelViewModel extends ViewModel { } get roomDetailsViewModel() { return this._roomDetailsViewModel; } + get memberListViewModel() { return this._memberListViewModel; } _setupNavigation() { const details = this.navigation.observe("details"); this.track(details.subscribe(() => this._toggleRoomDetailsPanel())); this._toggleRoomDetailsPanel(); + + const members = this.navigation.observe("members"); + this.track(members.subscribe(() => this._toggleMemberListPanel())); + this._toggleMemberListPanel(); } _toggleRoomDetailsPanel() { @@ -25,4 +31,17 @@ export class RightPanelViewModel extends ViewModel { } this.emitChange("roomDetailsViewModel"); } + + async _toggleMemberListPanel() { + this._memberListViewModel = this.disposeTracked(this._memberListViewModel); + const enable = !!this.navigation.path.get("members")?.value; + if (enable) { + const list = await this._room.loadMemberList(); + const members = list.members; + this._memberListViewModel = this.track( + new MemberListViewModel(this.childOptions({members})) + ); + } + this.emitChange("memberListViewModel"); + } } diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index 67a34012..c6bf3b70 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -1,10 +1,14 @@ import {TemplateView} from "../../general/TemplateView.js"; import {RoomDetailsView} from "./RoomDetailsView.js"; +import {MemberListView} from "./MemberListView.js"; export class RightPanelView extends TemplateView { render(t, vm) { - return t.div({ className: "RightPanelView"}, - t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null) + return t.div({className: "RightPanelView"}, + [ + t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), + t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : null) + ] ); } } From abd2c19bf29a63ecb188538e60e4e50b6085a478 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 15:18:05 +0530 Subject: [PATCH 023/118] Remove unused param Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/RightPanelView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index c6bf3b70..077c3fc6 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -3,7 +3,7 @@ import {RoomDetailsView} from "./RoomDetailsView.js"; import {MemberListView} from "./MemberListView.js"; export class RightPanelView extends TemplateView { - render(t, vm) { + render(t) { return t.div({className: "RightPanelView"}, [ t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), From 7e72d57be7942273bb1fd819cb13636ff4e7d8db Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 17 Jun 2021 16:45:36 +0530 Subject: [PATCH 024/118] Make list scrollable Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 5 +++++ src/platform/web/ui/session/rightpanel/MemberListView.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index aff2aad6..8d6fd3b1 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -33,3 +33,8 @@ justify-content: flex-end; width: 100%; } + +ul.MemberListView { + overflow: scroll; + height: 100vh; +} diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index b441f319..5b941a23 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -4,6 +4,6 @@ import {MemberTileView} from "./MemberTileView.js"; export class MemberListView extends TemplateView { render(t, vm) { - return t.view(new ListView({list: vm.memberTileViewModels}, tileViewModel => new MemberTileView(tileViewModel))); + return t.view(new ListView({list: vm.memberTileViewModels, className:"MemberListView"}, tileViewModel => new MemberTileView(tileViewModel))); } } From ddb7a16fb8a22e21ba614f9e891bc0486156f115 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:12:20 +0530 Subject: [PATCH 025/118] Make member private Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberTileViewModel.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index 9ce14d78..fd5ba096 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -3,10 +3,14 @@ import {ViewModel} from "../../ViewModel.js"; export class MemberTileViewModel extends ViewModel { constructor(options) { super(options); - this.member = options.member; + this._member = options.member; } get displayName() { - return this.member.displayName; + return this._member.displayName; + } + + get userId() { + return this._member.userId; } } From f7a6fbd90171eeb84f28098512d9c957b95c6dd8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:13:19 +0530 Subject: [PATCH 026/118] Make getUserLevel() public Signed-off-by: RMidhunSuresh --- src/matrix/room/timeline/PowerLevels.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/timeline/PowerLevels.js b/src/matrix/room/timeline/PowerLevels.js index d2fb026d..c19f992d 100644 --- a/src/matrix/room/timeline/PowerLevels.js +++ b/src/matrix/room/timeline/PowerLevels.js @@ -42,10 +42,10 @@ export class PowerLevels { if (this._membership !== "join") { return Number.MIN_SAFE_INTEGER; } - return this._getUserLevel(this._ownUserId); + return this.getUserLevel(this._ownUserId); } - _getUserLevel(userId) { + getUserLevel(userId) { if (this._plEvent) { let userLevel = this._plEvent.content?.users?.[userId]; if (typeof userLevel !== "number") { From cb5e598e955535889c46d058f63342e871f53226 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:14:11 +0530 Subject: [PATCH 027/118] Getter for powerlevel from room Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 9d33c5c5..ac5fa59e 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -388,6 +388,10 @@ export class BaseRoom extends EventEmitter { return this._summary.data.membership; } + get powerLevels() { + return this._timeline.powerLevels; + } + enableSessionBackup(sessionBackup) { this._roomEncryption?.enableSessionBackup(sessionBackup); // TODO: do we really want to do this every time you open the app? From 3e23392a0e98727d0cbebfb64c148d36e1da61d8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:15:17 +0530 Subject: [PATCH 028/118] Consider powerlevels in comparator and add tests Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/comparator.js | 92 +++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/domain/session/rightpanel/comparator.js diff --git a/src/domain/session/rightpanel/comparator.js b/src/domain/session/rightpanel/comparator.js new file mode 100644 index 00000000..b369c5f2 --- /dev/null +++ b/src/domain/session/rightpanel/comparator.js @@ -0,0 +1,92 @@ +import {PowerLevels} from "../../../matrix/room/timeline/PowerLevels.js"; + +export function createMemberComparator(powerLevels) { + return function comparator(member, otherMember) { + const p1 = powerLevels.getUserLevel(member.userId); + const p2 = powerLevels.getUserLevel(otherMember.userId); + if (p1 !== p2) { return p2 - p1; } + return member.displayName?.localeCompare(otherMember.displayName); + }; +} + +export function tests() { + + function createComparatorWithPowerLevel(map) { + let users = {}; + for (const prop in map) { + Object.assign(users, {[prop]: map[prop]}); + } + const powerLevelEvent = { + content: {users, users_default: 0} + }; + return createMemberComparator(new PowerLevels({powerLevelEvent})); + } + + return { + "power_level(member1) > power_level(member2) returns value <= 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50}); + const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2) <= 0, true); + }, + + "power_level(member1) < power_level(member2) returns value > 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50}); + const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + assert.strictEqual(fn(member1, member2) > 0, true); + }, + + "alphabetic compare on displayName": assert => { + const fn = createComparatorWithPowerLevel(); + const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + assert.strictEqual(fn(member1, member2) > 0, true); + assert.strictEqual(fn(member2, member1) <= 0, true); + }, + + "alphabetic compare with case (alice comes before Bob)": assert => { + const fn = createComparatorWithPowerLevel(); + const member1 = {userId: "@bob:hs.tld", displayName: "Bob"}; + const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + assert.strictEqual(fn(member1, member2) > 0, true); + assert.strictEqual(fn(member2, member1) <= 0, true); + }, + + "equal powerlevel and same names returns 0": assert => { + const fn = createComparatorWithPowerLevel({"@bobby:hs.tld": 50, "@bob:hs.tld": 50}); + const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member2 = {userId: "@bobby:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2), 0); + assert.strictEqual(fn(member2, member1), 0); + }, + + "(both_negative_powerlevel) power_level(member1) < power_level(member2) returns value > 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -100, "@bob:hs.tld": -50}); + const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2) > 0, true); + }, + + "(both_negative_powerlevel) power_level(member1) > power_level(member2) returns value <= 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -50, "@bob:hs.tld": -100}); + const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2) <= 0, true); + }, + + "(one_negative_powerlevel) power_level(member1) > power_level(member2) returns value <= 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50, "@bob:hs.tld": -100}); + const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2) <= 0, true); + }, + + "(one_negative_powerlevel) power_level(member1) < power_level(member2) returns value > 0": assert => { + const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -100, "@bob:hs.tld": 50}); + const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + assert.strictEqual(fn(member1, member2) > 0, true); + }, + }; +} From a9ff6ab026b901fb2ad5514760318f5961699884 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:16:23 +0530 Subject: [PATCH 029/118] Sort memberlist using powerlevel Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 7 ++----- src/domain/session/rightpanel/RightPanelViewModel.js | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 691efd0d..737fb6cf 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -1,15 +1,12 @@ import {ViewModel} from "../../ViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; - -function comparator(member, otherMember) { - return member.displayName?.localeCompare(otherMember.displayName); -} +import {createMemberComparator} from "./comparator.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) - .sortValues(comparator); + .sortValues(createMemberComparator(options.powerLevels)); } _filterJoinedMembers(members) { diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index a9cdc23b..3010f79c 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -38,8 +38,9 @@ export class RightPanelViewModel extends ViewModel { if (enable) { const list = await this._room.loadMemberList(); const members = list.members; + const powerLevels = this._room.powerLevels; this._memberListViewModel = this.track( - new MemberListViewModel(this.childOptions({members})) + new MemberListViewModel(this.childOptions({members, powerLevels})) ); } this.emitChange("memberListViewModel"); From a1e3ff3ed8a4f46668dbbaa816270886478f537c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 18 Jun 2021 13:29:08 +0530 Subject: [PATCH 030/118] Use name instead of displayName Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberTileViewModel.js | 4 +- src/domain/session/rightpanel/comparator.js | 40 +++++++++---------- .../ui/session/rightpanel/MemberTileView.js | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index fd5ba096..c6c489de 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -6,8 +6,8 @@ export class MemberTileViewModel extends ViewModel { this._member = options.member; } - get displayName() { - return this._member.displayName; + get name() { + return this._member.name; } get userId() { diff --git a/src/domain/session/rightpanel/comparator.js b/src/domain/session/rightpanel/comparator.js index b369c5f2..d4f86176 100644 --- a/src/domain/session/rightpanel/comparator.js +++ b/src/domain/session/rightpanel/comparator.js @@ -5,7 +5,7 @@ export function createMemberComparator(powerLevels) { const p1 = powerLevels.getUserLevel(member.userId); const p2 = powerLevels.getUserLevel(otherMember.userId); if (p1 !== p2) { return p2 - p1; } - return member.displayName?.localeCompare(otherMember.displayName); + return member.name?.localeCompare(otherMember.name); }; } @@ -25,67 +25,67 @@ export function tests() { return { "power_level(member1) > power_level(member2) returns value <= 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50}); - const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; - const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member1 = {userId: "@alice:hs.tld", name: "alice"}; + const member2 = {userId: "@bob:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2) <= 0, true); }, "power_level(member1) < power_level(member2) returns value > 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50}); - const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; - const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member1 = {userId: "@bob:hs.tld", name: "bob"}; + const member2 = {userId: "@alice:hs.tld", name: "alice"}; assert.strictEqual(fn(member1, member2) > 0, true); }, - "alphabetic compare on displayName": assert => { + "alphabetic compare on name": assert => { const fn = createComparatorWithPowerLevel(); - const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; - const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member1 = {userId: "@bob:hs.tld", name: "bob"}; + const member2 = {userId: "@alice:hs.tld", name: "alice"}; assert.strictEqual(fn(member1, member2) > 0, true); assert.strictEqual(fn(member2, member1) <= 0, true); }, "alphabetic compare with case (alice comes before Bob)": assert => { const fn = createComparatorWithPowerLevel(); - const member1 = {userId: "@bob:hs.tld", displayName: "Bob"}; - const member2 = {userId: "@alice:hs.tld", displayName: "alice"}; + const member1 = {userId: "@bob:hs.tld", name: "Bob"}; + const member2 = {userId: "@alice:hs.tld", name: "alice"}; assert.strictEqual(fn(member1, member2) > 0, true); assert.strictEqual(fn(member2, member1) <= 0, true); }, "equal powerlevel and same names returns 0": assert => { const fn = createComparatorWithPowerLevel({"@bobby:hs.tld": 50, "@bob:hs.tld": 50}); - const member1 = {userId: "@bob:hs.tld", displayName: "bob"}; - const member2 = {userId: "@bobby:hs.tld", displayName: "bob"}; + const member1 = {userId: "@bob:hs.tld", name: "bob"}; + const member2 = {userId: "@bobby:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2), 0); assert.strictEqual(fn(member2, member1), 0); }, "(both_negative_powerlevel) power_level(member1) < power_level(member2) returns value > 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -100, "@bob:hs.tld": -50}); - const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; - const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member1 = {userId: "@alice:hs.tld", name: "alice"}; + const member2 = {userId: "@bob:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2) > 0, true); }, "(both_negative_powerlevel) power_level(member1) > power_level(member2) returns value <= 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -50, "@bob:hs.tld": -100}); - const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; - const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member1 = {userId: "@alice:hs.tld", name: "alice"}; + const member2 = {userId: "@bob:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2) <= 0, true); }, "(one_negative_powerlevel) power_level(member1) > power_level(member2) returns value <= 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": 50, "@bob:hs.tld": -100}); - const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; - const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member1 = {userId: "@alice:hs.tld", name: "alice"}; + const member2 = {userId: "@bob:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2) <= 0, true); }, "(one_negative_powerlevel) power_level(member1) < power_level(member2) returns value > 0": assert => { const fn = createComparatorWithPowerLevel({"@alice:hs.tld": -100, "@bob:hs.tld": 50}); - const member1 = {userId: "@alice:hs.tld", displayName: "alice"}; - const member2 = {userId: "@bob:hs.tld", displayName: "bob"}; + const member1 = {userId: "@alice:hs.tld", name: "alice"}; + const member2 = {userId: "@bob:hs.tld", name: "bob"}; assert.strictEqual(fn(member1, member2) > 0, true); }, }; diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index e4b010a4..59d61b62 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -2,6 +2,6 @@ import {TemplateView} from "../../general/TemplateView.js"; export class MemberTileView extends TemplateView { render(t, vm) { - return t.div(vm.displayName); + return t.div(vm.name); } } From 2e8c4563aa3e31bd9a5c6386ee59d45c5d67b432 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 13:33:26 +0530 Subject: [PATCH 031/118] Implement set method to support update from value Signed-off-by: RMidhunSuresh --- src/observable/map/ObservableMap.js | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/observable/map/ObservableMap.js b/src/observable/map/ObservableMap.js index 4e9df5bb..b72cd039 100644 --- a/src/observable/map/ObservableMap.js +++ b/src/observable/map/ObservableMap.js @@ -54,6 +54,17 @@ export class ObservableMap extends BaseObservableMap { } } + set(key, value) { + if (this._values.has(key)) { + // We set the value here because update only supports inline updates + this._values.set(key, value); + return this.update(key); + } + else { + return this.add(key, value); + } + } + reset() { this._values.clear(); this.emitReset(); @@ -136,6 +147,31 @@ export function tests() { assert.equal(result, false); }, + test_set(assert) { + let add_fired = 0, update_fired = 0; + const map = new ObservableMap(); + map.subscribe({ + onAdd(key, value) { + add_fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 5}); + }, + onUpdate(key, value, params) { + update_fired += 1; + assert.equal(key, 1); + assert.deepEqual(value, {value: 7}); + } + }); + // Add + map.set(1, {value: 5}); + assert.equal(map.size, 1); + assert.equal(add_fired, 1); + // Update + map.set(1, {value: 7}); + assert.equal(map.size, 1); + assert.equal(update_fired, 1); + }, + test_remove(assert) { let fired = 0; const map = new ObservableMap(); From 11eb9c7783dadea01323b331f6c7fa245aafe2bd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 15:35:17 +0530 Subject: [PATCH 032/118] Use set instead of add in memberlist Signed-off-by: RMidhunSuresh --- src/matrix/room/members/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/room/members/MemberList.js b/src/matrix/room/members/MemberList.js index 05e6ea9a..de07adf5 100644 --- a/src/matrix/room/members/MemberList.js +++ b/src/matrix/room/members/MemberList.js @@ -28,7 +28,7 @@ export class MemberList extends RetainedValue { afterSync(memberChanges) { for (const [userId, memberChange] of memberChanges.entries()) { - this._members.add(userId, memberChange.member); + this._members.set(userId, memberChange.member); } } From 4fcaac3865159b90d1df98699965c726213b8778 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 15:46:01 +0530 Subject: [PATCH 033/118] Add binding in view Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberTileView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index 59d61b62..4e299ace 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -2,6 +2,6 @@ import {TemplateView} from "../../general/TemplateView.js"; export class MemberTileView extends TemplateView { render(t, vm) { - return t.div(vm.name); + return t.div(vm => vm.name); } } From b62473ae79cdc1731ddd3228c90f1b2b1c571461 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 15:51:18 +0530 Subject: [PATCH 034/118] Add update method Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberTileViewModel.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index c6c489de..899f2b7b 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -3,7 +3,8 @@ import {ViewModel} from "../../ViewModel.js"; export class MemberTileViewModel extends ViewModel { constructor(options) { super(options); - this._member = options.member; + this._member = this._options.member; + this._shouldDisambiguate = options.shouldDisambiguate; } get name() { @@ -13,4 +14,9 @@ export class MemberTileViewModel extends ViewModel { get userId() { return this._member.userId; } + + updateFrom(newMember) { + this._member = newMember; + } + } From 0819dcb29ef1ad9f46ba25053d06e1366d6fcf49 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 15:52:05 +0530 Subject: [PATCH 035/118] Use MappedList instead of MappedMap - MappedList supports an optional updater method Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 737fb6cf..9f8f382d 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -1,12 +1,13 @@ import {ViewModel} from "../../ViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {createMemberComparator} from "./comparator.js"; +import {MappedList} from "../../../observable/list/MappedList.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); - this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) - .sortValues(createMemberComparator(options.powerLevels)); + this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members) + .sortValues(createMemberComparator(options.powerLevels))); } _filterJoinedMembers(members) { @@ -14,8 +15,10 @@ export class MemberListViewModel extends ViewModel { } _mapTileViewModels(members) { - return members.mapValues((member, emitChange) => { - return new MemberTileViewModel(this.childOptions({member, emitChange})); - }); + const mapper = (member) => { + return new MemberTileViewModel(this.childOptions({member})); + } + const updater = (vm, params, newMember) => vm.updateFrom(newMember); + return new MappedList(members, mapper, updater); } } From f41c835e91644e1c18798dee21a0309a37594c3e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 17:33:15 +0530 Subject: [PATCH 036/118] Support updater in MappedMap Signed-off-by: RMidhunSuresh --- src/observable/index.js | 4 ++-- src/observable/map/MappedMap.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/observable/index.js b/src/observable/index.js index 4c455407..47b68e91 100644 --- a/src/observable/index.js +++ b/src/observable/index.js @@ -34,8 +34,8 @@ Object.assign(BaseObservableMap.prototype, { return new SortedMapList(this, comparator); }, - mapValues(mapper) { - return new MappedMap(this, mapper); + mapValues(mapper, updater) { + return new MappedMap(this, mapper, updater); }, filterValues(filter) { diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.js index ec33c4dd..47013df8 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.js @@ -20,10 +20,11 @@ so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate how should the mapped value be notified of an update though? and can it then decide to not propagate the update? */ export class MappedMap extends BaseObservableMap { - constructor(source, mapper) { + constructor(source, mapper, updater) { super(); this._source = source; this._mapper = mapper; + this._updater = updater; this._mappedValues = new Map(); } @@ -55,6 +56,7 @@ export class MappedMap extends BaseObservableMap { } const mappedValue = this._mappedValues.get(key); if (mappedValue !== undefined) { + this._updater?.(mappedValue, params, value); // TODO: map params somehow if needed? this.emitUpdate(key, mappedValue, params); } From 404129f60eb18f68243f14f7a9efa04eda30b9bb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 22 Jun 2021 17:33:31 +0530 Subject: [PATCH 037/118] Settle on MappedMap Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 9f8f382d..f7e2afc0 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -1,13 +1,12 @@ import {ViewModel} from "../../ViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {createMemberComparator} from "./comparator.js"; -import {MappedList} from "../../../observable/list/MappedList.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); - this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members) - .sortValues(createMemberComparator(options.powerLevels))); + this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) + .sortValues(createMemberComparator(options.powerLevels)); } _filterJoinedMembers(members) { @@ -15,10 +14,10 @@ export class MemberListViewModel extends ViewModel { } _mapTileViewModels(members) { - const mapper = (member) => { - return new MemberTileViewModel(this.childOptions({member})); + const mapper = (member, emitUpdate) => { + return new MemberTileViewModel(this.childOptions({member, emitUpdate})); } const updater = (vm, params, newMember) => vm.updateFrom(newMember); - return new MappedList(members, mapper, updater); + return members.mapValues(mapper, updater); } } From dea0cad252958a8bcb670e54851ef60f656c236b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 24 Jun 2021 16:26:33 +0530 Subject: [PATCH 038/118] Fix bug in FilteredMap Caused multiple updates to be triggered. Signed-off-by: RMidhunSuresh --- src/observable/map/FilteredMap.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.js index 8e936f5d..f7090502 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.js @@ -91,8 +91,9 @@ export class FilteredMap extends BaseObservableMap { const isIncluded = this._filter(value, key); this._included.set(key, isIncluded); this._emitForUpdate(wasIncluded, isIncluded, key, value, params); + } else { + this.emitUpdate(key, value, params); } - this.emitUpdate(key, value, params); } _emitForUpdate(wasIncluded, isIncluded, key, value, params = null) { @@ -191,5 +192,31 @@ export function tests() { // "filter changed values": assert => { // }, + + "emits must trigger once": assert => { + const source = new ObservableMap(); + let count_add = 0, count_update = 0, count_remove = 0; + source.add("num1", 1); + source.add("num2", 2); + source.add("num3", 3); + const oddMap = new FilteredMap(source, x => x % 2 !== 0); + oddMap.subscribe({ + onAdd() { + count_add += 1; + }, + onRemove() { + count_remove += 1; + }, + onUpdate() { + count_update += 1; + } + }); + source.set("num3", 4); + source.set("num3", 5); + source.set("num3", 7); + assert.strictEqual(count_add, 1); + assert.strictEqual(count_update, 1); + assert.strictEqual(count_remove, 1); + } } } From b8542c675467a9a5d75aae44b2783250131010f2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Jun 2021 20:12:06 +0530 Subject: [PATCH 039/118] Implement name disambiguation Signed-off-by: RMidhunSuresh --- .../session/rightpanel/disambiguator.js | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/domain/session/rightpanel/disambiguator.js diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js new file mode 100644 index 00000000..e740cd49 --- /dev/null +++ b/src/domain/session/rightpanel/disambiguator.js @@ -0,0 +1,147 @@ +export class Disambiguator { + constructor() { + this._map = new Map(); + } + + _flatten(name, array) { + const vm = array.pop(); + vm.setDisambiguation(false); + this._map.set(name, vm); + } + + _unDisambiguate(vm, array) { + const idx = array.indexOf(vm); + if (idx !== -1) { + const [removed] = array.splice(idx, 1); + removed.setDisambiguation(false); + } + } + + async _handlePreviousName(vm) { + const previousName = vm.previousName; + if (!previousName) { return; } + const value = this._map.get(previousName); + if (Array.isArray(value)) { + this._unDisambiguate(vm, value); + if (value.length === 1) { this._flatten(previousName, value); } + } else { + this._map.delete(previousName); + } + } + + async _updateMap(vm) { + const name = vm.name; + if (this._map.has(name)) { + const value = this._map.get(name); + if (Array.isArray(value)) { + value.push(vm); + } else { + this._map.set(name, [value, vm]); + } + } else { + this._map.set(name, vm); + } + } + + async disambiguate(vm) { + if (!vm.nameChanged) { return; } + await this._handlePreviousName(vm); + await this._updateMap(vm); + const value = this._map.get(vm.name); + if (Array.isArray(value)) { + value.forEach((vm) => vm.setDisambiguation(true)); + } + } +} + +export function tests(){ + + class MockViewModel { + constructor(name, userId) { + this.name = name; + this.disambiguate = false; + this.userId = userId; + this.nameChanged = true; + } + + updateName(newName) { + if (this.name !== newName) { + this.previousName = this.name; + this.nameChanged = true; + } + else { + this.nameChanged = false; + } + this.name = newName; + } + + setDisambiguation(status) { + this.disambiguate = status; + } + } + + function createVmAndDisambiguator(nameList) { + const d = new Disambiguator(); + const array = nameList.map(([name, id]) => new MockViewModel(name, id)); + return [...array, d]; + } + + return { + "Unique names": async assert => { + const [vm1, vm2, d] = createVmAndDisambiguator([["foo", "a"], ["bar", "b"]]); + await d.disambiguate(vm1); + await d.disambiguate(vm2); + assert.strictEqual(vm1.disambiguate, false); + assert.strictEqual(vm2.disambiguate, false); + }, + + "Same names are disambiguated": async assert => { + const [vm1, vm2, vm3, d] = createVmAndDisambiguator([["foo", "a"], ["foo", "b"], ["foo", "c"]]); + await d.disambiguate(vm1); + await d.disambiguate(vm2); + await d.disambiguate(vm3); + assert.strictEqual(vm1.disambiguate, true); + assert.strictEqual(vm2.disambiguate, true); + assert.strictEqual(vm3.disambiguate, true); + }, + + "Name updates disambiguate": async assert => { + const [vm1, vm2, vm3, d] = createVmAndDisambiguator([["foo", "a"], ["bar", "b"], ["jar", "c"]]); + await d.disambiguate(vm1); + await d.disambiguate(vm2); + await d.disambiguate(vm3); + + vm2.updateName("foo"); + await d.disambiguate(vm2); + assert.strictEqual(vm1.disambiguate, true); + assert.strictEqual(vm2.disambiguate, true); + + vm1.updateName("bar"); + await d.disambiguate(vm1); + assert.strictEqual(vm1.disambiguate, false); + assert.strictEqual(vm2.disambiguate, false); + + vm3.updateName("foo"); + await d.disambiguate(vm3); + vm1.updateName("foo"); + await d.disambiguate(vm1); + assert.strictEqual(vm1.disambiguate, true); + assert.strictEqual(vm2.disambiguate, true); + assert.strictEqual(vm3.disambiguate, true); + + vm2.updateName("bar"); + await d.disambiguate(vm2); + assert.strictEqual(vm1.disambiguate, true); + assert.strictEqual(vm2.disambiguate, false); + assert.strictEqual(vm3.disambiguate, true); + }, + + "Multiple disambiguate events": async assert => { + const [vm1, d] = createVmAndDisambiguator([["foo", "a"]]); + await d.disambiguate(vm1); + vm1.updateName(vm1.name); + await d.disambiguate(vm1); + assert.strictEqual(vm1.disambiguate, false); + }, + }; +} From bcb46fc54a5568eed2a964f7fb99e2b62fbd7814 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Jun 2021 20:12:31 +0530 Subject: [PATCH 040/118] Make tile vm support disambiguation Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberTileViewModel.js | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index 899f2b7b..3ee201db 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -4,19 +4,47 @@ export class MemberTileViewModel extends ViewModel { constructor(options) { super(options); this._member = this._options.member; - this._shouldDisambiguate = options.shouldDisambiguate; + this._previousName = null; + this._nameChanged = true; } get name() { - return this._member.name; + return `${this._member.name}${this._disambiguationPart}`; + } + + get _disambiguationPart() { + return this._disambiguate ? ` (${this.userId})` : ""; } get userId() { return this._member.userId; } - updateFrom(newMember) { - this._member = newMember; + get previousName() { + return this._previousName; } + get nameChanged() { + return this._nameChanged; + } + + _updatePreviousName(newName) { + const currentName = this._member.name; + if (currentName !== newName) { + this._previousName = currentName; + this._nameChanged = true; + } else { + this._nameChanged = false; + } + } + + setDisambiguation(status) { + this._disambiguate = status; + this.emitChange(); + } + + updateFrom(newMember) { + this._updatePreviousName(newMember.name); + this._member = newMember; + } } From ef400275026eb124d60bf4c9f5e372f8da717346 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Jun 2021 20:13:02 +0530 Subject: [PATCH 041/118] Call disambiguate on update/map Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index f7e2afc0..48d99747 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -1,12 +1,14 @@ import {ViewModel} from "../../ViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; import {createMemberComparator} from "./comparator.js"; +import {Disambiguator} from "./disambiguator.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) .sortValues(createMemberComparator(options.powerLevels)); + this.nameDisambiguator = new Disambiguator(); } _filterJoinedMembers(members) { @@ -14,10 +16,16 @@ export class MemberListViewModel extends ViewModel { } _mapTileViewModels(members) { - const mapper = (member, emitUpdate) => { - return new MemberTileViewModel(this.childOptions({member, emitUpdate})); + const mapper = (member, emitChange) => { + const vm = new MemberTileViewModel(this.childOptions({member, emitChange})); + this.nameDisambiguator.disambiguate(vm); + return vm; } - const updater = (vm, params, newMember) => vm.updateFrom(newMember); + const updater = (vm, params, newMember) => { + vm.updateFrom(newMember); + this.nameDisambiguator.disambiguate(vm); + }; return members.mapValues(mapper, updater); } + } From 3d4ba2015070eb3600615879b0ee34f8945707bf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Jun 2021 20:46:15 +0530 Subject: [PATCH 042/118] Put up a temporary loading view Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberListView.js | 7 +++++++ src/platform/web/ui/session/rightpanel/RightPanelView.js | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 5b941a23..9c4a847a 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -1,9 +1,16 @@ import {TemplateView} from "../../general/TemplateView.js"; import {ListView} from "../../general/ListView.js"; import {MemberTileView} from "./MemberTileView.js"; +import {spinner} from "../../common.js"; export class MemberListView extends TemplateView { render(t, vm) { return t.view(new ListView({list: vm.memberTileViewModels, className:"MemberListView"}, tileViewModel => new MemberTileView(tileViewModel))); } } + +export class MemberListLoadingView extends TemplateView { + render(t) { + return t.div(["Loading ", spinner(t)]); + } +} diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index 077c3fc6..aac92458 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -1,13 +1,13 @@ import {TemplateView} from "../../general/TemplateView.js"; import {RoomDetailsView} from "./RoomDetailsView.js"; -import {MemberListView} from "./MemberListView.js"; +import {MemberListLoadingView, MemberListView} from "./MemberListView.js"; export class RightPanelView extends TemplateView { render(t) { return t.div({className: "RightPanelView"}, [ t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), - t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : null) + t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : new MemberListLoadingView()) ] ); } From 86c1550850e4ccaef42c408df1857ea11c578af8 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 25 Jun 2021 23:27:46 +0530 Subject: [PATCH 043/118] Switch to collator for perf reasons Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/comparator.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/comparator.js b/src/domain/session/rightpanel/comparator.js index d4f86176..089ea1ad 100644 --- a/src/domain/session/rightpanel/comparator.js +++ b/src/domain/session/rightpanel/comparator.js @@ -1,11 +1,17 @@ import {PowerLevels} from "../../../matrix/room/timeline/PowerLevels.js"; export function createMemberComparator(powerLevels) { + const collator = new Intl.Collator(); + //TODO: This is so that all names with @ do not club together; but do we care? + const removeCharacter = string => string.charAt(0) === "@"? string.slice(1) : string; + return function comparator(member, otherMember) { const p1 = powerLevels.getUserLevel(member.userId); const p2 = powerLevels.getUserLevel(otherMember.userId); if (p1 !== p2) { return p2 - p1; } - return member.name?.localeCompare(otherMember.name); + const name = removeCharacter(member.name); + const otherName = removeCharacter(otherMember.name); + return collator.compare(name, otherName); }; } From 7139711314a8f0938bf0d06dc8e1d9c6ea790d4b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 26 Jun 2021 14:32:39 +0530 Subject: [PATCH 044/118] Add avatar to tile Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 4 +++- .../session/rightpanel/MemberTileViewModel.js | 18 ++++++++++++++++++ .../session/rightpanel/RightPanelViewModel.js | 3 ++- .../ui/session/rightpanel/MemberTileView.js | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 48d99747..114c61b7 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -9,6 +9,7 @@ export class MemberListViewModel extends ViewModel { this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) .sortValues(createMemberComparator(options.powerLevels)); this.nameDisambiguator = new Disambiguator(); + this.mediaRepository = options.mediaRepository; } _filterJoinedMembers(members) { @@ -17,7 +18,8 @@ export class MemberListViewModel extends ViewModel { _mapTileViewModels(members) { const mapper = (member, emitChange) => { - const vm = new MemberTileViewModel(this.childOptions({member, emitChange})); + const mediaRepository = this.mediaRepository; + const vm = new MemberTileViewModel(this.childOptions({member, emitChange, mediaRepository})); this.nameDisambiguator.disambiguate(vm); return vm; } diff --git a/src/domain/session/rightpanel/MemberTileViewModel.js b/src/domain/session/rightpanel/MemberTileViewModel.js index 3ee201db..23a7eeef 100644 --- a/src/domain/session/rightpanel/MemberTileViewModel.js +++ b/src/domain/session/rightpanel/MemberTileViewModel.js @@ -1,9 +1,11 @@ import {ViewModel} from "../../ViewModel.js"; +import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar.js"; export class MemberTileViewModel extends ViewModel { constructor(options) { super(options); this._member = this._options.member; + this._mediaRepository = options.mediaRepository this._previousName = null; this._nameChanged = true; } @@ -47,4 +49,20 @@ export class MemberTileViewModel extends ViewModel { this._updatePreviousName(newMember.name); this._member = newMember; } + + get avatarLetter() { + return avatarInitials(this.name); + } + + get avatarColorNumber() { + return getIdentifierColorNumber(this.userId) + } + + avatarUrl(size) { + return getAvatarHttpUrl(this._member.avatarUrl, size, this.platform, this._mediaRepository); + } + + get avatarTitle() { + return this.name; + } } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 3010f79c..f01f9491 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -39,8 +39,9 @@ export class RightPanelViewModel extends ViewModel { const list = await this._room.loadMemberList(); const members = list.members; const powerLevels = this._room.powerLevels; + const mediaRepository = this._room.mediaRepository; this._memberListViewModel = this.track( - new MemberListViewModel(this.childOptions({members, powerLevels})) + new MemberListViewModel(this.childOptions({members, powerLevels, mediaRepository})) ); } this.emitChange("memberListViewModel"); diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index 4e299ace..f2b5d499 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -1,7 +1,8 @@ import {TemplateView} from "../../general/TemplateView.js"; +import {AvatarView} from "../../avatar.js"; export class MemberTileView extends TemplateView { render(t, vm) { - return t.div(vm => vm.name); + return t.div([t.view(new AvatarView(vm, 32)), vm => vm.name]); } } From 3fb89a81bce1aca230421b89fa33e7cdd6c2ef93 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 26 Jun 2021 14:51:01 +0530 Subject: [PATCH 045/118] Add some initial styling Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 4 ++++ .../web/ui/css/themes/element/theme.css | 17 +++++++++++++++++ .../web/ui/session/rightpanel/MemberTileView.js | 5 ++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 8d6fd3b1..c083bd20 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -38,3 +38,7 @@ ul.MemberListView { overflow: scroll; height: 100vh; } + +.MemberTileView { + display: flex; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 885383d7..3108ca27 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -865,3 +865,20 @@ button.link { .RoomDetailsView .close { background-image: url("./icons/clear.svg"); } + +/* Memberlist Panel */ + +.MemberTileView { + margin-top: 8px; +} + +.MemberTileView .avatar { + margin-right: 8px; +} + +.MemberTileView_name { + text-overflow: ellipsis; + overflow: clip; + width: 184px; + white-space: nowrap; +} diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index f2b5d499..da0d5d23 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -3,6 +3,9 @@ import {AvatarView} from "../../avatar.js"; export class MemberTileView extends TemplateView { render(t, vm) { - return t.div([t.view(new AvatarView(vm, 32)), vm => vm.name]); + return t.div({ className: "MemberTileView" }, [ + t.view(new AvatarView(vm, 32)), + t.div({ className: "MemberTileView_name" }, (vm) => vm.name), + ]); } } From d64d07aee5d5ab12338845f897df4791f7abcc43 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Jun 2021 15:02:57 +0530 Subject: [PATCH 046/118] Move loading view into separate file Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/LoadingView.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/platform/web/ui/session/rightpanel/LoadingView.js diff --git a/src/platform/web/ui/session/rightpanel/LoadingView.js b/src/platform/web/ui/session/rightpanel/LoadingView.js new file mode 100644 index 00000000..eeafea70 --- /dev/null +++ b/src/platform/web/ui/session/rightpanel/LoadingView.js @@ -0,0 +1,8 @@ +import {TemplateView} from "../../general/TemplateView.js"; +import {spinner} from "../../common.js"; + +export class LoadingView extends TemplateView { + render(t) { + return t.div(["Loading ", spinner(t)]); + } +} From bcfd1bd1352e7789b9049eb328d350a4a9215362 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Jun 2021 15:05:35 +0530 Subject: [PATCH 047/118] Support loading view for all panels Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 2 ++ src/domain/session/rightpanel/RightPanelViewModel.js | 1 + .../session/rightpanel/RoomDetailsViewModel.js | 4 ++++ .../web/ui/session/rightpanel/MemberListView.js | 7 ------- .../web/ui/session/rightpanel/RightPanelView.js | 12 ++++++++---- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 114c61b7..0a6c4add 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -12,6 +12,8 @@ export class MemberListViewModel extends ViewModel { this.mediaRepository = options.mediaRepository; } + get type() { return "member-list"; } + _filterJoinedMembers(members) { return members.filterValues(member => member.membership === "join"); } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index f01f9491..0a04f8a1 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -11,6 +11,7 @@ export class RightPanelViewModel extends ViewModel { get roomDetailsViewModel() { return this._roomDetailsViewModel; } get memberListViewModel() { return this._memberListViewModel; } + get activeViewModel() { return this._roomDetailsViewModel ?? this._memberListViewModel; } _setupNavigation() { const details = this.navigation.observe("details"); diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index b9f05835..896fab54 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -9,6 +9,10 @@ export class RoomDetailsViewModel extends ViewModel { this._room.on("change", this._onRoomChange); } + get type() { + return "room-details"; + } + get roomId() { return this._room.id; } diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 9c4a847a..5b941a23 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -1,16 +1,9 @@ import {TemplateView} from "../../general/TemplateView.js"; import {ListView} from "../../general/ListView.js"; import {MemberTileView} from "./MemberTileView.js"; -import {spinner} from "../../common.js"; export class MemberListView extends TemplateView { render(t, vm) { return t.view(new ListView({list: vm.memberTileViewModels, className:"MemberListView"}, tileViewModel => new MemberTileView(tileViewModel))); } } - -export class MemberListLoadingView extends TemplateView { - render(t) { - return t.div(["Loading ", spinner(t)]); - } -} diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index aac92458..8f932004 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -1,13 +1,17 @@ import {TemplateView} from "../../general/TemplateView.js"; import {RoomDetailsView} from "./RoomDetailsView.js"; -import {MemberListLoadingView, MemberListView} from "./MemberListView.js"; +import {MemberListView} from "./MemberListView.js"; +import {LoadingView} from "./LoadingView.js"; export class RightPanelView extends TemplateView { render(t) { - return t.div({className: "RightPanelView"}, + const viewFromType = { + "room-details": RoomDetailsView, + "member-list": MemberListView + }; + return t.div({ className: "RightPanelView" }, [ - t.mapView(vm => vm.roomDetailsViewModel, roomDetailsViewModel => roomDetailsViewModel ? new RoomDetailsView(roomDetailsViewModel) : null), - t.mapView(vm => vm.memberListViewModel, memberListViewModel => memberListViewModel ? new MemberListView(memberListViewModel) : new MemberListLoadingView()) + t.mapView(vm => vm.activeViewModel, vm => vm ? new viewFromType[vm.type](vm) : new LoadingView()) ] ); } From e9354238142b52f29f2754d8a28ee811e17b8ca0 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 27 Jun 2021 18:10:50 +0530 Subject: [PATCH 048/118] Remove duplication in RightPanelViewModel --- .../session/rightpanel/RightPanelViewModel.js | 58 +++++++++---------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 0a04f8a1..cb32f5cd 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -9,42 +9,38 @@ export class RightPanelViewModel extends ViewModel { this._setupNavigation(); } - get roomDetailsViewModel() { return this._roomDetailsViewModel; } - get memberListViewModel() { return this._memberListViewModel; } - get activeViewModel() { return this._roomDetailsViewModel ?? this._memberListViewModel; } + get activeViewModel() { return this._activeViewModel; } + + async _memberArguments() { + const list = await this._room.loadMemberList(); + const room = this._room; + return {members: list.members, powerLevels: room.powerLevels, mediaRepository: room.mediaRepository}; + } _setupNavigation() { - const details = this.navigation.observe("details"); - this.track(details.subscribe(() => this._toggleRoomDetailsPanel())); - this._toggleRoomDetailsPanel(); - - const members = this.navigation.observe("members"); - this.track(members.subscribe(() => this._toggleMemberListPanel())); - this._toggleMemberListPanel(); + this._hookSegmentToToggler("details", RoomDetailsViewModel, () => { return {room: this._room}; }); + this._hookSegmentToToggler("members", MemberListViewModel, () => this._memberArguments()); } - _toggleRoomDetailsPanel() { - this._roomDetailsViewModel = this.disposeTracked(this._roomDetailsViewModel); - const enable = !!this.navigation.path.get("details")?.value; - if (enable) { - const room = this._room; - this._roomDetailsViewModel = this.track(new RoomDetailsViewModel(this.childOptions({room}))); - } - this.emitChange("roomDetailsViewModel"); + _hookSegmentToToggler(segment, viewmodel, argCreator) { + const observable = this.navigation.observe(segment); + const toggler = this._setupToggler(segment, viewmodel, argCreator); + this.track(observable.subscribe(() => toggler())); } - async _toggleMemberListPanel() { - this._memberListViewModel = this.disposeTracked(this._memberListViewModel); - const enable = !!this.navigation.path.get("members")?.value; - if (enable) { - const list = await this._room.loadMemberList(); - const members = list.members; - const powerLevels = this._room.powerLevels; - const mediaRepository = this._room.mediaRepository; - this._memberListViewModel = this.track( - new MemberListViewModel(this.childOptions({members, powerLevels, mediaRepository})) - ); - } - this.emitChange("memberListViewModel"); + _setupToggler(segment, viewmodel, argCreator) { + const toggler = async (skipDispose = false) => { + if (!skipDispose) { + this._activeViewModel = this.disposeTracked(this._activeViewModel); + } + const enable = !!this.navigation.path.get(segment)?.value; + if (enable) { + const args = await argCreator(); + this._activeViewModel = this.track(new viewmodel(this.childOptions(args))); + } + this.emitChange("activeViewModel"); + }; + toggler(true); + return toggler; } } From 3bb82e55bf5fe72e13646214544f6801e070b405 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 28 Jun 2021 18:33:12 +0530 Subject: [PATCH 049/118] Disambiguator is not async Signed-off-by: RMidhunSuresh --- .../session/rightpanel/disambiguator.js | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index e740cd49..1ca14703 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -17,7 +17,7 @@ export class Disambiguator { } } - async _handlePreviousName(vm) { + _handlePreviousName(vm) { const previousName = vm.previousName; if (!previousName) { return; } const value = this._map.get(previousName); @@ -29,7 +29,7 @@ export class Disambiguator { } } - async _updateMap(vm) { + _updateMap(vm) { const name = vm.name; if (this._map.has(name)) { const value = this._map.get(name); @@ -43,10 +43,10 @@ export class Disambiguator { } } - async disambiguate(vm) { + disambiguate(vm) { if (!vm.nameChanged) { return; } - await this._handlePreviousName(vm); - await this._updateMap(vm); + this._handlePreviousName(vm); + this._updateMap(vm); const value = this._map.get(vm.name); if (Array.isArray(value)) { value.forEach((vm) => vm.setDisambiguation(true)); @@ -87,60 +87,60 @@ export function tests(){ } return { - "Unique names": async assert => { + "Unique names": assert => { const [vm1, vm2, d] = createVmAndDisambiguator([["foo", "a"], ["bar", "b"]]); - await d.disambiguate(vm1); - await d.disambiguate(vm2); + d.disambiguate(vm1); + d.disambiguate(vm2); assert.strictEqual(vm1.disambiguate, false); assert.strictEqual(vm2.disambiguate, false); }, - "Same names are disambiguated": async assert => { + "Same names are disambiguated": assert => { const [vm1, vm2, vm3, d] = createVmAndDisambiguator([["foo", "a"], ["foo", "b"], ["foo", "c"]]); - await d.disambiguate(vm1); - await d.disambiguate(vm2); - await d.disambiguate(vm3); + d.disambiguate(vm1); + d.disambiguate(vm2); + d.disambiguate(vm3); assert.strictEqual(vm1.disambiguate, true); assert.strictEqual(vm2.disambiguate, true); assert.strictEqual(vm3.disambiguate, true); }, - "Name updates disambiguate": async assert => { + "Name updates disambiguate": assert => { const [vm1, vm2, vm3, d] = createVmAndDisambiguator([["foo", "a"], ["bar", "b"], ["jar", "c"]]); - await d.disambiguate(vm1); - await d.disambiguate(vm2); - await d.disambiguate(vm3); + d.disambiguate(vm1); + d.disambiguate(vm2); + d.disambiguate(vm3); vm2.updateName("foo"); - await d.disambiguate(vm2); + d.disambiguate(vm2); assert.strictEqual(vm1.disambiguate, true); assert.strictEqual(vm2.disambiguate, true); vm1.updateName("bar"); - await d.disambiguate(vm1); + d.disambiguate(vm1); assert.strictEqual(vm1.disambiguate, false); assert.strictEqual(vm2.disambiguate, false); vm3.updateName("foo"); - await d.disambiguate(vm3); + d.disambiguate(vm3); vm1.updateName("foo"); - await d.disambiguate(vm1); + d.disambiguate(vm1); assert.strictEqual(vm1.disambiguate, true); assert.strictEqual(vm2.disambiguate, true); assert.strictEqual(vm3.disambiguate, true); vm2.updateName("bar"); - await d.disambiguate(vm2); + d.disambiguate(vm2); assert.strictEqual(vm1.disambiguate, true); assert.strictEqual(vm2.disambiguate, false); assert.strictEqual(vm3.disambiguate, true); }, - "Multiple disambiguate events": async assert => { + "Multiple disambiguate events": assert => { const [vm1, d] = createVmAndDisambiguator([["foo", "a"]]); - await d.disambiguate(vm1); + d.disambiguate(vm1); vm1.updateName(vm1.name); - await d.disambiguate(vm1); + d.disambiguate(vm1); assert.strictEqual(vm1.disambiguate, false); }, }; From ab0a48ab31f3f7d8eb6f063a293680d0f96b2279 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 14:22:39 +0530 Subject: [PATCH 050/118] rename rightpanel to right-panel Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 20 +++++++++---------- src/domain/session/RoomGridViewModel.js | 2 +- src/domain/session/SessionViewModel.js | 4 ++-- .../session/leftpanel/LeftPanelViewModel.js | 2 +- src/domain/session/room/RoomViewModel.js | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 1e4a5941..677319a1 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -37,8 +37,8 @@ function allowsChild(parent, child) { // downside of the approach: both of these will control which tile is selected return type === "room" || type === "empty-grid-tile"; case "room": - return type === "lightbox" || type === "rightpanel"; - case "rightpanel": + return type === "lightbox" || type === "right-panel"; + case "right-panel": return type === "details"|| type === "members"; default: return false; @@ -116,7 +116,7 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { } segments.push(new Segment("room", roomId)); if (currentNavPath.get("details")?.value) { - segments.push(new Segment("rightpanel")); + segments.push(new Segment("right-panel")); segments.push(new Segment("details")); } } else if (type === "last-session") { @@ -128,11 +128,11 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { segments.push(sessionSegment); } } else if (type === "details") { - segments.push(new Segment("rightpanel")); + segments.push(new Segment("right-panel")); segments.push(new Segment("details")); } else if (type === "members") { //TODO: Fix duplication here. - segments.push(new Segment("rightpanel")); + segments.push(new Segment("right-panel")); segments.push(new Segment("members")); } else { // might be undefined, which will be turned into true by Segment @@ -162,7 +162,7 @@ export function stringifyPath(path) { urlPath += `/${segment.type}/${segment.value}`; } break; - case "rightpanel": + case "right-panel": continue; default: urlPath += `/${segment.type}`; @@ -197,13 +197,13 @@ export function tests() { const urlPath = stringifyPath(path); assert.equal(urlPath, "/session/1/rooms/a,b,c/1"); }, - "stringify url with rightpanel and details segment": assert => { + "stringify url with right-panel and details segment": assert => { const nav = new Navigation(allowsChild); const path = nav.pathFrom([ new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), new Segment("room", "b"), - new Segment("rightpanel"), + new Segment("right-panel"), new Segment("details") ]); const urlPath = stringifyPath(path); @@ -287,7 +287,7 @@ export function tests() { new Segment("session", 1), new Segment("rooms", ["a", "b", "c"]), new Segment("room", "b"), - new Segment("rightpanel", true), + new Segment("right-panel", true), new Segment("details", true) ]); const segments = parseUrlPath("/session/1/open-room/a", path); @@ -298,7 +298,7 @@ export function tests() { assert.deepEqual(segments[1].value, ["a", "b", "c"]); assert.equal(segments[2].type, "room"); assert.equal(segments[2].value, "a"); - assert.equal(segments[3].type, "rightpanel"); + assert.equal(segments[3].type, "right-panel"); assert.equal(segments[3].value, true); assert.equal(segments[4].type, "details"); assert.equal(segments[4].value, true); diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index c0aa5a52..e0f78b29 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -83,7 +83,7 @@ export class RoomGridViewModel extends ViewModel { let path = this.navigation.path.until("rooms"); path = path.with(this.navigation.segment("room", roomId)); if (detailsShown) { - path = path.with(this.navigation.segment("rightpanel", true)); + path = path.with(this.navigation.segment("right-panel", true)); path = path.with(this.navigation.segment("details", true)); } this.navigation.applyPath(path); diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index a66d66a0..83b7f1af 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -82,7 +82,7 @@ export class SessionViewModel extends ViewModel { this._updateLightbox(lightbox.get()); - const rightpanel = this.navigation.observe("rightpanel"); + const rightpanel = this.navigation.observe("right-panel"); this.track(rightpanel.subscribe(() => this._updateRightPanel())); this._updateRightPanel(); } @@ -260,7 +260,7 @@ export class SessionViewModel extends ViewModel { _updateRightPanel() { this._rightPanelViewModel = this.disposeTracked(this._rightPanelViewModel); - const enable = !!this.navigation.path.get("rightpanel")?.value; + const enable = !!this.navigation.path.get("right-panel")?.value; if (enable) { const room = this._roomFromNavigation(); this._rightPanelViewModel = this.track(new RightPanelViewModel(this.childOptions({room}))); diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 45f1f02b..0fdfcf36 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -96,7 +96,7 @@ export class LeftPanelViewModel extends ViewModel { let _path = path; const details = this.navigation.path.get("details"); if (details?.value) { - _path = _path.with(this.navigation.segment("rightpanel")); + _path = _path.with(this.navigation.segment("right-panel")); _path = _path.with(details) } return _path; diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 30dae699..3b04f83a 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -290,7 +290,7 @@ export class RoomViewModel extends ViewModel { openDetailsPanel() { let path = this.navigation.path.until("room"); - path = path.with(this.navigation.segment("rightpanel", true)); + path = path.with(this.navigation.segment("right-panel", true)); path = path.with(this.navigation.segment("details", true)); this.navigation.applyPath(path); } From 41806b5e825fc9c979befe83c6bd8474aeeb9989 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 14:37:41 +0530 Subject: [PATCH 051/118] Remove duplication Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 677319a1..c3d3f264 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -87,6 +87,11 @@ function roomsSegmentWithRoom(rooms, roomId, path) { } } +function pushRightPanelSegment(array, segment) { + array.push(new Segment("right-panel")); + array.push(new Segment(segment)); +} + export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { // substr(1) to take of initial / const parts = urlPath.substr(1).split("/"); @@ -116,8 +121,7 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { } segments.push(new Segment("room", roomId)); if (currentNavPath.get("details")?.value) { - segments.push(new Segment("right-panel")); - segments.push(new Segment("details")); + pushRightPanelSegment(segments, "details"); } } else if (type === "last-session") { let sessionSegment = currentNavPath.get("session"); @@ -127,13 +131,8 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { if (sessionSegment) { segments.push(sessionSegment); } - } else if (type === "details") { - segments.push(new Segment("right-panel")); - segments.push(new Segment("details")); - } else if (type === "members") { - //TODO: Fix duplication here. - segments.push(new Segment("right-panel")); - segments.push(new Segment("members")); + } else if (type === "details" || type === "members") { + pushRightPanelSegment(segments, type); } else { // might be undefined, which will be turned into true by Segment const value = iterator.next().value; From ad6122a5b635f46712abd05e557f757022085075 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 14:44:27 +0530 Subject: [PATCH 052/118] Add explaining comment Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index c3d3f264..836f24f1 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -162,6 +162,7 @@ export function stringifyPath(path) { } break; case "right-panel": + // Ignore right-panel in url continue; default: urlPath += `/${segment.type}`; From da1e981948f86bd56dda931986b207b7893d8840 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 30 Jun 2021 15:35:48 +0530 Subject: [PATCH 053/118] name changes Signed-off-by: RMidhunSuresh --- .../session/rightpanel/RightPanelViewModel.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index cb32f5cd..9486c042 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -11,25 +11,25 @@ export class RightPanelViewModel extends ViewModel { get activeViewModel() { return this._activeViewModel; } - async _memberArguments() { + async _getMemberArguments() { const list = await this._room.loadMemberList(); const room = this._room; return {members: list.members, powerLevels: room.powerLevels, mediaRepository: room.mediaRepository}; } _setupNavigation() { - this._hookSegmentToToggler("details", RoomDetailsViewModel, () => { return {room: this._room}; }); - this._hookSegmentToToggler("members", MemberListViewModel, () => this._memberArguments()); + this._hookUpdaterToSegment("details", RoomDetailsViewModel, () => { return {room: this._room}; }); + this._hookUpdaterToSegment("members", MemberListViewModel, () => this._getMemberArguments()); } - _hookSegmentToToggler(segment, viewmodel, argCreator) { + _hookUpdaterToSegment(segment, viewmodel, argCreator) { const observable = this.navigation.observe(segment); - const toggler = this._setupToggler(segment, viewmodel, argCreator); - this.track(observable.subscribe(() => toggler())); + const updater = this._setupUpdater(segment, viewmodel, argCreator); + this.track(observable.subscribe(() => updater())); } - _setupToggler(segment, viewmodel, argCreator) { - const toggler = async (skipDispose = false) => { + _setupUpdater(segment, viewmodel, argCreator) { + const updater = async (skipDispose = false) => { if (!skipDispose) { this._activeViewModel = this.disposeTracked(this._activeViewModel); } @@ -40,7 +40,7 @@ export class RightPanelViewModel extends ViewModel { } this.emitChange("activeViewModel"); }; - toggler(true); - return toggler; + updater(true); + return updater; } } From ea06d4f88e05ab964724e968a145337aa408ecb4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 2 Jul 2021 15:33:04 +0530 Subject: [PATCH 054/118] Eliminate double lookup Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/disambiguator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index 1ca14703..8be92137 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -31,8 +31,8 @@ export class Disambiguator { _updateMap(vm) { const name = vm.name; - if (this._map.has(name)) { - const value = this._map.get(name); + const value = this._map.get(name); + if (value) { if (Array.isArray(value)) { value.push(vm); } else { From 89e256e563be91160c76f9c8215e53ed43545c0b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 2 Jul 2021 15:40:27 +0530 Subject: [PATCH 055/118] Return array to prevent fetching again Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/disambiguator.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index 8be92137..153fc6c1 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -35,8 +35,11 @@ export class Disambiguator { if (value) { if (Array.isArray(value)) { value.push(vm); + return value; } else { - this._map.set(name, [value, vm]); + const array = [value, vm] + this._map.set(name, array); + return array; } } else { this._map.set(name, vm); @@ -46,11 +49,8 @@ export class Disambiguator { disambiguate(vm) { if (!vm.nameChanged) { return; } this._handlePreviousName(vm); - this._updateMap(vm); - const value = this._map.get(vm.name); - if (Array.isArray(value)) { - value.forEach((vm) => vm.setDisambiguation(true)); - } + const value = this._updateMap(vm); + value?.forEach((vm) => vm.setDisambiguation(true)); } } From 11d411c9a91947e270c72b6b7e51714fc69a003d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 2 Jul 2021 15:59:29 +0530 Subject: [PATCH 056/118] Add failing test For empty names not un-disambiguating Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/disambiguator.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index 153fc6c1..44acc733 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -143,5 +143,15 @@ export function tests(){ d.disambiguate(vm1); assert.strictEqual(vm1.disambiguate, false); }, + + "Empty names must un-disambiguate": assert => { + const [vm1, vm2, d] = createVmAndDisambiguator([["", "a"], ["", "b"]]); + d.disambiguate(vm1); + d.disambiguate(vm2); + vm1.updateName("foo"); + d.disambiguate(vm1); + assert.strictEqual(vm1.disambiguate, false); + assert.strictEqual(vm2.disambiguate, false); + } }; } From 53fc6a7af9caeebf4dc8f363d05200f6b6ec5793 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 2 Jul 2021 16:00:38 +0530 Subject: [PATCH 057/118] Check prev name is string Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/disambiguator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index 44acc733..a387a790 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -19,7 +19,7 @@ export class Disambiguator { _handlePreviousName(vm) { const previousName = vm.previousName; - if (!previousName) { return; } + if (typeof previousName !== "string") { return; } const value = this._map.get(previousName); if (Array.isArray(value)) { this._unDisambiguate(vm, value); From db515d48c1d6b1e725f931029f210967eb280ab3 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 2 Jul 2021 16:12:10 +0530 Subject: [PATCH 058/118] Inline flatten method Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/disambiguator.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/disambiguator.js index a387a790..f3fa551b 100644 --- a/src/domain/session/rightpanel/disambiguator.js +++ b/src/domain/session/rightpanel/disambiguator.js @@ -3,12 +3,6 @@ export class Disambiguator { this._map = new Map(); } - _flatten(name, array) { - const vm = array.pop(); - vm.setDisambiguation(false); - this._map.set(name, vm); - } - _unDisambiguate(vm, array) { const idx = array.indexOf(vm); if (idx !== -1) { @@ -23,7 +17,11 @@ export class Disambiguator { const value = this._map.get(previousName); if (Array.isArray(value)) { this._unDisambiguate(vm, value); - if (value.length === 1) { this._flatten(previousName, value); } + if (value.length === 1) { + const vm = value[0]; + vm.setDisambiguation(false); + this._map.set(previousName, vm); + } } else { this._map.delete(previousName); } From 35f6043d06e2aedc3c91721cae097fae1fd7ad7b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 3 Jul 2021 21:18:28 +0530 Subject: [PATCH 059/118] Support slice Signed-off-by: RMidhunSuresh --- src/observable/list/SortedMapList.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index babf5c35..93bf415a 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -119,6 +119,10 @@ export class SortedMapList extends BaseObservableList { return this._sourceMap.size; } + slice(start, end) { + return this._sortedPairs.slice(start, end); + } + [Symbol.iterator]() { const it = this._sortedPairs.values(); return { From 1d5b16395fb0fee466724c13788a26af07af9342 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 3 Jul 2021 21:18:55 +0530 Subject: [PATCH 060/118] Export function Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/ListView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/ListView.js b/src/platform/web/ui/general/ListView.js index 2e29996c..884eedc4 100644 --- a/src/platform/web/ui/general/ListView.js +++ b/src/platform/web/ui/general/ListView.js @@ -17,7 +17,7 @@ limitations under the License. import {el} from "./html.js"; import {mountView} from "./utils.js"; -function insertAt(parentNode, idx, childNode) { +export function insertAt(parentNode, idx, childNode) { const isLast = idx === parentNode.childElementCount; if (isLast) { parentNode.appendChild(childNode); From 0b9f4a5e1d8b4e282f063567163cbe54afdf856a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 3 Jul 2021 21:19:21 +0530 Subject: [PATCH 061/118] Add LazyListView Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 128 ++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/platform/web/ui/general/LazyListView.js diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js new file mode 100644 index 00000000..1ea10d70 --- /dev/null +++ b/src/platform/web/ui/general/LazyListView.js @@ -0,0 +1,128 @@ +import {mountView} from "./utils.js"; +import {insertAt, ListView} from "./ListView.js"; + +class Range { + constructor(start = 0, end = 0) { + this.start = start; + this.end = end; + this._expanded = false; + } + + _onInitialExpand() { + if (this._expanded) { return; } + this._initialStart = this.start; + this._expanded = true; + } + + expandFromEnd(units) { + this._onInitialExpand(); + this.start = this.end; + this.end += units; + this._expanded = true; + } + + contains(idx) { + const start = this._expanded ? this._initialStart : this.start; + return idx >= start && idx <= this.end; + } +} + +export class LazyListView extends ListView { + constructor({itemHeight, height, appendCount = 5, ...options}, childCreator) { + super(options, childCreator); + this._itemHeight = itemHeight; + this._height = height; + this._appendCount = appendCount; + this._range = new Range(); + } + + _isFullyScrolled() { + return this._root.scrollHeight - Math.abs(this._root.scrollTop) === this._root.clientHeight; + } + + _renderMoreIfNeeded() { + if (!this._isFullyScrolled()) { + return; + } + this._range.expandFromEnd(this._appendCount); + this._renderElementsInRange(); + } + + _renderElementsInRange() { + const items = this._list.slice(this._range.start, this._range.end); + const fragment = document.createDocumentFragment(); + for (const item of items) { + const view = this._childCreator(item.value); + this._childInstances.push(view); + fragment.appendChild(mountView(view, this._mountArgs)); + } + this._root.appendChild(fragment); + } + + _calculateInitialRenderCount() { + return Math.ceil(this._height / this._itemHeight); + } + + loadList() { + if (!this._list) { + return; + } + this._subscription = this._list.subscribe(this); + this._range.end = this._calculateInitialRenderCount() + this._appendCount; + this._childInstances = []; + this._renderElementsInRange(); + /* + Hooking to scroll events can be expensive. + But in most of these scroll events, we return early. + Do we need to do more (like event throttling)? + */ + this._root.addEventListener("scroll", () => this._renderMoreIfNeeded()); + } + + // onAdd, onRemove, ... should be called only if the element is already rendered + onAdd(idx, value) { + if (this._range.contains(idx)) { + super.onAdd(idx, value); + } + } + + onRemove(idx, value) { + if (this._range.contains(idx)) { + super.onRemove(idx, value); + } + } + + onUpdate(idx, value, params) { + if (this._range.contains(idx)) { + super.onUpdate(idx, value, params); + } + } + + recreateItem(idx, value) { + if (this._range.contains(idx)) { + super.recreateItem(idx, value) + } + } + + onMove(fromIdx, toIdx, value) { + const fromInRange = this._range.contains(fromIdx); + const toInRange = this._range.contains(toIdx); + if (fromInRange && toInRange) { + super.onMove(fromIdx, toIdx, value); + } + else if (fromInRange && !toInRange) { + this.onBeforeListChanged(); + const [child] = this._childInstances.splice(fromIdx, 1); + child.root().remove(); + this.onListChanged(); + } + else if (!fromInRange && toInRange) { + this.onBeforeListChanged(); + const child = this._childCreator(value); + this._childInstances.splice(toIdx, 0, child); + insertAt(this._root, toIdx, mountView(child, this._mountArgs)); + this.onListChanged(); + } + } + +} From ee072343f5d9f8fa7d9d2d4d9ec2ed7eda9a0c0d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sat, 3 Jul 2021 21:19:41 +0530 Subject: [PATCH 062/118] Switch to lazy list Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberListView.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 5b941a23..318516cd 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -1,9 +1,14 @@ import {TemplateView} from "../../general/TemplateView.js"; -import {ListView} from "../../general/ListView.js"; +import {LazyListView} from "../../general/LazyListView.js"; import {MemberTileView} from "./MemberTileView.js"; export class MemberListView extends TemplateView { render(t, vm) { - return t.view(new ListView({list: vm.memberTileViewModels, className:"MemberListView"}, tileViewModel => new MemberTileView(tileViewModel))); + return t.view(new LazyListView({ + list: vm.memberTileViewModels, + className: "MemberListView", + height: 691, + itemHeight: 32 + }, tileViewModel => new MemberTileView(tileViewModel))); } } From 452eee6767e46520a2cb8c31fcfcc7ce09d5c867 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Sun, 11 Jul 2021 18:12:27 +0530 Subject: [PATCH 063/118] Incorporate lazyrender code from element Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 12 +- src/platform/web/ui/general/LazyListView.js | 147 +++++++++++++------- 2 files changed, 103 insertions(+), 56 deletions(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index c083bd20..aa568068 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -1,5 +1,12 @@ .RightPanelView{ grid-area: right; + min-height: 0; +} + +.LazyListParent { + overflow:scroll; + overflow-x:hidden; + height: 100vh; } .RoomDetailsView { @@ -34,11 +41,6 @@ width: 100%; } -ul.MemberListView { - overflow: scroll; - height: 100vh; -} - .MemberTileView { display: flex; } diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 1ea10d70..53c7b12f 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -1,55 +1,88 @@ +import {el} from "./html.js"; import {mountView} from "./utils.js"; import {insertAt, ListView} from "./ListView.js"; -class Range { - constructor(start = 0, end = 0) { - this.start = start; - this.end = end; - this._expanded = false; +class ItemRange { + constructor(topCount, renderCount, bottomCount) { + this.topCount = topCount; + this.renderCount = renderCount; + this.bottomCount = bottomCount; } - _onInitialExpand() { - if (this._expanded) { return; } - this._initialStart = this.start; - this._expanded = true; + contains(range) { + // don't contain empty ranges + // as it will prevent clearing the list + // once it is scrolled far enough out of view + if (!range.renderCount && this.renderCount) { + return false; + } + return range.topCount >= this.topCount && + (range.topCount + range.renderCount) <= (this.topCount + this.renderCount); } - expandFromEnd(units) { - this._onInitialExpand(); - this.start = this.end; - this.end += units; - this._expanded = true; + containsIndex(idx) { + return idx >= this.topCount && idx <= (this.topCount + this.renderCount); } - contains(idx) { - const start = this._expanded ? this._initialStart : this.start; - return idx >= start && idx <= this.end; + expand(amount) { + // don't expand ranges that won't render anything + if (this.renderCount === 0) { + return this; + } + + const topGrow = Math.min(amount, this.topCount); + const bottomGrow = Math.min(amount, this.bottomCount); + return new ItemRange( + this.topCount - topGrow, + this.renderCount + topGrow + bottomGrow, + this.bottomCount - bottomGrow, + ); + } + + totalSize() { + return this.topCount + this.renderCount + this.bottomCount; } } export class LazyListView extends ListView { - constructor({itemHeight, height, appendCount = 5, ...options}, childCreator) { + constructor({itemHeight, height, ...options}, childCreator) { super(options, childCreator); this._itemHeight = itemHeight; this._height = height; - this._appendCount = appendCount; - this._range = new Range(); + this._overflowMargin = 5; + this._overflowItems = 20; } - _isFullyScrolled() { - return this._root.scrollHeight - Math.abs(this._root.scrollTop) === this._root.clientHeight; + _getVisibleRange() { + const length = this._list ? this._list.length : 0; + const scrollTop = this._parent.scrollTop; + const topCount = Math.min(Math.max(0, Math.floor(scrollTop / this._itemHeight)), length); + const itemsAfterTop = length - topCount; + const visibleItems = this._height !== 0 ? Math.ceil(this._height / this._itemHeight) : 0; + const renderCount = Math.min(visibleItems, itemsAfterTop); + const bottomCount = itemsAfterTop - renderCount; + return new ItemRange(topCount, renderCount, bottomCount); } _renderMoreIfNeeded() { - if (!this._isFullyScrolled()) { - return; + const range = this._getVisibleRange(); + const intersectRange = range.expand(this._overflowMargin); + const renderRange = range.expand(this._overflowItems); + const listHasChangedSize = !!this._renderRange && this._list.length !== this._renderRange.totalSize(); + console.log("currentRange", range); + console.log("renderRange", renderRange); + console.log("intersectRange", intersectRange); + console.log("LastRenderedRange", this._renderRange); + // only update render Range if the list has shrunk/grown and we need to adjust padding OR + // if the new range + overflowMargin isn't contained by the old anymore + if (listHasChangedSize || !this._renderRange || !this._renderRange.contains(intersectRange)) { + console.log("New render change"); + this._renderRange = renderRange; + this._renderElementsInRange(); } - this._range.expandFromEnd(this._appendCount); - this._renderElementsInRange(); } - _renderElementsInRange() { - const items = this._list.slice(this._range.start, this._range.end); + _renderItems(items) { const fragment = document.createDocumentFragment(); for (const item of items) { const view = this._childCreator(item.value); @@ -59,54 +92,66 @@ export class LazyListView extends ListView { this._root.appendChild(fragment); } - _calculateInitialRenderCount() { - return Math.ceil(this._height / this._itemHeight); + _renderElementsInRange() { + const { topCount, renderCount, bottomCount } = this._renderRange; + const paddingTop = topCount * this._itemHeight; + const paddingBottom = bottomCount * this._itemHeight; + const renderedItems = (this._list || []).slice( + topCount, + topCount + renderCount, + ); + this._root.style.paddingTop = `${paddingTop}px`; + this._root.style.paddingBottom = `${paddingBottom}px`; + this._root.innerHTML = ""; + this._renderItems(renderedItems); } - loadList() { - if (!this._list) { - return; - } - this._subscription = this._list.subscribe(this); - this._range.end = this._calculateInitialRenderCount() + this._appendCount; - this._childInstances = []; - this._renderElementsInRange(); + mount() { + const root = super.mount(); + this._parent = el("div", {className: "LazyListParent"}, root); /* Hooking to scroll events can be expensive. But in most of these scroll events, we return early. Do we need to do more (like event throttling)? */ - this._root.addEventListener("scroll", () => this._renderMoreIfNeeded()); + this._parent.addEventListener("scroll", () => this._renderMoreIfNeeded()); + this._renderMoreIfNeeded(); + return this._parent; + } + + loadList() { + if (!this._list) { return; } + this._subscription = this._list.subscribe(this); + this._childInstances = []; } // onAdd, onRemove, ... should be called only if the element is already rendered - onAdd(idx, value) { - if (this._range.contains(idx)) { - super.onAdd(idx, value); - } + onAdd() { + this._renderMoreIfNeeded(); } - onRemove(idx, value) { - if (this._range.contains(idx)) { - super.onRemove(idx, value); - } + onRemove() { + this._renderMoreIfNeeded(); } onUpdate(idx, value, params) { - if (this._range.contains(idx)) { + console.log("onUpdate"); + if (this._renderRange.containsIndex(idx)) { super.onUpdate(idx, value, params); } } recreateItem(idx, value) { - if (this._range.contains(idx)) { + console.log("recreateItem"); + if (this._renderRange.containsIndex(idx)) { super.recreateItem(idx, value) } } onMove(fromIdx, toIdx, value) { - const fromInRange = this._range.contains(fromIdx); - const toInRange = this._range.contains(toIdx); + console.log("onMove"); + const fromInRange = this._renderRange.containsIndex(fromIdx); + const toInRange = this._renderRange.containsIndex(toIdx); if (fromInRange && toInRange) { super.onMove(fromIdx, toIdx, value); } From f05574f579393f81848ba8c621700ae3371b4e8d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 13:57:37 +0530 Subject: [PATCH 064/118] Fix updates Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 65 ++++++++++++++++----- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 53c7b12f..6ad0f057 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -42,6 +42,10 @@ class ItemRange { totalSize() { return this.topCount + this.renderCount + this.bottomCount; } + + normalize(idx) { + return idx - this.topCount; + } } export class LazyListView extends ListView { @@ -64,7 +68,7 @@ export class LazyListView extends ListView { return new ItemRange(topCount, renderCount, bottomCount); } - _renderMoreIfNeeded() { + _renderIfNeeded() { const range = this._getVisibleRange(); const intersectRange = range.expand(this._overflowMargin); const renderRange = range.expand(this._overflowItems); @@ -83,6 +87,7 @@ export class LazyListView extends ListView { } _renderItems(items) { + this._childInstances = []; const fragment = document.createDocumentFragment(); for (const item of items) { const view = this._childCreator(item.value); @@ -114,8 +119,8 @@ export class LazyListView extends ListView { But in most of these scroll events, we return early. Do we need to do more (like event throttling)? */ - this._parent.addEventListener("scroll", () => this._renderMoreIfNeeded()); - this._renderMoreIfNeeded(); + this._parent.addEventListener("scroll", () => this._renderIfNeeded()); + this._renderIfNeeded(); return this._parent; } @@ -123,49 +128,81 @@ export class LazyListView extends ListView { if (!this._list) { return; } this._subscription = this._list.subscribe(this); this._childInstances = []; + // todo: this breaks update in parent } - // onAdd, onRemove, ... should be called only if the element is already rendered + + // If size of the list changes, re-render onAdd() { - this._renderMoreIfNeeded(); + this._renderIfNeeded(); } onRemove() { - this._renderMoreIfNeeded(); + this._renderIfNeeded(); } onUpdate(idx, value, params) { - console.log("onUpdate"); if (this._renderRange.containsIndex(idx)) { - super.onUpdate(idx, value, params); + const normalizedIdx = this._renderRange.normalize(idx); + super.onUpdate(normalizedIdx, value, params); } } recreateItem(idx, value) { - console.log("recreateItem"); if (this._renderRange.containsIndex(idx)) { super.recreateItem(idx, value) } } + _renderAdditionalElement(fromIdx, toIdx) { + const {topCount, renderCount} = this._renderRange; + const childFromIndex = index => this._childCreator(this._list.get(index)); + if (toIdx < fromIdx) { + // Element is moved up the list, so render element from top boundary + const index = topCount; + const child = childFromIndex(index); + // Modify childInstances here + this._root.insertBefore(mountView(child, this._mountArgs), this._root.firstChild); + } + else { + // Element is moved down the list, so render element from bottom boundary + const index = topCount + renderCount - 1; + const child = childFromIndex(index); + this._root.appendChild(mountView(child, this._mountArgs)); + } + } + + _removeAdditionalElement(fromIdx, toIdx) { + if (toIdx < fromIdx) { + // Element comes from the bottom, so remove element at bottom + this._root.lastChild.remove(); + } + else { + this._root.firstChild.remove(); + } + } + onMove(fromIdx, toIdx, value) { - console.log("onMove"); const fromInRange = this._renderRange.containsIndex(fromIdx); const toInRange = this._renderRange.containsIndex(toIdx); + const normalizedFromIdx = this._renderRange.normalize(fromIdx); + const normalizedToIdx = this._renderRange.normalize(toIdx); if (fromInRange && toInRange) { - super.onMove(fromIdx, toIdx, value); + super.onMove(normalizedFromIdx, normalizedToIdx, value); } else if (fromInRange && !toInRange) { this.onBeforeListChanged(); - const [child] = this._childInstances.splice(fromIdx, 1); + const [child] = this._childInstances.splice(normalizedFromIdx, 1); child.root().remove(); + this._renderAdditionalElement(fromIdx, toIdx); this.onListChanged(); } else if (!fromInRange && toInRange) { this.onBeforeListChanged(); const child = this._childCreator(value); - this._childInstances.splice(toIdx, 0, child); - insertAt(this._root, toIdx, mountView(child, this._mountArgs)); + this._childInstances.splice(normalizedToIdx, 0, child); + this._removeAdditionalElement(fromIdx, toIdx); + insertAt(this._root, normalizedToIdx, mountView(child, this._mountArgs)); this.onListChanged(); } } From 96e2bb0b1a6fccc841873b81ddb825305f8c1701 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 14:44:55 +0530 Subject: [PATCH 065/118] Add explaining comment Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 6ad0f057..cd31bb61 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -44,6 +44,10 @@ class ItemRange { } normalize(idx) { + /* + Since we don't render the entire list, the index we use to do dom + manipulation or access childInstances will be different. + */ return idx - this.topCount; } } @@ -73,14 +77,15 @@ export class LazyListView extends ListView { const intersectRange = range.expand(this._overflowMargin); const renderRange = range.expand(this._overflowItems); const listHasChangedSize = !!this._renderRange && this._list.length !== this._renderRange.totalSize(); - console.log("currentRange", range); - console.log("renderRange", renderRange); - console.log("intersectRange", intersectRange); - console.log("LastRenderedRange", this._renderRange); + // console.log("currentRange", range); + // console.log("renderRange", renderRange); + // console.log("intersectRange", intersectRange); + // console.log("LastRenderedRange", this._renderRange); // only update render Range if the list has shrunk/grown and we need to adjust padding OR // if the new range + overflowMargin isn't contained by the old anymore if (listHasChangedSize || !this._renderRange || !this._renderRange.contains(intersectRange)) { console.log("New render change"); + console.log("scrollTop", this._parent.scrollTop); this._renderRange = renderRange; this._renderElementsInRange(); } From 9a00143623ed30c84033eb9d7df9cc41a2fd57d9 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 14:49:01 +0530 Subject: [PATCH 066/118] Improve comment Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index cd31bb61..4d8163f5 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -45,8 +45,10 @@ class ItemRange { normalize(idx) { /* - Since we don't render the entire list, the index we use to do dom - manipulation or access childInstances will be different. + map index from list to index in rendered range + eg: if the index range of this._list is [0, 200] and we have rendered + elements in range [50, 60] then index 50 in list must map to index 0 + in DOM tree/childInstance array. */ return idx - this.topCount; } From 4cb9adc9ea7d0a949c58856d7e34958bad96f780 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 14:49:32 +0530 Subject: [PATCH 067/118] Remove misleading comment Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 4d8163f5..90782648 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -123,7 +123,6 @@ export class LazyListView extends ListView { this._parent = el("div", {className: "LazyListParent"}, root); /* Hooking to scroll events can be expensive. - But in most of these scroll events, we return early. Do we need to do more (like event throttling)? */ this._parent.addEventListener("scroll", () => this._renderIfNeeded()); From 85924aba92a428f53437fdc36fbcee5d59068f03 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 14:54:28 +0530 Subject: [PATCH 068/118] Fix update method Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 90782648..cfd57f1e 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -130,11 +130,16 @@ export class LazyListView extends ListView { return this._parent; } + update(attributes) { + this._renderRange = null; + super.update(attributes); + this._renderIfNeeded(); + } + loadList() { if (!this._list) { return; } this._subscription = this._list.subscribe(this); this._childInstances = []; - // todo: this breaks update in parent } From 5338457ea06cab4ea79feb607635c0571d983861 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 15:07:18 +0530 Subject: [PATCH 069/118] Use normalized index in recreateItem Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index cfd57f1e..92ab1ee6 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -140,6 +140,10 @@ export class LazyListView extends ListView { if (!this._list) { return; } this._subscription = this._list.subscribe(this); this._childInstances = []; + /* + super.loadList() would render the entire list at this point. + We instead lazy render a part of the list in _renderIfNeeded + */ } @@ -161,7 +165,8 @@ export class LazyListView extends ListView { recreateItem(idx, value) { if (this._renderRange.containsIndex(idx)) { - super.recreateItem(idx, value) + const normalizedIdx = this._renderRange.normalize(idx); + super.recreateItem(normalizedIdx, value) } } From d3a8e959622ac6ac047d97d0a6ab97da44ec03d5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Mon, 12 Jul 2021 20:41:33 +0530 Subject: [PATCH 070/118] Find height of container from DOM Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 37 +++++++++++++------ .../ui/session/rightpanel/MemberListView.js | 1 - 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 92ab1ee6..e623eb07 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -55,12 +55,11 @@ class ItemRange { } export class LazyListView extends ListView { - constructor({itemHeight, height, ...options}, childCreator) { + constructor({itemHeight, overflowMargin = 5, overflowItems = 20,...options}, childCreator) { super(options, childCreator); this._itemHeight = itemHeight; - this._height = height; - this._overflowMargin = 5; - this._overflowItems = 20; + this._overflowMargin = overflowMargin; + this._overflowItems = overflowItems; } _getVisibleRange() { @@ -83,16 +82,32 @@ export class LazyListView extends ListView { // console.log("renderRange", renderRange); // console.log("intersectRange", intersectRange); // console.log("LastRenderedRange", this._renderRange); - // only update render Range if the list has shrunk/grown and we need to adjust padding OR - // if the new range + overflowMargin isn't contained by the old anymore - if (listHasChangedSize || !this._renderRange || !this._renderRange.contains(intersectRange)) { - console.log("New render change"); - console.log("scrollTop", this._parent.scrollTop); + // only update render Range if the new range + overflowMargin isn't contained by the old anymore + if (listHasChangedSize || !this._renderRange.contains(intersectRange)) { + // console.log("New render change"); + // console.log("scrollTop", this._parent.scrollTop); this._renderRange = renderRange; this._renderElementsInRange(); } } + async _initialRender() { + /* + Wait two frames for the return from mount() to be inserted into DOM. + This should be enough, but if this gives us trouble we can always use + MutationObserver. + */ + await new Promise(r => requestAnimationFrame(r)); + await new Promise(r => requestAnimationFrame(r)); + + this._height = this._parent.clientHeight; + if(this._height === 0) { throw "LazyListView could not calculate parent height."} + const range = this._getVisibleRange(); + const renderRange = range.expand(this._overflowItems); + this._renderRange = renderRange; + this._renderElementsInRange(); + } + _renderItems(items) { this._childInstances = []; const fragment = document.createDocumentFragment(); @@ -126,14 +141,14 @@ export class LazyListView extends ListView { Do we need to do more (like event throttling)? */ this._parent.addEventListener("scroll", () => this._renderIfNeeded()); - this._renderIfNeeded(); + this._initialRender(); return this._parent; } update(attributes) { this._renderRange = null; super.update(attributes); - this._renderIfNeeded(); + this._initialRender(); } loadList() { diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 318516cd..5bd4e86f 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -7,7 +7,6 @@ export class MemberListView extends TemplateView { return t.view(new LazyListView({ list: vm.memberTileViewModels, className: "MemberListView", - height: 691, itemHeight: 32 }, tileViewModel => new MemberTileView(tileViewModel))); } From ea0851eb9498f466490e1e13d7a1702bde1eb58e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jul 2021 13:05:54 +0530 Subject: [PATCH 071/118] Keep memberlist panel open on room/grid change Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 12 ++++++++++++ src/domain/session/RoomGridViewModel.js | 8 +++----- src/domain/session/leftpanel/LeftPanelViewModel.js | 14 ++++++-------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 836f24f1..2f039eef 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -92,6 +92,16 @@ function pushRightPanelSegment(array, segment) { array.push(new Segment(segment)); } +export function addPanelIfNeeded(navigation, panel, path) { + const segment = navigation.path.get(panel); + let _path = path; + if (segment?.value) { + _path = _path.with(navigation.segment("right-panel")); + _path = _path.with(segment) + } + return _path; +} + export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { // substr(1) to take of initial / const parts = urlPath.substr(1).split("/"); @@ -122,6 +132,8 @@ export function parseUrlPath(urlPath, currentNavPath, defaultSessionId) { segments.push(new Segment("room", roomId)); if (currentNavPath.get("details")?.value) { pushRightPanelSegment(segments, "details"); + } else if (currentNavPath.get("members")?.value) { + pushRightPanelSegment(segments, "members"); } } else if (type === "last-session") { let sessionSegment = currentNavPath.get("session"); diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index e0f78b29..a193d48b 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -15,6 +15,7 @@ limitations under the License. */ import {ViewModel} from "../ViewModel.js"; +import {addPanelIfNeeded} from "../navigation/index.js"; function dedupeSparse(roomIds) { return roomIds.map((id, idx) => { @@ -79,13 +80,10 @@ export class RoomGridViewModel extends ViewModel { } _switchToRoom(roomId) { - const detailsShown = !!this.navigation.path.get("details")?.value; let path = this.navigation.path.until("rooms"); path = path.with(this.navigation.segment("room", roomId)); - if (detailsShown) { - path = path.with(this.navigation.segment("right-panel", true)); - path = path.with(this.navigation.segment("details", true)); - } + path = addPanelIfNeeded(this.navigation, "details", path); + path = addPanelIfNeeded(this.navigation, "members", path); this.navigation.applyPath(path); } diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 0fdfcf36..549adf6a 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -20,6 +20,7 @@ import {RoomTileViewModel} from "./RoomTileViewModel.js"; import {InviteTileViewModel} from "./InviteTileViewModel.js"; import {RoomFilter} from "./RoomFilter.js"; import {ApplyMap} from "../../../observable/map/ApplyMap.js"; +import {addPanelIfNeeded} from "../../navigation/index.js"; export class LeftPanelViewModel extends ViewModel { constructor(options) { @@ -92,13 +93,10 @@ export class LeftPanelViewModel extends ViewModel { } } - _pathForDetails(path) { + _pathForRightPanel(path) { let _path = path; - const details = this.navigation.path.get("details"); - if (details?.value) { - _path = _path.with(this.navigation.segment("right-panel")); - _path = _path.with(details) - } + _path = addPanelIfNeeded(this.navigation, "details", _path); + _path = addPanelIfNeeded(this.navigation, "members", _path); return _path; } @@ -108,13 +106,13 @@ export class LeftPanelViewModel extends ViewModel { if (this.gridEnabled) { if (room) { path = path.with(room); - path = this._pathForDetails(path); + path = this._pathForRightPanel(path); } } else { if (room) { path = path.with(this.navigation.segment("rooms", [room.value])); path = path.with(room); - path = this._pathForDetails(path); + path = this._pathForRightPanel(path); } else { path = path.with(this.navigation.segment("rooms", [])); path = path.with(this.navigation.segment("empty-grid-tile", 0)); From f506cf615bd98bb63f016674ed26f522f4368330 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jul 2021 17:39:17 +0530 Subject: [PATCH 072/118] div --> li Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberTileView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index da0d5d23..bd90e4d3 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -3,7 +3,7 @@ import {AvatarView} from "../../avatar.js"; export class MemberTileView extends TemplateView { render(t, vm) { - return t.div({ className: "MemberTileView" }, [ + return t.li({ className: "MemberTileView" }, [ t.view(new AvatarView(vm, 32)), t.div({ className: "MemberTileView_name" }, (vm) => vm.name), ]); From c539c386996c43c60825d65d58a0e5873f00ae9c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jul 2021 17:49:43 +0530 Subject: [PATCH 073/118] Account for padding in itemHeight Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberListView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 5bd4e86f..5667c20e 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -7,7 +7,7 @@ export class MemberListView extends TemplateView { return t.view(new LazyListView({ list: vm.memberTileViewModels, className: "MemberListView", - itemHeight: 32 + itemHeight: 40 }, tileViewModel => new MemberTileView(tileViewModel))); } } From d1f465e6ccd39e73a98f5d27ef63c5a92cf1e028 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Tue, 13 Jul 2021 18:13:45 +0530 Subject: [PATCH 074/118] Replace slice with iterator Signed-off-by: RMidhunSuresh --- src/observable/list/SortedMapList.js | 4 ---- src/platform/web/ui/general/LazyListView.js | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index 93bf415a..babf5c35 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -119,10 +119,6 @@ export class SortedMapList extends BaseObservableList { return this._sourceMap.size; } - slice(start, end) { - return this._sortedPairs.slice(start, end); - } - [Symbol.iterator]() { const it = this._sortedPairs.values(); return { diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index e623eb07..e5d4b33b 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -112,21 +112,30 @@ export class LazyListView extends ListView { this._childInstances = []; const fragment = document.createDocumentFragment(); for (const item of items) { - const view = this._childCreator(item.value); + const view = this._childCreator(item); this._childInstances.push(view); fragment.appendChild(mountView(view, this._mountArgs)); } this._root.appendChild(fragment); } + _itemsFromList(start, end) { + const array = []; + let i = 0; + for (const item of this._list) { + if (i >= start && i < end) { + array.push(item); + } + i = i + 1; + } + return array; + } _renderElementsInRange() { const { topCount, renderCount, bottomCount } = this._renderRange; const paddingTop = topCount * this._itemHeight; const paddingBottom = bottomCount * this._itemHeight; - const renderedItems = (this._list || []).slice( - topCount, - topCount + renderCount, - ); + const renderedItems = this._itemsFromList(topCount, topCount + renderCount); + console.log(renderedItems); this._root.style.paddingTop = `${paddingTop}px`; this._root.style.paddingBottom = `${paddingBottom}px`; this._root.innerHTML = ""; From 8a976ef24b3444b0ce2c73fac84bb538eecea588 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 12:59:44 +0530 Subject: [PATCH 075/118] Make powerLevels observable Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 4 +- .../session/rightpanel/RightPanelViewModel.js | 3 +- src/matrix/room/BaseRoom.js | 44 ++++++++++++++++++- src/matrix/room/timeline/Timeline.js | 29 ++---------- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 0a6c4add..bc54ad3e 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -6,8 +6,10 @@ import {Disambiguator} from "./disambiguator.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); + const powerLevels = options.powerLevelsObservable.get(); + // We should subscribe to the observable here so that we can resort when pl changes this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) - .sortValues(createMemberComparator(options.powerLevels)); + .sortValues(createMemberComparator(powerLevels)); this.nameDisambiguator = new Disambiguator(); this.mediaRepository = options.mediaRepository; } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 9486c042..29755e8d 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -14,7 +14,8 @@ export class RightPanelViewModel extends ViewModel { async _getMemberArguments() { const list = await this._room.loadMemberList(); const room = this._room; - return {members: list.members, powerLevels: room.powerLevels, mediaRepository: room.mediaRepository}; + const powerLevelsObservable = await this._room.observePowerLevels(); + return {members: list.members, powerLevelsObservable, mediaRepository: room.mediaRepository}; } _setupNavigation() { diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index ac5fa59e..ddeb063b 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -28,6 +28,9 @@ import {EventEntry} from "./timeline/entries/EventEntry.js"; import {ObservedEventMap} from "./ObservedEventMap.js"; import {DecryptionSource} from "../e2ee/common.js"; import {ensureLogItem} from "../../logging/utils.js"; +import {TimelineReader} from "./timeline/persistence/TimelineReader.js"; +import {PowerLevels} from "./timeline/PowerLevels.js"; +import {RetainedObservableValue} from "../../observable/ObservableValue.js"; const EVENT_ENCRYPTED_TYPE = "m.room.encrypted"; @@ -388,8 +391,44 @@ export class BaseRoom extends EventEmitter { return this._summary.data.membership; } - get powerLevels() { - return this._timeline.powerLevels; + async loadPowerLevels() { + const timelineReader = new TimelineReader({ + roomId: this._roomId, + storage: this._storage, + fragmentIdComparer: this._fragmentIdComparer + }); + const txn = await this._storage.readTxn( + timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers, this._storage.storeNames.roomState) + ); + const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", ""); + if (powerLevelsState) { + return new PowerLevels({ + powerLevelEvent: powerLevelsState.event, + ownUserId: this._user.id, + membership: this.membership + }); + } + const createState = await txn.roomState.get(this._roomId, "m.room.create", ""); + if (createState) { + return new PowerLevels({ + createEvent: createState.event, + ownUserId: this._user.id, + membership: this.membership + }); + } else { + const membership = this.membership; + return new PowerLevels({ownUserId: this._user.id, membership}); + } + } + + async observePowerLevels() { + let observable = this._powerLevels; + if (!observable) { + const powerLevels = await this.loadPowerLevels(); + observable = new RetainedObservableValue(powerLevels, () => { this._powerLevels = null; }); + this._powerLevels = observable; + } + return observable; } enableSessionBackup(sessionBackup) { @@ -433,6 +472,7 @@ export class BaseRoom extends EventEmitter { }, clock: this._platform.clock, logger: this._platform.logger, + powerLevelsObservable: await this.observePowerLevels() }); try { if (this._roomEncryption) { diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3d82a284..11a49f55 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -21,12 +21,11 @@ import {Direction} from "./Direction.js"; import {TimelineReader} from "./persistence/TimelineReader.js"; import {PendingEventEntry} from "./entries/PendingEventEntry.js"; import {RoomMember} from "../members/RoomMember.js"; -import {PowerLevels} from "./PowerLevels.js"; import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js"; import {REDACTION_TYPE} from "../common.js"; export class Timeline { - constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock}) { + constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable}) { this._roomId = roomId; this._storage = storage; this._closeCallback = closeCallback; @@ -44,7 +43,8 @@ export class Timeline { }); this._readerRequest = null; this._allEntries = null; - this._powerLevels = null; + this._powerLevels = powerLevelsObservable.get(); + this._disposables.track(powerLevelsObservable.subscribe(powerLevels => this._powerLevels = powerLevels)); } /** @package */ @@ -66,7 +66,6 @@ export class Timeline { // as they should only populate once the view subscribes to it // if they are populated already, the sender profile would be empty - this._powerLevels = await this._loadPowerLevels(membership, txn); // 30 seems to be a good amount to fill the entire screen const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(30, txn, log)); try { @@ -78,28 +77,6 @@ export class Timeline { // txn should be assumed to have finished here, as decryption will close it. } - async _loadPowerLevels(membership, txn) { - // TODO: update power levels as state is updated - const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", ""); - if (powerLevelsState) { - return new PowerLevels({ - powerLevelEvent: powerLevelsState.event, - ownUserId: this._ownMember.userId, - membership - }); - } - const createState = await txn.roomState.get(this._roomId, "m.room.create", ""); - if (createState) { - return new PowerLevels({ - createEvent: createState.event, - ownUserId: this._ownMember.userId, - membership - }); - } else { - return new PowerLevels({ownUserId: this._ownMember.userId, membership}); - } - } - _setupEntries(timelineEntries) { this._remoteEntries.setManySorted(timelineEntries); if (this._pendingEvents) { From 22fab3761ad063a01bfa12d55b4ba290b0a5a2d7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 13:52:31 +0530 Subject: [PATCH 076/118] Remove timeline reader and only use roomState Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index ddeb063b..832406e1 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -28,7 +28,6 @@ import {EventEntry} from "./timeline/entries/EventEntry.js"; import {ObservedEventMap} from "./ObservedEventMap.js"; import {DecryptionSource} from "../e2ee/common.js"; import {ensureLogItem} from "../../logging/utils.js"; -import {TimelineReader} from "./timeline/persistence/TimelineReader.js"; import {PowerLevels} from "./timeline/PowerLevels.js"; import {RetainedObservableValue} from "../../observable/ObservableValue.js"; @@ -392,14 +391,7 @@ export class BaseRoom extends EventEmitter { } async loadPowerLevels() { - const timelineReader = new TimelineReader({ - roomId: this._roomId, - storage: this._storage, - fragmentIdComparer: this._fragmentIdComparer - }); - const txn = await this._storage.readTxn( - timelineReader.readTxnStores.concat(this._storage.storeNames.roomMembers, this._storage.storeNames.roomState) - ); + const txn = await this._storage.readTxn([this._storage.storeNames.roomState]); const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", ""); if (powerLevelsState) { return new PowerLevels({ From 14c00f50fcca1bff9012ad00993fc69acd4860eb Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 13:54:29 +0530 Subject: [PATCH 077/118] Make loadPowerLevels private Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 832406e1..7f5992ee 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -390,7 +390,7 @@ export class BaseRoom extends EventEmitter { return this._summary.data.membership; } - async loadPowerLevels() { + async _loadPowerLevels() { const txn = await this._storage.readTxn([this._storage.storeNames.roomState]); const powerLevelsState = await txn.roomState.get(this._roomId, "m.room.power_levels", ""); if (powerLevelsState) { @@ -416,7 +416,7 @@ export class BaseRoom extends EventEmitter { async observePowerLevels() { let observable = this._powerLevels; if (!observable) { - const powerLevels = await this.loadPowerLevels(); + const powerLevels = await this._loadPowerLevels(); observable = new RetainedObservableValue(powerLevels, () => { this._powerLevels = null; }); this._powerLevels = observable; } From 8e39aed4b6fe6db239af69a1a3c944a8a934eb59 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 14:32:39 +0530 Subject: [PATCH 078/118] Ensure that power levels are loaded only once Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index 7f5992ee..bcb8141f 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -414,11 +414,14 @@ export class BaseRoom extends EventEmitter { } async observePowerLevels() { + if (this._powerLevelLoading) { await this._powerLevelLoading; } let observable = this._powerLevels; if (!observable) { - const powerLevels = await this._loadPowerLevels(); + this._powerLevelLoading = this._loadPowerLevels(); + const powerLevels = await this._powerLevelLoading; observable = new RetainedObservableValue(powerLevels, () => { this._powerLevels = null; }); this._powerLevels = observable; + this._powerLevelLoading = null; } return observable; } From 2502c4024a89706d63a4a5f78d6dcc70762d3b6b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 15:06:39 +0530 Subject: [PATCH 079/118] Fix broken tests Signed-off-by: RMidhunSuresh --- src/domain/session/room/timeline/ReactionsViewModel.js | 8 ++++++-- src/matrix/room/timeline/Timeline.js | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 8813512d..51226775 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -189,6 +189,8 @@ import {HomeServer as MockHomeServer} from "../../../../mocks/HomeServer.js"; // other imports import {BaseMessageTile} from "./tiles/BaseMessageTile.js"; import {MappedList} from "../../../../observable/list/MappedList.js"; +import {ObservableValue} from "../../../../observable/ObservableValue.js"; +import {PowerLevels} from "../../../../matrix/room/timeline/PowerLevels.js"; export function tests() { const fragmentIdComparer = new FragmentIdComparer([]); @@ -251,8 +253,9 @@ export function tests() { await txn.complete(); // 2. setup queue & timeline const queue = new SendQueue({roomId, storage, hsApi: new MockHomeServer().api}); + const powerLevelsObservable = new ObservableValue(new PowerLevels({ ownUserId: alice, membership: "join" })); const timeline = new Timeline({roomId, storage, fragmentIdComparer, - clock: new MockClock(), pendingEvents: queue.pendingEvents}); + clock: new MockClock(), pendingEvents: queue.pendingEvents, powerLevelsObservable}); // 3. load the timeline, which will load the message with the reaction await timeline.load(new User(alice), "join", new NullLogItem()); const tiles = mapMessageEntriesToBaseMessageTile(timeline, queue); @@ -310,8 +313,9 @@ export function tests() { await txn.complete(); // 2. setup queue & timeline const queue = new SendQueue({roomId, storage, hsApi: new MockHomeServer().api}); + const powerLevelsObservable = new ObservableValue(new PowerLevels({ ownUserId: alice, membership: "join" })); const timeline = new Timeline({roomId, storage, fragmentIdComparer, - clock: new MockClock(), pendingEvents: queue.pendingEvents}); + clock: new MockClock(), pendingEvents: queue.pendingEvents, powerLevelsObservable}); // 3. load the timeline, which will load the message with the reaction await timeline.load(new User(alice), "join", new NullLogItem()); diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 11a49f55..bd521e62 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -43,8 +43,14 @@ export class Timeline { }); this._readerRequest = null; this._allEntries = null; - this._powerLevels = powerLevelsObservable.get(); - this._disposables.track(powerLevelsObservable.subscribe(powerLevels => this._powerLevels = powerLevels)); + this.initializePowerLevels(powerLevelsObservable); + } + + initializePowerLevels(observable) { + if (observable) { + this._powerLevels = observable.get(); + this._disposables.track(observable.subscribe(powerLevels => this._powerLevels = powerLevels)); + } } /** @package */ From c073d4c0d075242ca6154abc4af08ae5eed6ae39 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 17:30:59 +0530 Subject: [PATCH 080/118] Unmount child views correctly Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index e5d4b33b..dffc20c9 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -130,15 +130,18 @@ export class LazyListView extends ListView { } return array; } + _renderElementsInRange() { const { topCount, renderCount, bottomCount } = this._renderRange; const paddingTop = topCount * this._itemHeight; const paddingBottom = bottomCount * this._itemHeight; const renderedItems = this._itemsFromList(topCount, topCount + renderCount); - console.log(renderedItems); this._root.style.paddingTop = `${paddingTop}px`; this._root.style.paddingBottom = `${paddingBottom}px`; - this._root.innerHTML = ""; + for (const child of this._childInstances) { + child.root().remove(); + child.unmount(); + } this._renderItems(renderedItems); } From fe4f6d23089447bedfd5b4a52d45a1ce19ef3cdd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 18:06:33 +0530 Subject: [PATCH 081/118] Remove listHasChangedSize Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index dffc20c9..68082e44 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -73,19 +73,17 @@ export class LazyListView extends ListView { return new ItemRange(topCount, renderCount, bottomCount); } - _renderIfNeeded() { + _renderIfNeeded(forceRender = false) { + /* + forceRender only because we don't optimize onAdd/onRemove yet. + Ideally, onAdd/onRemove should only render whatever has changed + update padding + update renderRange + */ const range = this._getVisibleRange(); const intersectRange = range.expand(this._overflowMargin); const renderRange = range.expand(this._overflowItems); - const listHasChangedSize = !!this._renderRange && this._list.length !== this._renderRange.totalSize(); - // console.log("currentRange", range); - // console.log("renderRange", renderRange); - // console.log("intersectRange", intersectRange); - // console.log("LastRenderedRange", this._renderRange); - // only update render Range if the new range + overflowMargin isn't contained by the old anymore - if (listHasChangedSize || !this._renderRange.contains(intersectRange)) { - // console.log("New render change"); - // console.log("scrollTop", this._parent.scrollTop); + // only update render Range if the new range + overflowMargin isn't contained by the old anymore + // or if we are force rendering + if (forceRender || !this._renderRange.contains(intersectRange)) { this._renderRange = renderRange; this._renderElementsInRange(); } @@ -176,11 +174,11 @@ export class LazyListView extends ListView { // If size of the list changes, re-render onAdd() { - this._renderIfNeeded(); + this._renderIfNeeded(true); } onRemove() { - this._renderIfNeeded(); + this._renderIfNeeded(true); } onUpdate(idx, value, params) { From 8ee9cb10c22f88d566c8d95d746f25584d0096c2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 18:10:44 +0530 Subject: [PATCH 082/118] Move css to layout.css Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/layout.css | 6 ++++++ src/platform/web/ui/css/right-panel.css | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index fecbbd60..05c5b4e4 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -208,3 +208,9 @@ the layout viewport up without resizing it when the keyboard shows */ min-height: 0; overflow-y: auto; } + +.LazyListParent { + overflow:scroll; + overflow-x:hidden; + height: 100vh; +} diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index aa568068..cc6453e0 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -3,12 +3,6 @@ min-height: 0; } -.LazyListParent { - overflow:scroll; - overflow-x:hidden; - height: 100vh; -} - .RoomDetailsView { flex-direction: column; height: 100%; From 72f79e8bef7927b2aedca209585b35c44e7429cc Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 19:14:02 +0530 Subject: [PATCH 083/118] Reduce padding Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 3108ca27..9e596bfb 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -868,6 +868,10 @@ button.link { /* Memberlist Panel */ +.MemberListView { + padding-left: 16px; +} + .MemberTileView { margin-top: 8px; } From 8e55967db3f6c74079e6fce5c24578a3129aec9e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 20:53:45 +0530 Subject: [PATCH 084/118] Create UI to open memberlist from details panel Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/RoomDetailsViewModel.js | 7 +++++++ .../web/ui/session/rightpanel/RoomDetailsView.js | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index 896fab54..a2dff761 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -62,4 +62,11 @@ export class RoomDetailsViewModel extends ViewModel { super.dispose(); this._room.off("change", this._onRoomChange); } + + openPanel(segment) { + let path = this.navigation.path.until("room"); + path = path.with(this.navigation.segment("right-panel", true)); + path = path.with(this.navigation.segment(segment, true)); + this.navigation.applyPath(path); + } } diff --git a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js index 8357b722..7e7fa363 100644 --- a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js @@ -16,7 +16,8 @@ export class RoomDetailsView extends TemplateView { this._createRoomAliasDisplay(vm), t.div({className: "RoomDetailsView_rows"}, [ - this._createRightPanelRow(t, vm.i18n`People`, {MemberCount: true}, vm => vm.memberCount), + this._createRightPanelButtonRow(t, vm.i18n`People`, { MemberCount: true }, vm => vm.memberCount, + () => vm.openPanel("members")), this._createRightPanelRow(t, vm.i18n`Encryption`, {EncryptionStatus: true}, encryptionString) ]) ]); @@ -35,6 +36,14 @@ export class RoomDetailsView extends TemplateView { ]); } + _createRightPanelButtonRow(t, label, labelClass, value, onClick) { + const labelClassString = classNames({RoomDetailsView_label: true, ...labelClass}); + return t.button({className: "RoomDetailsView_row", onClick}, [ + t.div({className: labelClassString}, [label]), + t.div({className: "RoomDetailsView_value"}, value) + ]); + } + _createButton(t, vm) { return t.div({className: "RoomDetailsView_buttons"}, [ From 21f47f21aaef6e7a4ad1a64877291c5831d87115 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Wed, 14 Jul 2021 20:54:33 +0530 Subject: [PATCH 085/118] Add chevron to button Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 9 +++--- .../themes/element/icons/chevron-small.svg | 3 ++ .../web/ui/css/themes/element/theme.css | 28 ++++++++++++++++++- 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/platform/web/ui/css/themes/element/icons/chevron-small.svg diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index cc6453e0..20c2f300 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -16,15 +16,16 @@ text-align: center; } -.RoomDetailsView_row { - justify-content: space-between; -} - .RoomDetailsView_label, .RoomDetailsView_row, .RoomDetailsView, .EncryptionIconView { display: flex; align-items: center; } +.RoomDetailsView_value { + display: flex; + justify-content: flex-end; +} + .EncryptionIconView { justify-content: center; } diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-small.svg b/src/platform/web/ui/css/themes/element/icons/chevron-small.svg new file mode 100644 index 00000000..741e6be0 --- /dev/null +++ b/src/platform/web/ui/css/themes/element/icons/chevron-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 9e596bfb..9a6951c0 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -789,8 +789,11 @@ button.link { /* Right Panel */ -.RoomDetailsView { +.RightPanelView { background: rgba(245, 245, 245, 0.90); +} + +.RoomDetailsView { padding: 16px; } @@ -813,6 +816,24 @@ button.link { margin-bottom: 20px; font-weight: 500; font-size: 15px; + width: 100%; + background: none; + border: none; + padding: 0; +} + +button.RoomDetailsView_row { + cursor: pointer; +} + +button.RoomDetailsView_row::after { + content: url("./icons/chevron-small.svg"); + margin-left: 12px; +} + +.RoomDetailsView_row:not(button)::after{ + content: " "; + width: 19px; } .RoomDetailsView_label::before { @@ -821,8 +842,13 @@ button.link { width: 20px; } +.RoomDetailsView_label { + width: 200px; +} + .RoomDetailsView_value { color: #737D8C; + width: 50px; } .MemberCount::before { From f98a8847e35b9b5bc084ce0dc200df465f02a2dd Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 13:08:05 +0530 Subject: [PATCH 086/118] Implement UX - Add chevron to member count in room details - Make some rows in panel buttons - Add user chrome to right panel - Style UI Signed-off-by: RMidhunSuresh --- .../session/rightpanel/MemberListViewModel.js | 4 ++++ .../session/rightpanel/RightPanelViewModel.js | 15 +++++++++++++++ .../session/rightpanel/RoomDetailsViewModel.js | 13 ++++++++----- src/platform/web/ui/css/right-panel.css | 10 ++++++++-- .../themes/element/icons/chevron-thin-left.svg | 3 +++ src/platform/web/ui/css/themes/element/theme.css | 16 ++++++++++++++-- .../web/ui/session/rightpanel/RightPanelView.js | 16 ++++++++++++++++ .../web/ui/session/rightpanel/RoomDetailsView.js | 7 ------- 8 files changed, 68 insertions(+), 16 deletions(-) create mode 100644 src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index bc54ad3e..3b6a2c98 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -16,6 +16,10 @@ export class MemberListViewModel extends ViewModel { get type() { return "member-list"; } + get shouldShowBackButton() { return true; } + + get previousSegmentName() { return "details"; } + _filterJoinedMembers(members) { return members.filterValues(member => member.membership === "join"); } diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 29755e8d..3e866208 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -44,4 +44,19 @@ export class RightPanelViewModel extends ViewModel { updater(true); return updater; } + + closePanel() { + const path = this.navigation.path.until("room"); + this.navigation.applyPath(path); + } + + showPreviousPanel() { + const segmentName = this.activeViewModel.previousSegmentName; + if (segmentName) { + let path = this.navigation.path.until("room"); + path = path.with(this.navigation.segment("right-panel", true)); + path = path.with(this.navigation.segment(segmentName, true)); + this.navigation.applyPath(path); + } + } } diff --git a/src/domain/session/rightpanel/RoomDetailsViewModel.js b/src/domain/session/rightpanel/RoomDetailsViewModel.js index a2dff761..111e444c 100644 --- a/src/domain/session/rightpanel/RoomDetailsViewModel.js +++ b/src/domain/session/rightpanel/RoomDetailsViewModel.js @@ -13,6 +13,14 @@ export class RoomDetailsViewModel extends ViewModel { return "room-details"; } + get shouldShowBackButton() { + return false; + } + + get previousSegmentName() { + return false; + } + get roomId() { return this._room.id; } @@ -53,11 +61,6 @@ export class RoomDetailsViewModel extends ViewModel { this.emitChange(); } - closePanel() { - const path = this.navigation.path.until("room"); - this.navigation.applyPath(path); - } - dispose() { super.dispose(); this._room.off("change", this._onRoomChange); diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 20c2f300..bf074ee5 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -30,10 +30,16 @@ justify-content: center; } -.RoomDetailsView_buttons { +.RightPanelView_buttons { display: flex; - justify-content: flex-end; + justify-content: space-between; width: 100%; + box-sizing: border-box; + padding: 8px; +} + +.RightPanelView_buttons .hide { + visibility: hidden; } .MemberTileView { diff --git a/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg b/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg new file mode 100644 index 00000000..092bf4fb --- /dev/null +++ b/src/platform/web/ui/css/themes/element/icons/chevron-thin-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 9a6951c0..cc1d8485 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -883,15 +883,19 @@ button.RoomDetailsView_row::after { content: url("./icons/e2ee-disabled.svg"); } -.RoomDetailsView .button-utility { +.RightPanelView_buttons .button-utility { width: 24px; height: 24px; } -.RoomDetailsView .close { +.RightPanelView_buttons .close { background-image: url("./icons/clear.svg"); } +.RightPanelView_buttons .back { + background-image: url("./icons/chevron-thin-left.svg"); +} + /* Memberlist Panel */ .MemberListView { @@ -912,3 +916,11 @@ button.RoomDetailsView_row::after { width: 184px; white-space: nowrap; } + +.LazyListParent { + overflow: hidden; +} + +.LazyListParent:hover { + overflow-y: auto; +} diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index 8f932004..c4f56243 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -11,8 +11,24 @@ export class RightPanelView extends TemplateView { }; return t.div({ className: "RightPanelView" }, [ + t.mapView(vm => vm.activeViewModel && vm, vm => vm ? new ButtonsView(vm) : null), t.mapView(vm => vm.activeViewModel, vm => vm ? new viewFromType[vm.type](vm) : new LoadingView()) ] ); } } + +class ButtonsView extends TemplateView { + render(t, vm) { + return t.div({ className: "RightPanelView_buttons" }, + [ + t.button({ + className: { + "back": true, + "button-utility": true, + "hide": !vm.activeViewModel.shouldShowBackButton + }, onClick: () => vm.showPreviousPanel()}), + t.button({className: "close button-utility", onClick: () => vm.closePanel()}) + ]); + } +} diff --git a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js index 7e7fa363..eb3522a4 100644 --- a/src/platform/web/ui/session/rightpanel/RoomDetailsView.js +++ b/src/platform/web/ui/session/rightpanel/RoomDetailsView.js @@ -6,7 +6,6 @@ export class RoomDetailsView extends TemplateView { render(t, vm) { const encryptionString = () => vm.isEncrypted ? vm.i18n`On` : vm.i18n`Off`; return t.div({className: "RoomDetailsView"}, [ - this._createButton(t, vm), t.div({className: "RoomDetailsView_avatar"}, [ t.view(new AvatarView(vm, 52)), @@ -44,12 +43,6 @@ export class RoomDetailsView extends TemplateView { ]); } - _createButton(t, vm) { - return t.div({className: "RoomDetailsView_buttons"}, - [ - t.button({className: "close button-utility", onClick: () => vm.closePanel()}) - ]); - } } class EncryptionIconView extends TemplateView { From b126ba13709b8dbdc2099e9d306385fba9164380 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 13:48:10 +0530 Subject: [PATCH 087/118] Fix lazy list css Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/layout.css | 2 -- src/platform/web/ui/css/right-panel.css | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index 05c5b4e4..793ea9b1 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -210,7 +210,5 @@ the layout viewport up without resizing it when the keyboard shows */ } .LazyListParent { - overflow:scroll; - overflow-x:hidden; height: 100vh; } diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index bf074ee5..eb1fcf80 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -1,6 +1,8 @@ .RightPanelView{ grid-area: right; min-height: 0; + display: flex; + flex-direction: column; } .RoomDetailsView { From c7e12c98b577772e6ff84355eaf9d6be6459e306 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 15:03:32 +0530 Subject: [PATCH 088/118] Make addPanelIfNeeded more generic Signed-off-by: RMidhunSuresh --- src/domain/navigation/index.js | 12 +++++++----- src/domain/session/RoomGridViewModel.js | 3 +-- src/domain/session/leftpanel/LeftPanelViewModel.js | 3 +-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/domain/navigation/index.js b/src/domain/navigation/index.js index 2f039eef..6ce63ffd 100644 --- a/src/domain/navigation/index.js +++ b/src/domain/navigation/index.js @@ -92,12 +92,14 @@ function pushRightPanelSegment(array, segment) { array.push(new Segment(segment)); } -export function addPanelIfNeeded(navigation, panel, path) { - const segment = navigation.path.get(panel); +export function addPanelIfNeeded(navigation, path) { + const segments = navigation.path.segments; + const i = segments.findIndex(segment => segment.type === "right-panel"); let _path = path; - if (segment?.value) { - _path = _path.with(navigation.segment("right-panel")); - _path = _path.with(segment) + if (i !== -1) { + _path = path.until("room"); + _path = _path.with(segments[i]); + _path = _path.with(segments[i + 1]); } return _path; } diff --git a/src/domain/session/RoomGridViewModel.js b/src/domain/session/RoomGridViewModel.js index a193d48b..08887f3e 100644 --- a/src/domain/session/RoomGridViewModel.js +++ b/src/domain/session/RoomGridViewModel.js @@ -82,8 +82,7 @@ export class RoomGridViewModel extends ViewModel { _switchToRoom(roomId) { let path = this.navigation.path.until("rooms"); path = path.with(this.navigation.segment("room", roomId)); - path = addPanelIfNeeded(this.navigation, "details", path); - path = addPanelIfNeeded(this.navigation, "members", path); + path = addPanelIfNeeded(this.navigation, path); this.navigation.applyPath(path); } diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 549adf6a..9cdb099b 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -95,8 +95,7 @@ export class LeftPanelViewModel extends ViewModel { _pathForRightPanel(path) { let _path = path; - _path = addPanelIfNeeded(this.navigation, "details", _path); - _path = addPanelIfNeeded(this.navigation, "members", _path); + _path = addPanelIfNeeded(this.navigation, _path); return _path; } From be46d870aa7837cb583ede4127b32ccce02a9e66 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 15:17:39 +0530 Subject: [PATCH 089/118] Center names Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index eb1fcf80..1413fab8 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -47,3 +47,8 @@ .MemberTileView { display: flex; } + +.MemberTileView_name { + display: flex; + align-items: center; +} From 5a54be2e2ebf965f919f9b495ac7e2ddd4e8d4e4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 16:25:59 +0530 Subject: [PATCH 090/118] Style loading view Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/layout.css | 6 ++++++ src/platform/web/ui/css/themes/element/theme.css | 9 +++++++++ src/platform/web/ui/session/rightpanel/LoadingView.js | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index 793ea9b1..df12534f 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -212,3 +212,9 @@ the layout viewport up without resizing it when the keyboard shows */ .LazyListParent { height: 100vh; } + +.LoadingView { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index cc1d8485..6dad1d3f 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -787,6 +787,15 @@ button.link { width: 100%; } +.LoadingView { + height: 100%; + width: 100%; +} + +.LoadingView .spinner { + margin-left: 5px; +} + /* Right Panel */ .RightPanelView { diff --git a/src/platform/web/ui/session/rightpanel/LoadingView.js b/src/platform/web/ui/session/rightpanel/LoadingView.js index eeafea70..f27e5f01 100644 --- a/src/platform/web/ui/session/rightpanel/LoadingView.js +++ b/src/platform/web/ui/session/rightpanel/LoadingView.js @@ -3,6 +3,6 @@ import {spinner} from "../../common.js"; export class LoadingView extends TemplateView { render(t) { - return t.div(["Loading ", spinner(t)]); + return t.div({ className: "LoadingView" }, ["Loading", spinner(t)]); } } From 5873ab63baf3a80538a5c88db8ee8ad9e71beb06 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 16:49:30 +0530 Subject: [PATCH 091/118] Release memberlist after panel is closed Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 4 +++- src/domain/session/rightpanel/RightPanelViewModel.js | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 3b6a2c98..9b14e2af 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -6,9 +6,11 @@ import {Disambiguator} from "./disambiguator.js"; export class MemberListViewModel extends ViewModel { constructor(options) { super(options); + const list = options.members; + this.track(() => list.release()); const powerLevels = options.powerLevelsObservable.get(); // We should subscribe to the observable here so that we can resort when pl changes - this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(options.members)) + this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(list.members)) .sortValues(createMemberComparator(powerLevels)); this.nameDisambiguator = new Disambiguator(); this.mediaRepository = options.mediaRepository; diff --git a/src/domain/session/rightpanel/RightPanelViewModel.js b/src/domain/session/rightpanel/RightPanelViewModel.js index 3e866208..c56080a8 100644 --- a/src/domain/session/rightpanel/RightPanelViewModel.js +++ b/src/domain/session/rightpanel/RightPanelViewModel.js @@ -12,10 +12,10 @@ export class RightPanelViewModel extends ViewModel { get activeViewModel() { return this._activeViewModel; } async _getMemberArguments() { - const list = await this._room.loadMemberList(); + const members = await this._room.loadMemberList(); const room = this._room; const powerLevelsObservable = await this._room.observePowerLevels(); - return {members: list.members, powerLevelsObservable, mediaRepository: room.mediaRepository}; + return {members, powerLevelsObservable, mediaRepository: room.mediaRepository}; } _setupNavigation() { From e406aa6e456475e426fd8f319beac0ffd98ad519 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 16:58:29 +0530 Subject: [PATCH 092/118] Add jsdoc for powerlevels Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index bcb8141f..cb2e439f 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -413,6 +413,11 @@ export class BaseRoom extends EventEmitter { } } + /** + * Get the PowerLevels of the room. + * Always subscribe to the value returned by this method. + * @returns {RetainedObservableValue} PowerLevels of the room + */ async observePowerLevels() { if (this._powerLevelLoading) { await this._powerLevelLoading; } let observable = this._powerLevels; From 960f2c283e754785bcc2d4eee7d5c9ee74d22a1c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 17:02:07 +0530 Subject: [PATCH 093/118] Remove comment Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/comparator.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/session/rightpanel/comparator.js b/src/domain/session/rightpanel/comparator.js index 089ea1ad..b501c1e1 100644 --- a/src/domain/session/rightpanel/comparator.js +++ b/src/domain/session/rightpanel/comparator.js @@ -2,7 +2,6 @@ import {PowerLevels} from "../../../matrix/room/timeline/PowerLevels.js"; export function createMemberComparator(powerLevels) { const collator = new Intl.Collator(); - //TODO: This is so that all names with @ do not club together; but do we care? const removeCharacter = string => string.charAt(0) === "@"? string.slice(1) : string; return function comparator(member, otherMember) { From 60793798c71fa4589dd2a11f90e097ee58cbac6c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 17:19:27 +0530 Subject: [PATCH 094/118] Subscribe to powerLevels Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 9b14e2af..9701923c 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -8,8 +8,11 @@ export class MemberListViewModel extends ViewModel { super(options); const list = options.members; this.track(() => list.release()); - const powerLevels = options.powerLevelsObservable.get(); - // We should subscribe to the observable here so that we can resort when pl changes + + const powerLevelsObservable = options.powerLevelsObservable; + this.track(powerLevelsObservable.subscribe(() => { /*resort based on new power levels here*/ })); + + const powerLevels = powerLevelsObservable.get(); this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(list.members)) .sortValues(createMemberComparator(powerLevels)); this.nameDisambiguator = new Disambiguator(); From 5c0c59044f68a09e0a4a72f6a2e1d12a97095ea2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 17:31:16 +0530 Subject: [PATCH 095/118] Move spinner before the text Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 2 +- src/platform/web/ui/session/rightpanel/LoadingView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 6dad1d3f..216558e3 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -793,7 +793,7 @@ button.link { } .LoadingView .spinner { - margin-left: 5px; + margin-right: 5px; } /* Right Panel */ diff --git a/src/platform/web/ui/session/rightpanel/LoadingView.js b/src/platform/web/ui/session/rightpanel/LoadingView.js index f27e5f01..7d59d10e 100644 --- a/src/platform/web/ui/session/rightpanel/LoadingView.js +++ b/src/platform/web/ui/session/rightpanel/LoadingView.js @@ -3,6 +3,6 @@ import {spinner} from "../../common.js"; export class LoadingView extends TemplateView { render(t) { - return t.div({ className: "LoadingView" }, ["Loading", spinner(t)]); + return t.div({ className: "LoadingView" }, [spinner(t), "Loading"]); } } From fe18b616884ca02f875fb3b31c4a5242dc70ca82 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 18:40:04 +0530 Subject: [PATCH 096/118] Css fixes Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 4 ---- src/platform/web/ui/css/themes/element/theme.css | 9 +++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 1413fab8..a6062072 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -46,9 +46,5 @@ .MemberTileView { display: flex; -} - -.MemberTileView_name { - display: flex; align-items: center; } diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index 216558e3..db3d5f27 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -909,6 +909,7 @@ button.RoomDetailsView_row::after { .MemberListView { padding-left: 16px; + padding-right: 16px; } .MemberTileView { @@ -921,15 +922,11 @@ button.RoomDetailsView_row::after { .MemberTileView_name { text-overflow: ellipsis; - overflow: clip; - width: 184px; + overflow: hidden; white-space: nowrap; + flex: 1; } .LazyListParent { - overflow: hidden; -} - -.LazyListParent:hover { overflow-y: auto; } From c4c0e0206f7a1fc22ac5cd1119dced83a5b1e703 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 18:43:01 +0530 Subject: [PATCH 097/118] Clear margin on ul Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index db3d5f27..e8ac6558 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -910,6 +910,7 @@ button.RoomDetailsView_row::after { .MemberListView { padding-left: 16px; padding-right: 16px; + margin: 0; } .MemberTileView { From 4bac98d39af2f7df20c66c86ff59fd9bc5befe73 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 18:47:02 +0530 Subject: [PATCH 098/118] More css fixes Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 2 +- src/platform/web/ui/css/themes/element/theme.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index a6062072..c8a64a27 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -37,7 +37,7 @@ justify-content: space-between; width: 100%; box-sizing: border-box; - padding: 8px; + padding: 16px; } .RightPanelView_buttons .hide { diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index e8ac6558..e785027d 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -914,7 +914,7 @@ button.RoomDetailsView_row::after { } .MemberTileView { - margin-top: 8px; + margin-bottom: 8px; } .MemberTileView .avatar { From 9a3d7e416a0e098a2c46a619f8df1bfe0ecb230d Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 18:51:15 +0530 Subject: [PATCH 099/118] Remove top padding of room details view Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index e785027d..29f0d01e 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -804,6 +804,7 @@ button.link { .RoomDetailsView { padding: 16px; + padding-top: 0; } .RoomDetailsView_id { From 0ac3d3727b8856af0fd4595444e8f921ae0c7967 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 21:40:34 +0530 Subject: [PATCH 100/118] Remove TemplateView Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberListView.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberListView.js b/src/platform/web/ui/session/rightpanel/MemberListView.js index 5667c20e..77235249 100644 --- a/src/platform/web/ui/session/rightpanel/MemberListView.js +++ b/src/platform/web/ui/session/rightpanel/MemberListView.js @@ -1,13 +1,12 @@ -import {TemplateView} from "../../general/TemplateView.js"; import {LazyListView} from "../../general/LazyListView.js"; import {MemberTileView} from "./MemberTileView.js"; -export class MemberListView extends TemplateView { - render(t, vm) { - return t.view(new LazyListView({ +export class MemberListView extends LazyListView{ + constructor(vm) { + super({ list: vm.memberTileViewModels, className: "MemberListView", itemHeight: 40 - }, tileViewModel => new MemberTileView(tileViewModel))); + }, tileViewModel => new MemberTileView(tileViewModel)); } } From da733f9f8a1d1556ca7ea1ac21ad1cd2fa151bf7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 21:50:40 +0530 Subject: [PATCH 101/118] Move files to members directory Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 4 ++-- src/domain/session/rightpanel/{ => members}/comparator.js | 2 +- src/domain/session/rightpanel/{ => members}/disambiguator.js | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename src/domain/session/rightpanel/{ => members}/comparator.js (98%) rename src/domain/session/rightpanel/{ => members}/disambiguator.js (100%) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 9701923c..19ca6e9a 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -1,7 +1,7 @@ import {ViewModel} from "../../ViewModel.js"; import {MemberTileViewModel} from "./MemberTileViewModel.js"; -import {createMemberComparator} from "./comparator.js"; -import {Disambiguator} from "./disambiguator.js"; +import {createMemberComparator} from "./members/comparator.js"; +import {Disambiguator} from "./members/disambiguator.js"; export class MemberListViewModel extends ViewModel { constructor(options) { diff --git a/src/domain/session/rightpanel/comparator.js b/src/domain/session/rightpanel/members/comparator.js similarity index 98% rename from src/domain/session/rightpanel/comparator.js rename to src/domain/session/rightpanel/members/comparator.js index b501c1e1..7b87abe7 100644 --- a/src/domain/session/rightpanel/comparator.js +++ b/src/domain/session/rightpanel/members/comparator.js @@ -1,4 +1,4 @@ -import {PowerLevels} from "../../../matrix/room/timeline/PowerLevels.js"; +import {PowerLevels} from "../../../../matrix/room/timeline/PowerLevels.js"; export function createMemberComparator(powerLevels) { const collator = new Intl.Collator(); diff --git a/src/domain/session/rightpanel/disambiguator.js b/src/domain/session/rightpanel/members/disambiguator.js similarity index 100% rename from src/domain/session/rightpanel/disambiguator.js rename to src/domain/session/rightpanel/members/disambiguator.js From 1f67aa34d36946a41d5379485bf3f21679c82cba Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 21:55:19 +0530 Subject: [PATCH 102/118] Move LoadingView.js Signed-off-by: RMidhunSuresh --- .../web/ui/{session/rightpanel => general}/LoadingView.js | 4 ++-- src/platform/web/ui/session/rightpanel/RightPanelView.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/platform/web/ui/{session/rightpanel => general}/LoadingView.js (59%) diff --git a/src/platform/web/ui/session/rightpanel/LoadingView.js b/src/platform/web/ui/general/LoadingView.js similarity index 59% rename from src/platform/web/ui/session/rightpanel/LoadingView.js rename to src/platform/web/ui/general/LoadingView.js index 7d59d10e..0041b03f 100644 --- a/src/platform/web/ui/session/rightpanel/LoadingView.js +++ b/src/platform/web/ui/general/LoadingView.js @@ -1,5 +1,5 @@ -import {TemplateView} from "../../general/TemplateView.js"; -import {spinner} from "../../common.js"; +import {TemplateView} from "./TemplateView.js"; +import {spinner} from "../common.js"; export class LoadingView extends TemplateView { render(t) { diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index c4f56243..72b56e4b 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -1,7 +1,7 @@ import {TemplateView} from "../../general/TemplateView.js"; import {RoomDetailsView} from "./RoomDetailsView.js"; import {MemberListView} from "./MemberListView.js"; -import {LoadingView} from "./LoadingView.js"; +import {LoadingView} from "../../general/LoadingView.js"; export class RightPanelView extends TemplateView { render(t) { From 694b62730999667cbbfbe4f844cf70bdf97dc3b7 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 23:15:23 +0530 Subject: [PATCH 103/118] Inline method Signed-off-by: RMidhunSuresh --- src/domain/session/leftpanel/LeftPanelViewModel.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 9cdb099b..13583b14 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -93,25 +93,19 @@ export class LeftPanelViewModel extends ViewModel { } } - _pathForRightPanel(path) { - let _path = path; - _path = addPanelIfNeeded(this.navigation, _path); - return _path; - } - toggleGrid() { const room = this.navigation.path.get("room"); let path = this.navigation.path.until("session"); if (this.gridEnabled) { if (room) { path = path.with(room); - path = this._pathForRightPanel(path); + path = addPanelIfNeeded(this.navigation, path); } } else { if (room) { path = path.with(this.navigation.segment("rooms", [room.value])); path = path.with(room); - path = this._pathForRightPanel(path); + path = addPanelIfNeeded(this.navigation, path); } else { path = path.with(this.navigation.segment("rooms", [])); path = path.with(this.navigation.segment("empty-grid-tile", 0)); From 4946683b239ac19af5fb7422dda157ca27de978e Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 23:22:33 +0530 Subject: [PATCH 104/118] Use ifView instead of mapView Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/RightPanelView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index 72b56e4b..f812d6aa 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -11,7 +11,7 @@ export class RightPanelView extends TemplateView { }; return t.div({ className: "RightPanelView" }, [ - t.mapView(vm => vm.activeViewModel && vm, vm => vm ? new ButtonsView(vm) : null), + t.ifView(vm => vm.activeViewModel, vm => new ButtonsView(vm)), t.mapView(vm => vm.activeViewModel, vm => vm ? new viewFromType[vm.type](vm) : new LoadingView()) ] ); From c410aed672668d6ab64adec9c132e62b9dbcf63a Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 23:26:51 +0530 Subject: [PATCH 105/118] Use flex instead of setting height Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/layout.css | 2 +- src/platform/web/ui/css/right-panel.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/css/layout.css b/src/platform/web/ui/css/layout.css index df12534f..3fb23a27 100644 --- a/src/platform/web/ui/css/layout.css +++ b/src/platform/web/ui/css/layout.css @@ -210,7 +210,7 @@ the layout viewport up without resizing it when the keyboard shows */ } .LazyListParent { - height: 100vh; + flex: 1; } .LoadingView { diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index c8a64a27..77f63f70 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -7,7 +7,7 @@ .RoomDetailsView { flex-direction: column; - height: 100%; + flex: 1; } .RoomDetailsView_avatar { From ec8b6f9dd29e981b8f3bc9c4cab2365c768441cf Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Thu, 15 Jul 2021 23:35:44 +0530 Subject: [PATCH 106/118] Inline method Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/MemberListViewModel.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index 19ca6e9a..ce0fff40 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -13,7 +13,7 @@ export class MemberListViewModel extends ViewModel { this.track(powerLevelsObservable.subscribe(() => { /*resort based on new power levels here*/ })); const powerLevels = powerLevelsObservable.get(); - this.memberTileViewModels = this._mapTileViewModels(this._filterJoinedMembers(list.members)) + this.memberTileViewModels = this._mapTileViewModels(list.members.filterValues(member => member.membership === "join")) .sortValues(createMemberComparator(powerLevels)); this.nameDisambiguator = new Disambiguator(); this.mediaRepository = options.mediaRepository; @@ -25,10 +25,6 @@ export class MemberListViewModel extends ViewModel { get previousSegmentName() { return "details"; } - _filterJoinedMembers(members) { - return members.filterValues(member => member.membership === "join"); - } - _mapTileViewModels(members) { const mapper = (member, emitChange) => { const mediaRepository = this.mediaRepository; From 611524cddaa7997d236f6d946677b5bd74f238da Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 00:16:19 +0530 Subject: [PATCH 107/118] Log instead of throwing error Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 68082e44..84578f0d 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -99,7 +99,7 @@ export class LazyListView extends ListView { await new Promise(r => requestAnimationFrame(r)); this._height = this._parent.clientHeight; - if(this._height === 0) { throw "LazyListView could not calculate parent height."} + if (this._height === 0) { console.error("LazyListView could not calculate parent height."); } const range = this._getVisibleRange(); const renderRange = range.expand(this._overflowItems); this._renderRange = renderRange; From 0e0976c7f56ae0b6c889ce97a1c1fc9b64d2640b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 00:22:46 +0530 Subject: [PATCH 108/118] Inline method Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 84578f0d..f4ceb5c8 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -106,17 +106,6 @@ export class LazyListView extends ListView { this._renderElementsInRange(); } - _renderItems(items) { - this._childInstances = []; - const fragment = document.createDocumentFragment(); - for (const item of items) { - const view = this._childCreator(item); - this._childInstances.push(view); - fragment.appendChild(mountView(view, this._mountArgs)); - } - this._root.appendChild(fragment); - } - _itemsFromList(start, end) { const array = []; let i = 0; @@ -140,7 +129,14 @@ export class LazyListView extends ListView { child.root().remove(); child.unmount(); } - this._renderItems(renderedItems); + this._childInstances = []; + const fragment = document.createDocumentFragment(); + for (const item of renderedItems) { + const view = this._childCreator(item); + this._childInstances.push(view); + fragment.appendChild(mountView(view, this._mountArgs)); + } + this._root.appendChild(fragment); } mount() { From 829830c0fa4439c9405253a73d8038dcd6f4809b Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 13:10:09 +0530 Subject: [PATCH 109/118] Fix lazylist Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index f4ceb5c8..08052dac 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -126,8 +126,7 @@ export class LazyListView extends ListView { this._root.style.paddingTop = `${paddingTop}px`; this._root.style.paddingBottom = `${paddingBottom}px`; for (const child of this._childInstances) { - child.root().remove(); - child.unmount(); + this._removeChild(child); } this._childInstances = []; const fragment = document.createDocumentFragment(); @@ -167,6 +166,10 @@ export class LazyListView extends ListView { */ } + _removeChild(child) { + child.root().remove(); + child.unmount(); + } // If size of the list changes, re-render onAdd() { @@ -198,13 +201,14 @@ export class LazyListView extends ListView { // Element is moved up the list, so render element from top boundary const index = topCount; const child = childFromIndex(index); - // Modify childInstances here + this._childInstances.unshift(child); this._root.insertBefore(mountView(child, this._mountArgs), this._root.firstChild); } else { // Element is moved down the list, so render element from bottom boundary const index = topCount + renderCount - 1; const child = childFromIndex(index); + this._childInstances.push(child); this._root.appendChild(mountView(child, this._mountArgs)); } } @@ -212,10 +216,12 @@ export class LazyListView extends ListView { _removeAdditionalElement(fromIdx, toIdx) { if (toIdx < fromIdx) { // Element comes from the bottom, so remove element at bottom - this._root.lastChild.remove(); + const child = this._childInstances.pop(); + this._removeChild(child); } else { - this._root.firstChild.remove(); + const child = this._childInstances.shift(); + this._removeChild(child); } } @@ -230,7 +236,7 @@ export class LazyListView extends ListView { else if (fromInRange && !toInRange) { this.onBeforeListChanged(); const [child] = this._childInstances.splice(normalizedFromIdx, 1); - child.root().remove(); + this._removeChild(child); this._renderAdditionalElement(fromIdx, toIdx); this.onListChanged(); } From 0bd1d2b29141d76963ed65e4b8fde9a7bd838bee Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 13:39:02 +0530 Subject: [PATCH 110/118] Improve code Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 08052dac..90adc49e 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -214,15 +214,9 @@ export class LazyListView extends ListView { } _removeAdditionalElement(fromIdx, toIdx) { - if (toIdx < fromIdx) { - // Element comes from the bottom, so remove element at bottom - const child = this._childInstances.pop(); - this._removeChild(child); - } - else { - const child = this._childInstances.shift(); - this._removeChild(child); - } + // If element comes from the bottom, remove element at bottom and vice versa + const child = toIdx < fromIdx ? this._childInstances.pop() : this._childInstances.shift(); + this._removeChild(child); } onMove(fromIdx, toIdx, value) { From f366479c160d41558710b1113ffb3cf38cbfe588 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 13:39:17 +0530 Subject: [PATCH 111/118] Fix move bug Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 90adc49e..4fc581f5 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -237,8 +237,8 @@ export class LazyListView extends ListView { else if (!fromInRange && toInRange) { this.onBeforeListChanged(); const child = this._childCreator(value); - this._childInstances.splice(normalizedToIdx, 0, child); this._removeAdditionalElement(fromIdx, toIdx); + this._childInstances.splice(normalizedToIdx, 0, child); insertAt(this._root, normalizedToIdx, mountView(child, this._mountArgs)); this.onListChanged(); } From 9fdfebf00db0646e915a9373dba1619693d8953c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 14:19:48 +0530 Subject: [PATCH 112/118] Replace get with iterator and remove lambda Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 4fc581f5..c5b47059 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -118,6 +118,17 @@ export class LazyListView extends ListView { return array; } + _itemAtIndex(idx) { + let i = 0; + for (const item of this._list) { + if (i === idx) { + return item; + } + i = i + 1; + } + return null; + } + _renderElementsInRange() { const { topCount, renderCount, bottomCount } = this._renderRange; const paddingTop = topCount * this._itemHeight; @@ -196,18 +207,17 @@ export class LazyListView extends ListView { _renderAdditionalElement(fromIdx, toIdx) { const {topCount, renderCount} = this._renderRange; - const childFromIndex = index => this._childCreator(this._list.get(index)); if (toIdx < fromIdx) { // Element is moved up the list, so render element from top boundary const index = topCount; - const child = childFromIndex(index); + const child = this._childCreator(this._itemAtIndex(index)); this._childInstances.unshift(child); this._root.insertBefore(mountView(child, this._mountArgs), this._root.firstChild); } else { // Element is moved down the list, so render element from bottom boundary const index = topCount + renderCount - 1; - const child = childFromIndex(index); + const child = this._childCreator(this._itemAtIndex(index)); this._childInstances.push(child); this._root.appendChild(mountView(child, this._mountArgs)); } From 66d5f4d1a3be705cf3bcb456104b4d5547a1bc27 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 14:55:19 +0530 Subject: [PATCH 113/118] Make code clearer Signed-off-by: RMidhunSuresh --- src/platform/web/ui/general/LazyListView.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index c5b47059..f42084ae 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -205,7 +205,10 @@ export class LazyListView extends ListView { } } - _renderAdditionalElement(fromIdx, toIdx) { + /** + * Render additional element from top or bottom to offset the outgoing element + */ + _renderExtraOnMove(fromIdx, toIdx) { const {topCount, renderCount} = this._renderRange; if (toIdx < fromIdx) { // Element is moved up the list, so render element from top boundary @@ -223,7 +226,10 @@ export class LazyListView extends ListView { } } - _removeAdditionalElement(fromIdx, toIdx) { + /** + * Remove an element from top or bottom to make space for the incoming element + */ + _removeElementOnMove(fromIdx, toIdx) { // If element comes from the bottom, remove element at bottom and vice versa const child = toIdx < fromIdx ? this._childInstances.pop() : this._childInstances.shift(); this._removeChild(child); @@ -241,13 +247,13 @@ export class LazyListView extends ListView { this.onBeforeListChanged(); const [child] = this._childInstances.splice(normalizedFromIdx, 1); this._removeChild(child); - this._renderAdditionalElement(fromIdx, toIdx); + this._renderExtraOnMove(fromIdx, toIdx); this.onListChanged(); } else if (!fromInRange && toInRange) { this.onBeforeListChanged(); const child = this._childCreator(value); - this._removeAdditionalElement(fromIdx, toIdx); + this._removeElementOnMove(fromIdx, toIdx); this._childInstances.splice(normalizedToIdx, 0, child); insertAt(this._root, normalizedToIdx, mountView(child, this._mountArgs)); this.onListChanged(); From ec4a783759f42da0007f70f0072c16743cc1d5c5 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 15:49:21 +0530 Subject: [PATCH 114/118] Fix disambiguator - Do not disambiguate name on room rejoin Signed-off-by: RMidhunSuresh --- src/domain/session/rightpanel/members/disambiguator.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/session/rightpanel/members/disambiguator.js b/src/domain/session/rightpanel/members/disambiguator.js index f3fa551b..d6468031 100644 --- a/src/domain/session/rightpanel/members/disambiguator.js +++ b/src/domain/session/rightpanel/members/disambiguator.js @@ -32,9 +32,10 @@ export class Disambiguator { const value = this._map.get(name); if (value) { if (Array.isArray(value)) { + if (value.findIndex(member => member.userId === vm.userId) !== -1) { return; } value.push(vm); return value; - } else { + } else if(vm.userId !== value.userId) { const array = [value, vm] this._map.set(name, array); return array; From 436e87578e97db87ba1a1a50fc1c1b4c57aabba4 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 16:07:02 +0530 Subject: [PATCH 115/118] Initialize prop in constructor Signed-off-by: RMidhunSuresh --- src/matrix/room/BaseRoom.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/matrix/room/BaseRoom.js b/src/matrix/room/BaseRoom.js index cb2e439f..7ee04761 100644 --- a/src/matrix/room/BaseRoom.js +++ b/src/matrix/room/BaseRoom.js @@ -52,6 +52,8 @@ export class BaseRoom extends EventEmitter { this._getSyncToken = getSyncToken; this._platform = platform; this._observedEvents = null; + this._powerLevels = null; + this._powerLevelLoading = null; } async _eventIdsToEntries(eventIds, txn) { From 86bb56ab6fb8e99f86e23dabbde7e8c711bbc1a2 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 16:25:33 +0530 Subject: [PATCH 116/118] Fix layout of details panel in mobile Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/themes/element/theme.css | 2 +- 1 file changed, 1 insertion(+), 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 29f0d01e..56eaf224 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -858,7 +858,7 @@ button.RoomDetailsView_row::after { .RoomDetailsView_value { color: #737D8C; - width: 50px; + flex: 1; } .MemberCount::before { From 7c9755d1a6cd038c529824cf3e818205e0819559 Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 17:03:39 +0530 Subject: [PATCH 117/118] Fix width for smaller screens Signed-off-by: RMidhunSuresh --- src/platform/web/ui/css/right-panel.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index 77f63f70..937fa944 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -1,6 +1,7 @@ .RightPanelView{ grid-area: right; min-height: 0; + min-width: 0; display: flex; flex-direction: column; } From 1a721fe7e8703226872bb2d9e84a7e15f4a9ca6c Mon Sep 17 00:00:00 2001 From: RMidhunSuresh Date: Fri, 16 Jul 2021 17:13:05 +0530 Subject: [PATCH 118/118] Import from AvatarView.js Signed-off-by: RMidhunSuresh --- src/platform/web/ui/session/rightpanel/MemberTileView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/rightpanel/MemberTileView.js b/src/platform/web/ui/session/rightpanel/MemberTileView.js index bd90e4d3..95b04719 100644 --- a/src/platform/web/ui/session/rightpanel/MemberTileView.js +++ b/src/platform/web/ui/session/rightpanel/MemberTileView.js @@ -1,5 +1,5 @@ import {TemplateView} from "../../general/TemplateView.js"; -import {AvatarView} from "../../avatar.js"; +import {AvatarView} from "../../AvatarView.js"; export class MemberTileView extends TemplateView { render(t, vm) {