From 766ce4e21701c34109e3cde787aec975b9caeda4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 15 Apr 2021 15:12:14 +0200 Subject: [PATCH] create AvatarView and renderStaticAvatar (for timeline) and use it in RoomTileView, we make some efforts to only have one update listener for the entire list, because by default a subview would listen on the view model --- src/platform/web/ui/avatar.js | 119 ++++++++++++++++++ src/platform/web/ui/common.js | 19 --- .../web/ui/session/leftpanel/RoomTileView.js | 12 +- src/platform/web/ui/session/room/RoomView.js | 4 +- .../web/ui/session/room/timeline/common.js | 4 +- 5 files changed, 132 insertions(+), 26 deletions(-) create mode 100644 src/platform/web/ui/avatar.js diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js new file mode 100644 index 00000000..c68d5496 --- /dev/null +++ b/src/platform/web/ui/avatar.js @@ -0,0 +1,119 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {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._avatarUrl) { + this._avatarUrl = this.value.avatarUrl; + 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._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; + 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 + * @return {Element} + */ +export function renderStaticAvatar(vm, size) { + const hasAvatar = !!vm.avatarUrl; + const avatarClasses = classNames({ + avatar: true, + [`usercolor${vm.avatarColorNumber}`]: !hasAvatar, + }); + const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); + return tag.div({className: avatarClasses}, [avatarContent]); +} + +function renderImg(vm, size) { + const sizeStr = size.toString(); + return tag.img({src: vm.avatarUrl, width: sizeStr, height: sizeStr, title: vm.avatarTitle}); +} diff --git a/src/platform/web/ui/common.js b/src/platform/web/ui/common.js index f5b71198..9fbafcdf 100644 --- a/src/platform/web/ui/common.js +++ b/src/platform/web/ui/common.js @@ -31,22 +31,3 @@ export function spinner(t, extraClasses = undefined) { } } -/** - * @param {TemplateBuilder} t - * @param {Object} vm view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} - * @param {Number} size - * @return {Element} - */ -export function renderAvatar(t, vm, size) { - const hasAvatar = !!vm.avatarUrl; - const avatarClasses = { - avatar: true, - [`usercolor${vm.avatarColorNumber}`]: !hasAvatar, - }; - // TODO: handle updates from default to img or reverse - const sizeStr = size.toString(); - const avatarContent = hasAvatar ? - t.img({src: vm => vm.avatarUrl, width: sizeStr, height: sizeStr, title: vm => vm.avatarTitle}) : - vm => vm.avatarLetter; - return t.div({className: avatarClasses}, [avatarContent]); -} diff --git a/src/platform/web/ui/session/leftpanel/RoomTileView.js b/src/platform/web/ui/session/leftpanel/RoomTileView.js index fde02c25..84b38b62 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 {renderAvatar} from "../../common.js"; +import {AvatarView} from "../../avatar.js"; export class RoomTileView extends TemplateView { render(t, vm) { @@ -26,12 +26,12 @@ export class RoomTileView extends TemplateView { }; return t.li({"className": classes}, [ t.a({href: vm.url}, [ - renderAvatar(t, vm, 32), + t.view(new AvatarView(vm, 32), {parentProvidesUpdates: true}), t.div({className: "description"}, [ t.div({className: {"name": true, unread: vm => vm.isUnread}}, vm => vm.name), t.div({ className: { - "badge": true, + badge: true, highlighted: vm => vm.isHighlighted, hidden: vm => !vm.badgeCount } @@ -40,4 +40,10 @@ export class RoomTileView extends TemplateView { ]) ]); } + + update(value, props) { + super.update(value); + // update the AvatarView as we told it to not subscribe itself with parentProvidesUpdates + this.updateSubViews(value, props); + } } diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 65f464d9..b445dcac 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -19,7 +19,7 @@ import {TemplateView} from "../../general/TemplateView.js"; import {TimelineList} from "./TimelineList.js"; import {TimelineLoadingView} from "./TimelineLoadingView.js"; import {MessageComposer} from "./MessageComposer.js"; -import {renderAvatar} from "../../common.js"; +import {AvatarView} from "../../avatar.js"; export class RoomView extends TemplateView { render(t, vm) { @@ -27,7 +27,7 @@ export class RoomView extends TemplateView { t.div({className: "TimelinePanel"}, [ t.div({className: "RoomHeader middle-header"}, [ t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), - renderAvatar(t, vm, 32), + t.view(new AvatarView(vm, 32)), t.div({className: "room-description"}, [ t.h2(vm => vm.name), ]), diff --git a/src/platform/web/ui/session/room/timeline/common.js b/src/platform/web/ui/session/room/timeline/common.js index 0d1e30ee..22bcd6b1 100644 --- a/src/platform/web/ui/session/room/timeline/common.js +++ b/src/platform/web/ui/session/room/timeline/common.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {renderAvatar} from "../../../common.js"; +import {renderStaticAvatar} from "../../../avatar.js"; export function renderMessage(t, vm, children) { const classes = { @@ -28,7 +28,7 @@ export function renderMessage(t, vm, children) { }; const profile = t.div({className: "profile"}, [ - renderAvatar(t, vm, 30), + renderStaticAvatar(vm, 30), t.div({className: `sender usercolor${vm.avatarColorNumber}`}, vm.displayName) ]); children = [profile].concat(children);