feat(desktop): show full URL tooltip on masked link hover#1625
Conversation
ca682c2 to
6bf9b0d
Compare
Masked link — URL revealed on hoverA Bare link — no tooltipA pasted |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6bf9b0d449
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return ( | ||
| <MaskedLinkTooltip disabled={isLinearLink} href={href} label={label}> | ||
| {anchor} | ||
| </MaskedLinkTooltip> |
There was a problem hiding this comment.
Suppress masked-link tooltips inside hidden spoilers
When the masked link is inside an unrevealed spoiler, this wrapper still installs a Radix tooltip trigger on the hidden anchor. The spoiler CSS only makes .buzz-spoiler__content transparent; it does not disable pointer/focus events, and the tooltip content is portaled outside the spoiler, so hovering or tab-focusing ||[secret](https://private.example/path)|| reveals the URL before the spoiler is opened. Please disable the tooltip while the trigger is within .buzz-spoiler[data-revealed="false"] or otherwise prevent hidden spoiler descendants from opening it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ea27128.
SpoilerInline now provides a SpoilerHiddenContext (true while unrevealed, and always true for inert preview spoilers); MaskedLinkTooltip consumes it and renders the plain anchor with no Radix trigger while hidden, so neither hover nor focus can open the tooltip. It re-enables reactively when the spoiler is revealed.
Added an e2e regression in spoiler.spec.ts asserting the URL tooltip stays closed on both hover and focus of a masked link inside a hidden spoiler, then appears normally after reveal. The focus vector was the reproducible leak (the particles overlay intercepts the pointer, but focus bypasses it).
A masked link inside an unrevealed spoiler still installed a Radix tooltip trigger on the hidden anchor. The spoiler only masks its content visually and intercepts pointerdown/click to reveal — it does not suppress hover or focus — and the tooltip content is portaled outside the spoiler, so hovering or tab-focusing the hidden link leaked its URL before the spoiler was opened. SpoilerInline now provides a SpoilerHiddenContext (true while unrevealed, and always true for inert preview spoilers); MaskedLinkTooltip consumes it and renders the plain anchor with no tooltip trigger while hidden. It re-enables reactively when the spoiler is revealed. Covers both the hover and focus vectors. Reported by Codex review on block#1625. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aaron Goldsmith <aargoldsmith@gmail.com>
Masked links — [text](url) where the visible text hides the destination — now show a tooltip with the full URL on hover, mirroring Slack's anti-phishing affordance. Bare pasted URLs and GFM autolinks are left unchanged: their label already is the destination, modulo cosmetic differences (omitted scheme, trailing slash, host case). isMaskedLink parses both sides with the URL API and compares component-wise so cosmetic differences stay tooltip-free while spoof shapes always trigger: scheme downgrades ([https://x](http://x)), case-only path/query changes, protocol-relative hrefs, and userinfo labels (safe.com@evil.com) are all treated as masked. The tooltip content is pointer-events-none so it can never intercept clicks meant for the link or a spoiler wrapping it, and never truncates so the full destination stays auditable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aaron Goldsmith <aargoldsmith@gmail.com>
The spoiler-link reveal test failed intermittently on cold-start runs
(~1 in 3), pre-existing on main and surfaced while validating the
masked-link tooltip. click({force: true}) computes its target point from
the link's bounding box while the freshly sent row is still settling
layout, so the click can land a few px above the link on the author
name. Wait for the link's position to be stable across two animation
frames before clicking.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Aaron Goldsmith <aargoldsmith@gmail.com>
A masked link inside an unrevealed spoiler still installed a Radix tooltip trigger on the hidden anchor. The spoiler only masks its content visually and intercepts pointerdown/click to reveal — it does not suppress hover or focus — and the tooltip content is portaled outside the spoiler, so hovering or tab-focusing the hidden link leaked its URL before the spoiler was opened. SpoilerInline now provides a SpoilerHiddenContext (true while unrevealed, and always true for inert preview spoilers); MaskedLinkTooltip consumes it and renders the plain anchor with no tooltip trigger while hidden. It re-enables reactively when the spoiler is revealed. Covers both the hover and focus vectors. Reported by Codex review on block#1625. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Aaron Goldsmith <aargoldsmith@gmail.com>
ea27128 to
7ae40d1
Compare


Summary
Masked links —
[text](url)where the visible text hides the destination — nowshow a tooltip with the full URL on hover, mirroring Slack's anti-phishing
affordance. The reader can see where a "click here" link actually goes before
clicking. Bare pasted URLs and GFM autolinks are unchanged: their label already
is the destination, so a tooltip would be redundant.
Motivation
A message like
[the quarterly report](https://intranet.example.com/…)rendersonly the label. Without hovering, there's no way to tell a legitimate link from
a lookalike (
[accounts.google.com](https://evil.example)). Slack added exactlythis hover-to-reveal behavior; this brings desktop Buzz to parity.
Scope
Deliberately narrow for a first version:
(modulo cosmetics), no tooltip.
are out of scope.
buzz://deep links, and Linear-styled links keep their existing hover treatments.
How it works
isMaskedLink(label, href)(desktop/src/shared/lib/maskedLink.ts) parsesboth sides with the
URLAPI and compares component-wise. Cosmeticdifferences stay tooltip-free (omitted scheme, trailing slash, host case)
while spoof shapes always trigger.
(
desktop/src/shared/ui/markdown/MaskedLinkTooltip.tsx) —markdown.tsxisat its file-size ceiling. It reuses the existing Radix tooltip primitive and
the app-global
TooltipProvider.pointer-events-none(never intercepts a click meant forthe link or a spoiler wrapping it) and never truncates (
max-w-md+overflow-wrap:anywhere) so the full destination stays auditable — most URLsfit on one line, long ones wrap at natural boundaries instead of mid-word.
Adversarial review
The first predicate draft used naive string normalization (scheme-stripping +
whole-URL lowercasing). An adversarial review pass (codex + gemini) found
spoofable false negatives, all now covered by tests:
[https://x](http://x)//host/…hrefsafe.com@evil.comAlso included
A second, separate commit fixes a pre-existing flaky e2e test
(
spoiler.spec.ts) surfaced while validating this change:click({force:true})computed its target from the link's bounding box while the freshly sent row was
still settling layout, so the click occasionally landed a few px off on the
author name (~1 in 3 cold runs, reproducible on
main). Now waits for thelink's position to be stable across two animation frames before clicking.
Testing
just cigreen (fmt, clippy, desktop lint, unit tests, mobile tests, builds).isMaskedLinkcovering every spoof case above.the previously flaky test.
don't, and the documented spoof cases all trigger.
Not in scope
buzz://anchors.(
[docs](example.com/x)) render as relative URLs and resolve against the apporigin. Independent of this change; tracked separately.