From 8b8d86c5d26e2fa8cf419fdd8d0e56433f95d71a Mon Sep 17 00:00:00 2001 From: Kalvin C Date: Tue, 4 Aug 2026 13:38:19 -0700 Subject: [PATCH] fix(desktop): integer-align custom reaction emoji (#4779) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - remove the fractional half-pixel translation from custom reaction emoji - preserve the existing 28px reaction pill, 14×14 glyph box, and `object-fit: contain` - add real-app Playwright coverage for integer centering and non-square intrinsic dimensions ### Related issue None found. Follow-up to the Buzz emoji-warp investigation. ### Testing - `cd desktop && pnpm exec playwright test tests/e2e/custom-emoji.spec.ts --project=smoke` (15 passed) - `cd desktop && pnpm test` (4,171 passed) - `cd desktop && pnpm lint` (passed; two pre-existing informational `useTemplate` diagnostics) - `cd desktop && pnpm typecheck` (passed) - `cd desktop && pnpm exec biome check src/features/messages/ui/MessageReactions.tsx tests/e2e/custom-emoji.spec.ts` (passed) Independent review also mutation-tested the regression coverage by restoring the half-pixel transform and confirming the new test fails. No after screenshot is included because the patch preserves dimensions and fixes subpixel raster alignment; the real-app test asserts the mechanism directly. Validated at `bc95969b21b58d83b7f94de4ad25e499e52b35fb`. Signed-off-by: Kalvin Chau Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz> --- .../features/messages/ui/MessageReactions.tsx | 2 +- desktop/tests/e2e/custom-emoji.spec.ts | 40 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageReactions.tsx b/desktop/src/features/messages/ui/MessageReactions.tsx index cbcb873f5..d4bec8db6 100644 --- a/desktop/src/features/messages/ui/MessageReactions.tsx +++ b/desktop/src/features/messages/ui/MessageReactions.tsx @@ -17,7 +17,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; const REACTION_PILL_BASE_CLASSES = "inline-flex h-7 items-center rounded-full border text-xs font-medium leading-none transition-colors"; -const REACTION_CUSTOM_GLYPH_CLASSES = "h-3.5 w-3.5 -translate-y-[0.5px]"; +const REACTION_CUSTOM_GLYPH_CLASSES = "h-3.5 w-3.5"; const REACTION_NATIVE_GLYPH_CLASSES = "h-3 w-3 text-xs"; const REACTION_COUNT_CLASSES = "text-muted-foreground"; const REACTION_NATIVE_COUNT_CLASSES = diff --git a/desktop/tests/e2e/custom-emoji.spec.ts b/desktop/tests/e2e/custom-emoji.spec.ts index ae20aaef2..a48799bcb 100644 --- a/desktop/tests/e2e/custom-emoji.spec.ts +++ b/desktop/tests/e2e/custom-emoji.spec.ts @@ -19,6 +19,7 @@ import { waitForAnimations } from "../helpers/animations"; // live even in mock-bridge mode (the mock only intercepts Tauri commands), so // this spec uses the simpler mock-bridge setup like messaging.spec.ts. const SHORTCODE = "buzz"; +const MOCK_MEDIA_PROXY_PORT = 54321; async function waitForMockLiveSubscription( page: import("@playwright/test").Page, @@ -60,6 +61,16 @@ test.beforeEach(async ({ page }) => { body: '', }), ); + // Keep the reaction fixture deliberately non-square so the real renderer can + // prove object-contain letterboxes it inside the fixed square glyph box. + await page.route( + `http://127.0.0.1:${MOCK_MEDIA_PROXY_PORT}/media/**`, + (route) => + route.fulfill({ + contentType: "image/svg+xml", + body: '', + }), + ); }); test("typing a known :shortcode: renders an inline emoji node in the composer", async ({ @@ -236,7 +247,6 @@ test("native emoji-only messages leave space below the author metadata", async ( // real 127.0.0.1 URL rather than the buzz-media:// fallback. const REACTION_SHORTCODE = "react"; -const MOCK_MEDIA_PROXY_PORT = 54321; const SELECTED_ACTION_CLASS = /(^|\s)bg-secondary(\s|$)/; // A seeded message in `general` with a real 64-hex id — the only reactable // target in mock mode (getReactionTargetId() requires a 64-hex `e` tag, which @@ -336,6 +346,25 @@ test("reacting with a custom emoji renders via the loopback media proxy", async `^http://127\\.0\\.0\\.1:${MOCK_MEDIA_PROXY_PORT}/media/[\\da-f]{64}\\.png$`, ), ); + await expect(reactionPill).toHaveCSS("height", "28px"); + await expect(reactionImg).toHaveCSS("height", "14px"); + await expect(reactionImg).toHaveCSS("width", "14px"); + await expect(reactionImg).toHaveCSS("object-fit", "contain"); + await expect(reactionImg).toHaveCSS("transform", "none"); + await expect + .poll(() => + reactionImg.evaluate((image) => { + const imageRect = image.getBoundingClientRect(); + const pillRect = image.closest("button")?.getBoundingClientRect(); + if (!(image instanceof HTMLImageElement) || !pillRect) return null; + return { + naturalHeight: image.naturalHeight, + naturalWidth: image.naturalWidth, + topOffset: imageRect.top - pillRect.top, + }; + }), + ) + .toEqual({ naturalHeight: 20, naturalWidth: 40, topOffset: 7 }); await expect .poll(() => quickReactionStorageContains(page, `:${REACTION_SHORTCODE}:`)) @@ -348,6 +377,15 @@ test("reacting with a custom emoji renders via the loopback media proxy", async ); const inlineAddReactionButton = row.getByLabel("Add reaction"); + // The picker closes with the pointer/focus position depending on animation + // timing. Put the row into a deterministic idle state before checking the + // pill's pre-existing hidden behavior. + await page.mouse.move(0, 0); + await page.evaluate(() => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }); await expect .poll(() => inlineAddReactionButton.evaluate((button) => {