import { NgClass } from "@angular/common"; import { Component, OnChanges, input } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; import { Utils } from "@bitwarden/common/platform/misc/utils"; type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall"; const SizeClasses: Record = { xlarge: ["tw-h-24", "tw-w-24", "tw-min-w-24"], large: ["tw-h-16", "tw-w-16", "tw-min-w-16"], default: ["tw-h-10", "tw-w-10", "tw-min-w-10"], small: ["tw-h-7", "tw-w-7", "tw-min-w-7"], xsmall: ["tw-h-6", "tw-w-6", "tw-min-w-6"], }; /** * Avatars display a unique color that helps a user visually recognize their logged in account. * A variance in color across the avatar component is important as it is used in Account Switching as a * visual indicator to recognize which of a personal or work account a user is logged into. */ @Component({ selector: "bit-avatar", template: `@if (src) { }`, imports: [NgClass], }) export class AvatarComponent implements OnChanges { readonly border = input(false); readonly color = input(); readonly id = input(); readonly text = input(); readonly title = input(); readonly size = input("default"); private svgCharCount = 2; private svgFontSize = 20; private svgFontWeight = 300; private svgSize = 48; src?: SafeResourceUrl; constructor(public sanitizer: DomSanitizer) {} ngOnChanges() { this.generate(); } get classList() { return ["tw-rounded-full", "tw-inline"] .concat(SizeClasses[this.size()] ?? []) .concat(this.border() ? ["tw-border", "tw-border-solid", "tw-border-secondary-600"] : []); } private generate() { const color = this.color(); const text = this.text(); const id = this.id(); if (!text && !color && !id) { throw new Error("Must supply `text`, `color`, or `id` input."); } let chars: string | null = null; const upperCaseText = text?.toUpperCase() ?? ""; chars = this.getFirstLetters(upperCaseText, this.svgCharCount); if (chars == null) { chars = this.unicodeSafeSubstring(upperCaseText, this.svgCharCount); } // If the chars contain an emoji, only show it. const emojiMatch = chars.match(Utils.regexpEmojiPresentation); if (emojiMatch) { chars = emojiMatch[0]; } let svg: HTMLElement; let hexColor = color ?? ""; if (!Utils.isNullOrWhitespace(hexColor)) { svg = this.createSvgElement(this.svgSize, hexColor); } else if (!Utils.isNullOrWhitespace(id ?? "")) { hexColor = Utils.stringToColor(id!.toString()); svg = this.createSvgElement(this.svgSize, hexColor); } else { hexColor = Utils.stringToColor(upperCaseText); svg = this.createSvgElement(this.svgSize, hexColor); } const charObj = this.createTextElement(chars, hexColor); svg.appendChild(charObj); const html = window.document.createElement("div").appendChild(svg).outerHTML; const svgHtml = window.btoa(unescape(encodeURIComponent(html))); // This is safe because the only user provided value, chars is set using `textContent` this.src = this.sanitizer.bypassSecurityTrustResourceUrl( "data:image/svg+xml;base64," + svgHtml, ); } private getFirstLetters(data: string, count: number): string | null { const parts = data.split(" "); if (parts.length > 1) { let text = ""; for (let i = 0; i < count; i++) { text += this.unicodeSafeSubstring(parts[i], 1); } return text; } return null; } private createSvgElement(size: number, color: string): HTMLElement { const svgTag = window.document.createElement("svg"); svgTag.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svgTag.setAttribute("pointer-events", "none"); svgTag.setAttribute("width", size.toString()); svgTag.setAttribute("height", size.toString()); svgTag.style.backgroundColor = color; svgTag.style.width = size + "px"; svgTag.style.height = size + "px"; return svgTag; } private createTextElement(character: string, color: string): HTMLElement { const textTag = window.document.createElement("text"); textTag.setAttribute("text-anchor", "middle"); textTag.setAttribute("y", "50%"); textTag.setAttribute("x", "50%"); textTag.setAttribute("dy", "0.35em"); textTag.setAttribute("pointer-events", "auto"); textTag.setAttribute("fill", Utils.pickTextColorBasedOnBgColor(color, 135, true)); textTag.setAttribute( "font-family", 'Roboto,"Helvetica Neue",Helvetica,Arial,' + 'sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"', ); // Warning do not use innerHTML here, characters are user provided textTag.textContent = character; textTag.style.fontWeight = this.svgFontWeight.toString(); textTag.style.fontSize = this.svgFontSize + "px"; return textTag; } private unicodeSafeSubstring(str: string, count: number) { const characters = str.match(/./gu); return characters != null ? characters.slice(0, count).join("") : ""; } }