fix(desktop): integer-align custom reaction emoji (#4779)

## 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 <kalvin@block.xyz>
Co-authored-by: npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc <5288f082d91aff2de5bc8be23fbfa01ca2e3859cac57a91b7a3fa9f12349505c@buzz.block.builderlab.xyz>
This commit is contained in:
Kalvin C
2026-08-04 20:38:19 +00:00
committed by GitHub
co-authored by npub122y0pqkertljmedu303rl0aqrj3w8pvu43t6jxm6875lzg6f2pwqegc3xc
parent ce3cf3cd25
commit 8b8d86c5d2
2 changed files with 40 additions and 2 deletions
@@ -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 =
+39 -1
View File
@@ -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: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="#22c55e"/></svg>',
}),
);
// 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: '<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"><rect width="40" height="20" fill="#22c55e"/></svg>',
}),
);
});
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) => {