diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index fea17ba1..49a90dd5 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,8 @@ export class Platform { this._disposables.track(disposable); } } + 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()); diff --git a/src/platform/web/ui/AvatarView.js b/src/platform/web/ui/AvatarView.js new file mode 100644 index 00000000..1f6f2736 --- /dev/null +++ b/src/platform/web/ui/AvatarView.js @@ -0,0 +1,86 @@ +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.textContent = vm.avatarLetter; + this._root.classList.add(bgColorClass); + } + } + const hasAvatar = !!vm.avatarUrl(this._size); + if (this._avatarTitleChanged() && hasAvatar) { + const element = this._root.firstChild; + if (element.tagName === "IMG") { + element.setAttribute("title", vm.avatarTitle); + } + } + if (this._avatarLetterChanged() && !hasAvatar) { + this._root.textContent = vm.avatarLetter; + } + } +} diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 8845f887..2e2b0142 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -14,90 +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 {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; - } - } -} - +import {tag, text, classNames, setAttribute} from "./general/html.js"; /** * @param {Object} vm view model with {avatarUrl, avatarColorNumber, avatarTitle, avatarLetter} * @param {Number} size @@ -108,16 +25,36 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { let avatarClasses = classNames({ avatar: true, [`size-${size}`]: true, - [`usercolor${vm.avatarColorNumber}`]: !hasAvatar, + [`usercolor${vm.avatarColorNumber}`]: !hasAvatar }); if (extraClasses) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - return tag.div({className: avatarClasses}, [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; } -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}); } + +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 avatarColorNumber = parent.getAttribute("data-avatar-color"); + parent.classList.add(`usercolor${avatarColorNumber}`); + const avatarLetter = parent.getAttribute("data-avatar-letter"); + parent.textContent = avatarLetter; +} 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) {