Skip to content

Commit 45067ec

Browse files
wesbillmanPinky
andauthored
fix(desktop): render autolinked message links as chips (#1241)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
1 parent 733c766 commit 45067ec

4 files changed

Lines changed: 140 additions & 34 deletions

File tree

desktop/src/features/messages/lib/messageLink.test.mjs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
buildMessageLink,
66
isMessageLink,
77
parseMessageLink,
8+
resolveMessageLinkRenderTarget,
89
} from "./messageLink.ts";
910

1011
const CHANNEL = "f570339f-8f8a-4e08-a779-8d954aa44109";
@@ -118,3 +119,31 @@ test("isMessageLink matches buzz://message and legacy buzz://message", () => {
118119
assert.equal(isMessageLink(undefined), false);
119120
assert.equal(isMessageLink(""), false);
120121
});
122+
123+
test("resolveMessageLinkRenderTarget distinguishes autolinks from labeled links", () => {
124+
const href = `buzz://message?channel=${CHANNEL}&id=${MESSAGE}`;
125+
126+
assert.deepEqual(resolveMessageLinkRenderTarget({ href, label: href }), {
127+
kind: "pill",
128+
link: {
129+
channelId: CHANNEL,
130+
messageId: MESSAGE,
131+
threadRootId: null,
132+
},
133+
});
134+
assert.deepEqual(resolveMessageLinkRenderTarget({ href, label: "message" }), {
135+
kind: "label",
136+
link: {
137+
channelId: CHANNEL,
138+
messageId: MESSAGE,
139+
threadRootId: null,
140+
},
141+
});
142+
assert.deepEqual(
143+
resolveMessageLinkRenderTarget({
144+
href: "https://example.com",
145+
label: href,
146+
}),
147+
{ kind: "none" },
148+
);
149+
});

desktop/src/features/messages/lib/messageLink.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,34 @@ export function isMessageLink(href: string | undefined | null): boolean {
102102
if (!href) return false;
103103
return href.startsWith("buzz://message?") || href === "buzz://message";
104104
}
105+
106+
type MessageLinkRenderInput = {
107+
href: string;
108+
label: string;
109+
};
110+
111+
export type MessageLinkRenderTarget =
112+
| { kind: "pill"; link: ParsedMessageLink }
113+
| { kind: "label"; link: ParsedMessageLink }
114+
| { kind: "none" };
115+
116+
/**
117+
* Centralizes how markdown-rendered anchors map to message-link UI. Both
118+
* CommonMark autolinks (`<buzz://message?...>`) and explicitly labeled links
119+
* arrive as anchors; autolinks have label === href and should render as pills,
120+
* while intentionally labeled links keep their label.
121+
*/
122+
export function resolveMessageLinkRenderTarget({
123+
href,
124+
label,
125+
}: MessageLinkRenderInput): MessageLinkRenderTarget {
126+
if (!isMessageLink(href)) return { kind: "none" };
127+
128+
const parsed = parseMessageLink(href);
129+
if (!parsed.ok) return { kind: "none" };
130+
131+
return {
132+
kind: label === href ? "pill" : "label",
133+
link: parsed.value,
134+
};
135+
}

desktop/src/shared/ui/markdown.test.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,11 @@ test("messageLinkUrlTransform: preserves buzz://message href", () => {
471471
assert.match(html, /href="buzz:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
472472
});
473473

474+
test("messageLinkUrlTransform: preserves buzz://message autolink href", () => {
475+
const html = renderMarkdown("<buzz://message?channel=abc&id=xyz>");
476+
assert.match(html, /href="buzz:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
477+
});
478+
474479
test("messageLinkUrlTransform: preserves buzz://message href with thread", () => {
475480
const html = renderMarkdown(
476481
"[link](buzz://message?channel=c1&id=m1&thread=t1)",

desktop/src/shared/ui/markdown.tsx

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
2323
import {
2424
isMessageLink,
2525
parseMessageLink,
26+
resolveMessageLinkRenderTarget,
2627
type ParsedMessageLink,
2728
} from "@/features/messages/lib/messageLink";
2829
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
@@ -79,6 +80,14 @@ type ImetaEntry = {
7980

8081
type ImetaLookup = Map<string, ImetaEntry>;
8182

83+
type MessageLinkPillProps = {
84+
channels: Channel[];
85+
href: string;
86+
interactive: boolean;
87+
link: ParsedMessageLink;
88+
onOpenMessageLink: (link: ParsedMessageLink) => void;
89+
};
90+
8291
let shikiHighlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
8392
null;
8493
let shikiInitPromise: Promise<void> | null = null;
@@ -1252,6 +1261,46 @@ function getCodeBlockText(children: React.ReactNode) {
12521261
return getReactNodeText(children).replace(/\n$/, "");
12531262
}
12541263

1264+
function MessageLinkPill({
1265+
channels,
1266+
href,
1267+
interactive,
1268+
link,
1269+
onOpenMessageLink,
1270+
}: MessageLinkPillProps) {
1271+
const channel = channels.find((c) => c.id === link.channelId);
1272+
const channelLabel = channel?.name ?? "channel";
1273+
const shortId = link.messageId.slice(0, 6);
1274+
const label = (
1275+
<>
1276+
#{channelLabel} · {shortId}
1277+
</>
1278+
);
1279+
1280+
if (!interactive) {
1281+
return <span data-message-link="">{label}</span>;
1282+
}
1283+
1284+
return (
1285+
<button
1286+
type="button"
1287+
data-message-link=""
1288+
aria-label={`Open message in ${channelLabel}`}
1289+
title={href}
1290+
className={cn(
1291+
"cursor-pointer",
1292+
MENTION_CHIP_BASE_CLASSES,
1293+
MENTION_CHIP_HOVER_CLASSES,
1294+
)}
1295+
onClick={() => {
1296+
onOpenMessageLink(link);
1297+
}}
1298+
>
1299+
{label}
1300+
</button>
1301+
);
1302+
}
1303+
12551304
function InlineEmojiPopover({
12561305
alt,
12571306
resolvedSrc,
@@ -1788,18 +1837,32 @@ function createMarkdownComponents(
17881837
// Intercept `buzz://message?channel=…&id=…` links so a click navigates
17891838
// in-app instead of opening the URL in the OS browser. http(s) links
17901839
// continue to use the existing target="_blank" behavior.
1791-
if (isMessageLink(href)) {
1792-
const parsed = parseMessageLink(href ?? "");
1793-
if (parsed.ok) {
1794-
const target = parsed.value;
1840+
if (href) {
1841+
const messageLinkTarget = resolveMessageLinkRenderTarget({
1842+
href,
1843+
label: getReactNodeText(children),
1844+
});
1845+
if (messageLinkTarget.kind !== "none") {
1846+
if (messageLinkTarget.kind === "pill") {
1847+
return (
1848+
<MessageLinkPill
1849+
channels={runtimeRef.current.channels}
1850+
href={href}
1851+
interactive={interactive}
1852+
link={messageLinkTarget.link}
1853+
onOpenMessageLink={onOpenMessageLink}
1854+
/>
1855+
);
1856+
}
1857+
17951858
return (
17961859
<a
17971860
{...props}
17981861
className="font-medium text-primary underline underline-offset-4 transition-colors hover:text-primary/80 cursor-pointer"
17991862
href={href}
18001863
onClick={(event) => {
18011864
event.preventDefault();
1802-
onOpenMessageLink(target);
1865+
onOpenMessageLink(messageLinkTarget.link);
18031866
}}
18041867
>
18051868
{children}
@@ -2095,36 +2158,14 @@ function createMarkdownComponents(
20952158
return <span data-message-link="">{href}</span>;
20962159
}
20972160

2098-
const { channelId, messageId } = parsed.value;
2099-
const channel = channels.find((c) => c.id === channelId);
2100-
const channelLabel = channel?.name ?? "channel";
2101-
const shortId = messageId.slice(0, 6);
2102-
2103-
if (!interactive) {
2104-
return (
2105-
<span data-message-link="">
2106-
#{channelLabel} · {shortId}
2107-
</span>
2108-
);
2109-
}
2110-
21112161
return (
2112-
<button
2113-
type="button"
2114-
data-message-link=""
2115-
aria-label={`Open message in ${channelLabel}`}
2116-
title={href}
2117-
className={cn(
2118-
"cursor-pointer",
2119-
MENTION_CHIP_BASE_CLASSES,
2120-
MENTION_CHIP_HOVER_CLASSES,
2121-
)}
2122-
onClick={() => {
2123-
onOpenMessageLink(parsed.value);
2124-
}}
2125-
>
2126-
#{channelLabel} · {shortId}
2127-
</button>
2162+
<MessageLinkPill
2163+
channels={channels}
2164+
href={href}
2165+
interactive={interactive}
2166+
link={parsed.value}
2167+
onOpenMessageLink={onOpenMessageLink}
2168+
/>
21282169
);
21292170
},
21302171
} as Components;

0 commit comments

Comments
 (0)