From 7fc0cb242c7a7eac92fbe8bdfe4f0f3d9ae0df0f Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 11 Sep 2025 15:44:08 +0200 Subject: [PATCH] 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 --- playwright/e2e/composer/CIDER.spec.ts | 20 +++++++++++++++ .../composer/CIDER.spec.ts/mention-linux.png | Bin 0 -> 1613 bytes .../views/rooms/_BasicMessageComposer.pcss | 3 +++ src/Avatar.ts | 23 +++++++++++++++--- src/editor/parts.ts | 18 +++++++++++--- 5 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png diff --git a/playwright/e2e/composer/CIDER.spec.ts b/playwright/e2e/composer/CIDER.spec.ts index 8214c8058b..0d3454d351 100644 --- a/playwright/e2e/composer/CIDER.spec.ts +++ b/playwright/e2e/composer/CIDER.spec.ts @@ -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(); + }); }); }); diff --git a/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png b/playwright/snapshots/composer/CIDER.spec.ts/mention-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..d3b948956ea82862e28fba7db486874bf33fdf95 GIT binary patch literal 1613 zcmV-T2D15yP)X0ssI2{0Rb%000I8NklSS5J8+w z6eJ4|r;r*`9~m$sus`IlQ82xz^eW(GGpkFaWP<{oyU>~v_w|#>vdZ7Dq_~Ix004ju z1pH4)Ntq$2yScl5tH|3J7cn}h^Z3H3Pd+S^=km=gswg_F@Gi@>n?84|D$eW+mIx>K zHf_l}}ZBlB?Cbw&9`RNe=006LoptY^-Xj-}<6By#>zT+d}{V0Kf`? zO2rsb3#=`{dDKhB*gU+@NiyDcJtYa;nHWNA>M9yE16gT!c0pRGs9BM*FEQu3S`?B| zkQpI04(Q+!xpS$mCg(t6hFm2I%s7)7I_hAaRS(2It(c% z$-I$Oy-eNaC3OQS`nB})%jn_DXeZZsmLpM=o9rSZGi5s)O7`d1aJ_`Q>=hB|rM;fI z>wsb)OB7WMxtz8sVyPm78*Evhy(i%UGgwv2i7qE^3X&+!HV+Tpt!?dkz23nqxvC|Z z0002M7z7?tcSEXI*E_OmXdnLSXVWho{a&dZes1=#_&QDV9acw6-Nl2;@*Yn46KAU8ly^8nd%258?!_-!xWMWaeEbw`7S41v$oG%XtlBF-dwe!EGuc(!3vHJj`00EH{vN`al}3}NdN!<7>9Yi z-qOsD(MHQXdZ-hLsp;mTu^XE^sWm!EB%*pX48swLkB8V8;VH5E3JJwA&4va14BFGn z6#OaLldsaU9h#9x=tGkP004k-*oTCy`6?^>*|TTYE2=(mqvd*W2F` zn=1bJz57~u<>;|Qw5NZNNJoSsTDl@QBy_2+=13i<>*7lDYl5;4pFAikt<#XX{<8RB zQkPWPpe2JtQqtHHX;f)7tD74vOISsi6AEVdp1-4m!@6|?*YA7O^3**SDOu*>P{|Ae z0001%>}SrL`Q?Gsq`jX%Ztb{mx-@=o%wRc1`xQ3%S?^PDs+ep0*|L(jU40Ypxd8wG*vS0;dS74PnY^>Pr}O&z`#n4tuL%ouahpH%PHQXs_=db{(%xyM zC}Cc_zM64|Nn)h3MT*n00v1!K~w_(h4W&&&>w@(00000 LNkvXXu0mjf$yW#u literal 0 HcmV?d00001 diff --git a/res/css/views/rooms/_BasicMessageComposer.pcss b/res/css/views/rooms/_BasicMessageComposer.pcss index 5729c22075..b017b03da3 100644 --- a/res/css/views/rooms/_BasicMessageComposer.pcss +++ b/res/css/views/rooms/_BasicMessageComposer.pcss @@ -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 */ diff --git a/src/Avatar.ts b/src/Avatar.ts index 921332c250..8852578f28 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -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 diff --git a/src/editor/parts.ts b/src/editor/parts.ts index ad49058609..9e476ffaf8 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -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 => {