Align default avatar and fix colors in composer pills (#30739)

* fix: align default avatar in composer pills

* fix: use correct color for avatar in composer pills when there is no image

* test(e2e): add test for cider mention

* chore: fix typo
This commit is contained in:
Florian Duros
2025-09-11 15:44:08 +02:00
committed by GitHub
parent 5edcc4c1c4
commit 7fc0cb242c
5 changed files with 57 additions and 7 deletions

View File

@@ -14,6 +14,9 @@ const CtrlOrMeta = process.platform === "darwin" ? "Meta" : "Control";
test.describe("Composer", () => {
test.use({
displayName: "Janet",
botCreateOpts: {
displayName: "Bob",
},
});
test.use({
@@ -94,5 +97,22 @@ test.describe("Composer", () => {
).toBeVisible();
});
});
test("can send mention", { tag: "@screenshot" }, async ({ page, bot, app }) => {
// Set up a private room so we have another user to mention
await app.client.createRoom({
is_direct: true,
invite: [bot.credentials.userId],
});
await app.viewRoomByName("Bob");
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await composer.pressSequentially("@bob");
await page.getByRole("option", { name: "Bob" }).click();
await expect(composer.getByText("Bob")).toBeVisible();
await expect(composer).toMatchScreenshot("mention.png");
await composer.press("Enter");
await expect(page.locator(".mx_EventTile_body", { hasText: "Bob" })).toBeVisible();
});
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -10,6 +10,7 @@ Please see LICENSE files in the repository root for full details.
/* These are set in Javascript */
--avatar-letter: "";
--avatar-background: unset;
--avatar-color: unset;
--placeholder: "";
position: relative;
@@ -54,6 +55,8 @@ Please see LICENSE files in the repository root for full details.
span.mx_UserPill,
span.mx_RoomPill,
span.mx_SpacePill {
display: inline-flex;
align-items: center;
user-select: all;
position: relative;
cursor: unset; /* We don't want indicate clickability */

View File

@@ -13,14 +13,18 @@ import DMRoomMap from "./utils/DMRoomMap";
import { mediaFromMxc } from "./customisations/Media";
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
import { getFirstGrapheme } from "./utils/strings";
import ThemeWatcher from "./settings/watchers/ThemeWatcher";
/**
* Hardcoded from the Compound colors.
* Shade for background as defined in the compound web implementation
* https://github.com/vector-im/compound-web/blob/main/src/components/Avatar
*/
const AVATAR_BG_COLORS = ["#e9f2ff", "#faeefb", "#e3f7ed", "#ffecf0", "#ffefe4", "#e3f5f8", "#f1efff", "#e0f8d9"];
const AVATAR_TEXT_COLORS = ["#043894", "#671481", "#004933", "#7e0642", "#850000", "#004077", "#4c05b5", "#004b00"];
const AVATAR_BG_LIGHT_COLORS = ["#e0f8d9", "#e3f5f8", "#faeefb", "#f1efff", "#ffecf0", "#ffefe4"];
const AVATAR_TEXT_LIGHT_COLORS = ["#005f00", "#00548c", "#822198", "#5d26cd", "#9f0850", "#9b2200"];
const AVATAR_BG_DARK_COLORS = ["#002600", "#001b4e", "#37004e", "#22006a", "#450018", "#470000"];
const AVATAR_TEXT_DARK_COLORS = ["#56c02c", "#21bacd", "#d991de", "#ad9cfe", "#fe84a2", "#f6913d"];
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(
@@ -42,6 +46,13 @@ export function avatarUrlForMember(
return url;
}
/**
* Determines if the current theme is dark
*/
function isDarkTheme(): boolean {
return new ThemeWatcher().getEffectiveTheme() === "dark";
}
/**
* Determines the HEX color to use in the avatar pills
* @param id the user or room ID
@@ -51,7 +62,8 @@ export function getAvatarTextColor(id: string): string {
// eslint-disable-next-line react-hooks/rules-of-hooks
const index = useIdColorHash(id);
return AVATAR_TEXT_COLORS[index - 1];
// Use light colors by default
return isDarkTheme() ? AVATAR_TEXT_DARK_COLORS[index - 1] : AVATAR_TEXT_LIGHT_COLORS[index - 1];
}
export function avatarUrlForUser(
@@ -103,7 +115,10 @@ export function defaultAvatarUrlForString(s: string): string {
// overwritten color value in custom themes
const cssVariable = `--avatar-background-colors_${colorIndex}`;
const cssValue = getComputedStyle(document.body).getPropertyValue(cssVariable);
const color = cssValue || AVATAR_BG_COLORS[colorIndex - 1];
// Light colors are the default
const color =
cssValue || isDarkTheme() ? AVATAR_BG_DARK_COLORS[colorIndex - 1] : AVATAR_BG_LIGHT_COLORS[colorIndex - 1];
let dataUrl = colorToDataURLCache.get(color);
if (!dataUrl) {
// validate color as this can come from account_data

View File

@@ -297,7 +297,12 @@ export abstract class PillPart extends BasePart implements IPillPart {
}
// helper method for subclasses
protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void {
protected setAvatarVars(
node: HTMLElement,
avatarUrl: string,
initialLetter: string,
avatarTextColor?: string,
): void {
const avatarBackground = `url('${avatarUrl}')`;
const avatarLetter = `'${initialLetter}'`;
// check if the value is changing,
@@ -308,6 +313,9 @@ export abstract class PillPart extends BasePart implements IPillPart {
if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) {
node.style.setProperty("--avatar-letter", avatarLetter);
}
if (avatarTextColor && node.style.getPropertyValue("--avatar-color") !== avatarTextColor) {
node.style.setProperty("--avatar-color", avatarTextColor);
}
}
public serialize(): ISerializedPillPart {
@@ -421,11 +429,13 @@ class RoomPillPart extends PillPart {
protected setAvatar(node: HTMLElement): void {
let initialLetter = "";
let avatarUrl = Avatar.avatarUrlForRoom(this.room ?? null, 16, 16, "crop");
let avatarTextColor: string | undefined;
if (!avatarUrl) {
initialLetter = Avatar.getInitialLetter(this.room?.name || this.resourceId) ?? "";
avatarUrl = Avatar.defaultAvatarUrlForString(this.room?.roomId ?? this.resourceId);
avatarTextColor = Avatar.getAvatarTextColor(this.room?.roomId ?? this.resourceId);
}
this.setAvatarVars(node, avatarUrl, initialLetter);
this.setAvatarVars(node, avatarUrl, initialLetter, avatarTextColor);
}
public get type(): IPillPart["type"] {
@@ -479,10 +489,12 @@ class UserPillPart extends PillPart {
const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this.member.userId);
const avatarUrl = Avatar.avatarUrlForMember(this.member, 16, 16, "crop");
let initialLetter = "";
let avatarTextColor: string | undefined;
if (avatarUrl === defaultAvatarUrl) {
initialLetter = Avatar.getInitialLetter(name) ?? "";
avatarTextColor = Avatar.getAvatarTextColor(this.member.userId);
}
this.setAvatarVars(node, avatarUrl, initialLetter);
this.setAvatarVars(node, avatarUrl, initialLetter, avatarTextColor);
}
protected onClick = (): void => {