Member avatars without canvas (#9990)
* Strict typechecking fixes for Base/Member/Avatar Update the core avatar files to pass `--strict --noImplicitAny` typechecks. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Add tests for Base/Member/Avatar More thoroughly test the core avatar files. Not necessarily the most thorough, but an improvement. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Extract TextAvatar from BaseAvatar Extracted the fallback/textual avatar into its own component. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Use standard HTML for non-image avatars Firefox users with `resistFingerprinting` enabled were seeing random noise for rooms and users without avatars. There's no real reason to use data URLs to present flat colors. This converts non-image avatars to inline blocks with background colors. See https://github.com/vector-im/element-web/issues/23936 Signed-off-by: Clark Fischer <clark.fischer@gmail.com> * Have pills use solid backgrounds rather than colored images Similar to room and member avatars, pills now use colored pseudo-elements rather than background images. Signed-off-by: Clark Fischer <clark.fischer@gmail.com> --------- Signed-off-by: Clark Fischer <clark.fischer@gmail.com> Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
parent
64cec31981
commit
a8aa4de4b4
22 changed files with 806 additions and 311 deletions
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015, 2016, 2023 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.
|
||||
|
@ -24,16 +24,19 @@ import DMRoomMap from "./utils/DMRoomMap";
|
|||
import { mediaFromMxc } from "./customisations/Media";
|
||||
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
|
||||
|
||||
const DEFAULT_COLORS: Readonly<string[]> = ["#0DBD8B", "#368bd6", "#ac3ba8"];
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(
|
||||
member: RoomMember,
|
||||
member: RoomMember | null | undefined,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod: ResizeMethod,
|
||||
): string {
|
||||
let url: string;
|
||||
if (member?.getMxcAvatarUrl()) {
|
||||
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
let url: string | undefined;
|
||||
const mxcUrl = member?.getMxcAvatarUrl();
|
||||
if (mxcUrl) {
|
||||
url = mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
if (!url) {
|
||||
// member can be null here currently since on invites, the JS SDK
|
||||
|
@ -44,6 +47,17 @@ export function avatarUrlForMember(
|
|||
return url;
|
||||
}
|
||||
|
||||
export function getMemberAvatar(
|
||||
member: RoomMember | null | undefined,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod: ResizeMethod,
|
||||
): string | undefined {
|
||||
const mxcUrl = member?.getMxcAvatarUrl();
|
||||
if (!mxcUrl) return undefined;
|
||||
return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
|
||||
export function avatarUrlForUser(
|
||||
user: Pick<User, "avatarUrl">,
|
||||
width: number,
|
||||
|
@ -86,18 +100,10 @@ function urlForColor(color: string): string {
|
|||
// hard to install a listener here, even if there were a clear event to listen to
|
||||
const colorToDataURLCache = new Map<string, string>();
|
||||
|
||||
export function defaultAvatarUrlForString(s: string): string {
|
||||
export function defaultAvatarUrlForString(s: string | undefined): string {
|
||||
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
|
||||
const defaultColors = ["#0DBD8B", "#368bd6", "#ac3ba8"];
|
||||
let total = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
total += s.charCodeAt(i);
|
||||
}
|
||||
const colorIndex = total % defaultColors.length;
|
||||
// overwritten color value in custom themes
|
||||
const cssVariable = `--avatar-background-colors_${colorIndex}`;
|
||||
const cssValue = document.body.style.getPropertyValue(cssVariable);
|
||||
const color = cssValue || defaultColors[colorIndex];
|
||||
|
||||
const color = getColorForString(s);
|
||||
let dataUrl = colorToDataURLCache.get(color);
|
||||
if (!dataUrl) {
|
||||
// validate color as this can come from account_data
|
||||
|
@ -112,13 +118,23 @@ export function defaultAvatarUrlForString(s: string): string {
|
|||
return dataUrl;
|
||||
}
|
||||
|
||||
export function getColorForString(input: string): string {
|
||||
const charSum = [...input].reduce((s, c) => s + c.charCodeAt(0), 0);
|
||||
const index = charSum % DEFAULT_COLORS.length;
|
||||
|
||||
// overwritten color value in custom themes
|
||||
const cssVariable = `--avatar-background-colors_${index}`;
|
||||
const cssValue = document.body.style.getPropertyValue(cssVariable);
|
||||
return cssValue || DEFAULT_COLORS[index]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the first (non-sigil) character of 'name',
|
||||
* converted to uppercase
|
||||
* @param {string} name
|
||||
* @return {string} the first letter
|
||||
*/
|
||||
export function getInitialLetter(name: string): string {
|
||||
export function getInitialLetter(name: string): string | undefined {
|
||||
if (!name) {
|
||||
// XXX: We should find out what causes the name to sometimes be falsy.
|
||||
console.trace("`name` argument to `getInitialLetter` not supplied");
|
||||
|
@ -134,19 +150,20 @@ export function getInitialLetter(name: string): string {
|
|||
}
|
||||
|
||||
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
|
||||
return split(name, "", 1)[0].toUpperCase();
|
||||
return split(name, "", 1)[0]!.toUpperCase();
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(
|
||||
room: Room,
|
||||
room: Room | undefined,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod?: ResizeMethod,
|
||||
): string | null {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
if (room.getMxcAvatarUrl()) {
|
||||
return mediaFromMxc(room.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
const mxcUrl = room.getMxcAvatarUrl();
|
||||
if (mxcUrl) {
|
||||
return mediaFromMxc(mxcUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
|
||||
// space rooms cannot be DMs so skip the rest
|
||||
|
@ -159,8 +176,9 @@ export function avatarUrlForRoom(
|
|||
|
||||
// If there are only two members in the DM use the avatar of the other member
|
||||
const otherMember = room.getAvatarFallbackMember();
|
||||
if (otherMember?.getMxcAvatarUrl()) {
|
||||
return mediaFromMxc(otherMember.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
const otherMemberMxc = otherMember?.getMxcAvatarUrl();
|
||||
if (otherMemberMxc) {
|
||||
return mediaFromMxc(otherMemberMxc).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue