From 79815978483ef0ab78f7159c0add3492da6457a1 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 3 Aug 2026 14:09:46 -0700 Subject: [PATCH] fix(reactions): wrap long popover names (#3834) **Category:** fix **User Impact:** Long custom emoji names now stay contained inside reaction popovers and remain fully readable. **Problem:** An unbroken custom emoji name could force a reaction popover beyond its intended maximum width and overflow the message view. **Solution:** Give the reaction popover a definite 288px width and allow the complete emoji name to wrap within it without truncation or ellipsis. Short names retain the same content and interaction behavior.
File changes **desktop/src/features/messages/ui/MessageReactions.tsx** Bounds the reaction popover width and allows long names to break across lines while preserving the full shortcode. **desktop/tests/e2e/reaction-names.spec.ts** Covers fixed width, full text preservation, and wrapping for the maximum supported colon-wrapped reaction name, with deterministic seeded Picsum visual fixtures and explicit image-load waits.
## Reproduction Steps 1. Open a message with a custom emoji reaction whose name is 64 characters. 2. Hover or focus the reaction pill to open its details popover. 3. Confirm the popover remains 288px wide and the complete name wraps within it without ellipsis. 4. Open a short-name reaction and confirm its popover remains readable and unchanged in behavior. ## Screenshots | Before | After | | --- | --- | | ![Maximum-length name before](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/max-length-before-picsum.png) | ![Maximum-length name after](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/max-length-after-picsum.png) | **Short-name regression check** ![Short reaction name](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/3834/short-name-after-picsum.png) ## Verification - `pnpm test` in `desktop`: 3,858 passed - Focused reaction-name E2E with seeded Picsum captures: 2 passed - Desktop checks and commit hooks passed Originating Buzz channel: `f2ec9671-d78e-4cde-894c-9f4c458c7f1f` --------- Signed-off-by: Taylor Ho Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz> --- .../features/messages/ui/MessageReactions.tsx | 7 +- desktop/tests/e2e/reaction-names.spec.ts | 129 +++++++++++++++++- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageReactions.tsx b/desktop/src/features/messages/ui/MessageReactions.tsx index 2250b3931f..cbcb873f5b 100644 --- a/desktop/src/features/messages/ui/MessageReactions.tsx +++ b/desktop/src/features/messages/ui/MessageReactions.tsx @@ -126,7 +126,10 @@ function ReactionPopoverContent({ reaction }: { reaction: TimelineReaction }) {
{userText} reacted with
-
+
{displayName}
@@ -504,7 +507,7 @@ function ReactionPill({ align="start" side="top" sideOffset={6} - className="w-auto min-w-56 max-w-72 rounded-xl p-3" + className="w-72 rounded-xl p-3" onMouseEnter={handleMouseEnter} onMouseLeave={scheduleClose} onOpenAutoFocus={(e) => e.preventDefault()} diff --git a/desktop/tests/e2e/reaction-names.spec.ts b/desktop/tests/e2e/reaction-names.spec.ts index 512b913dff..028bb1645d 100644 --- a/desktop/tests/e2e/reaction-names.spec.ts +++ b/desktop/tests/e2e/reaction-names.spec.ts @@ -1,11 +1,23 @@ import { expect, test } from "@playwright/test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { waitForAnimations } from "../helpers/animations"; import { installMockBridge } from "../helpers/bridge"; const REACTION_TARGET_CONTENT = "React to me with a custom emoji"; const REACTION_TARGET_EVENT_ID = "d".repeat(64); const BOB_PUBKEY = "bb22a5299220cad76ffd46190ccbeede8ab5dc260faa28b6e5a2cb31b9aff260"; +const MAX_REACTION_NAME = + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl"; +const MAX_REACTION_AVATAR_URL = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Crect width="16" height="16" rx="4" fill="%23e5484d"/%3E%3C/svg%3E'; +const SHORT_REACTION_AVATAR_URL = + 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"%3E%3Crect width="16" height="16" rx="4" fill="%2300a36c"/%3E%3C/svg%3E'; +const SCREENSHOT_DIR = + process.env.REACTION_POPOVER_SCREENSHOT_DIR ?? + "test-results/reaction-popover-screenshots"; function reactionTargetRow(page: import("@playwright/test").Page) { return page @@ -14,8 +26,45 @@ function reactionTargetRow(page: import("@playwright/test").Page) { .last(); } +async function waitForImage( + image: import("@playwright/test").Locator, +): Promise { + await expect(image).toBeVisible(); + await expect + .poll(() => + image.evaluate( + (element) => + element instanceof HTMLImageElement && + element.complete && + element.naturalWidth > 0, + ), + ) + .toBe(true); +} + +async function capturePopover( + page: import("@playwright/test").Page, + popover: import("@playwright/test").Locator, + filename: string, +): Promise { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + await waitForAnimations(page); + await popover.screenshot({ + animations: "disabled", + path: path.join(SCREENSHOT_DIR, filename), + }); +} + test.beforeEach(async ({ page }) => { - await installMockBridge(page); + await installMockBridge(page, { + searchProfiles: [ + { + pubkey: BOB_PUBKEY, + displayName: "bob", + avatarUrl: SHORT_REACTION_AVATAR_URL, + }, + ], + }); }); test("reaction popover resolves a reactor with no authored message in the window", async ({ @@ -33,21 +82,89 @@ test("reaction popover resolves a reactor with no authored message in the window ); await page.evaluate( - ({ pubkey, targetId }) => { + ({ pubkey, targetId, avatarUrl }) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "general", - content: "🎉", - extraTags: [["e", targetId]], + content: ":react:", + extraTags: [ + ["e", targetId], + ["emoji", "react", avatarUrl], + ], kind: 7, pubkey, }); }, - { pubkey: BOB_PUBKEY, targetId: REACTION_TARGET_EVENT_ID }, + { + avatarUrl: SHORT_REACTION_AVATAR_URL, + pubkey: BOB_PUBKEY, + targetId: REACTION_TARGET_EVENT_ID, + }, ); const row = reactionTargetRow(page); - const pill = row.getByRole("button", { name: "Toggle 🎉 reaction" }); + const pill = row.getByRole("button", { name: "Toggle :react: reaction" }); await expect(pill).toBeVisible(); await pill.hover(); await expect(page.getByText("bob reacted with")).toBeVisible(); + const popover = page + .locator("[data-radix-popper-content-wrapper]") + .filter({ hasText: "bob reacted with" }); + const avatar = popover.locator("img"); + await expect(avatar).toHaveAttribute("src", SHORT_REACTION_AVATAR_URL); + await waitForImage(avatar); + await capturePopover(page, popover, "short-name-after.png"); +}); + +test("maximum-length reaction name wraps inside a fixed-width popover", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + await page.waitForFunction( + () => + window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ + channelName: "general", + kind: 7, + }) === true, + ); + + const reaction = `:${MAX_REACTION_NAME}:`; + await page.evaluate( + ({ content, targetId, avatarUrl }) => { + window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ + channelName: "general", + content, + extraTags: [ + ["e", targetId], + ["emoji", content.slice(1, -1), avatarUrl], + ], + kind: 7, + }); + }, + { + avatarUrl: MAX_REACTION_AVATAR_URL, + content: reaction, + targetId: REACTION_TARGET_EVENT_ID, + }, + ); + + const pill = reactionTargetRow(page).getByRole("button", { + name: `Toggle ${reaction} reaction`, + }); + await expect(pill).toBeVisible(); + await pill.focus(); + + const popover = page + .locator("[data-radix-popper-content-wrapper]") + .filter({ hasText: reaction }); + await expect(popover).toBeVisible(); + await expect(popover).toHaveCSS("width", "288px"); + const reactionName = popover.getByTestId("reaction-popover-name"); + await expect(reactionName).toHaveCSS("word-break", "break-all"); + await expect(reactionName).toHaveText(reaction); + const avatar = popover.locator("img"); + await expect(avatar).toHaveAttribute("src", MAX_REACTION_AVATAR_URL); + await waitForImage(avatar); + await capturePopover(page, popover, "max-length-after.png"); });