mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
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>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
buildMessageLink,
|
||||
isMessageLink,
|
||||
parseMessageLink,
|
||||
resolveMessageLinkRenderTarget,
|
||||
} from "./messageLink.ts";
|
||||
|
||||
const CHANNEL = "f570339f-8f8a-4e08-a779-8d954aa44109";
|
||||
@@ -118,3 +119,31 @@ test("isMessageLink matches buzz://message and legacy buzz://message", () => {
|
||||
assert.equal(isMessageLink(undefined), false);
|
||||
assert.equal(isMessageLink(""), false);
|
||||
});
|
||||
|
||||
test("resolveMessageLinkRenderTarget distinguishes autolinks from labeled links", () => {
|
||||
const href = `buzz://message?channel=${CHANNEL}&id=${MESSAGE}`;
|
||||
|
||||
assert.deepEqual(resolveMessageLinkRenderTarget({ href, label: href }), {
|
||||
kind: "pill",
|
||||
link: {
|
||||
channelId: CHANNEL,
|
||||
messageId: MESSAGE,
|
||||
threadRootId: null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(resolveMessageLinkRenderTarget({ href, label: "message" }), {
|
||||
kind: "label",
|
||||
link: {
|
||||
channelId: CHANNEL,
|
||||
messageId: MESSAGE,
|
||||
threadRootId: null,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
resolveMessageLinkRenderTarget({
|
||||
href: "https://example.com",
|
||||
label: href,
|
||||
}),
|
||||
{ kind: "none" },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -102,3 +102,34 @@ export function isMessageLink(href: string | undefined | null): boolean {
|
||||
if (!href) return false;
|
||||
return href.startsWith("buzz://message?") || href === "buzz://message";
|
||||
}
|
||||
|
||||
type MessageLinkRenderInput = {
|
||||
href: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type MessageLinkRenderTarget =
|
||||
| { kind: "pill"; link: ParsedMessageLink }
|
||||
| { kind: "label"; link: ParsedMessageLink }
|
||||
| { kind: "none" };
|
||||
|
||||
/**
|
||||
* Centralizes how markdown-rendered anchors map to message-link UI. Both
|
||||
* CommonMark autolinks (`<buzz://message?...>`) and explicitly labeled links
|
||||
* arrive as anchors; autolinks have label === href and should render as pills,
|
||||
* while intentionally labeled links keep their label.
|
||||
*/
|
||||
export function resolveMessageLinkRenderTarget({
|
||||
href,
|
||||
label,
|
||||
}: MessageLinkRenderInput): MessageLinkRenderTarget {
|
||||
if (!isMessageLink(href)) return { kind: "none" };
|
||||
|
||||
const parsed = parseMessageLink(href);
|
||||
if (!parsed.ok) return { kind: "none" };
|
||||
|
||||
return {
|
||||
kind: label === href ? "pill" : "label",
|
||||
link: parsed.value,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -471,6 +471,11 @@ test("messageLinkUrlTransform: preserves buzz://message href", () => {
|
||||
assert.match(html, /href="buzz:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
|
||||
});
|
||||
|
||||
test("messageLinkUrlTransform: preserves buzz://message autolink href", () => {
|
||||
const html = renderMarkdown("<buzz://message?channel=abc&id=xyz>");
|
||||
assert.match(html, /href="buzz:\/\/message\?channel=abc&(?:amp;)?id=xyz"/);
|
||||
});
|
||||
|
||||
test("messageLinkUrlTransform: preserves buzz://message href with thread", () => {
|
||||
const html = renderMarkdown(
|
||||
"[link](buzz://message?channel=c1&id=m1&thread=t1)",
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import {
|
||||
isMessageLink,
|
||||
parseMessageLink,
|
||||
resolveMessageLinkRenderTarget,
|
||||
type ParsedMessageLink,
|
||||
} from "@/features/messages/lib/messageLink";
|
||||
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
@@ -79,6 +80,14 @@ type ImetaEntry = {
|
||||
|
||||
type ImetaLookup = Map<string, ImetaEntry>;
|
||||
|
||||
type MessageLinkPillProps = {
|
||||
channels: Channel[];
|
||||
href: string;
|
||||
interactive: boolean;
|
||||
link: ParsedMessageLink;
|
||||
onOpenMessageLink: (link: ParsedMessageLink) => void;
|
||||
};
|
||||
|
||||
let shikiHighlighter: HighlighterGeneric<BundledLanguage, BundledTheme> | null =
|
||||
null;
|
||||
let shikiInitPromise: Promise<void> | null = null;
|
||||
@@ -1252,6 +1261,46 @@ function getCodeBlockText(children: React.ReactNode) {
|
||||
return getReactNodeText(children).replace(/\n$/, "");
|
||||
}
|
||||
|
||||
function MessageLinkPill({
|
||||
channels,
|
||||
href,
|
||||
interactive,
|
||||
link,
|
||||
onOpenMessageLink,
|
||||
}: MessageLinkPillProps) {
|
||||
const channel = channels.find((c) => c.id === link.channelId);
|
||||
const channelLabel = channel?.name ?? "channel";
|
||||
const shortId = link.messageId.slice(0, 6);
|
||||
const label = (
|
||||
<>
|
||||
#{channelLabel} · {shortId}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!interactive) {
|
||||
return <span data-message-link="">{label}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-message-link=""
|
||||
aria-label={`Open message in ${channelLabel}`}
|
||||
title={href}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
MENTION_CHIP_BASE_CLASSES,
|
||||
MENTION_CHIP_HOVER_CLASSES,
|
||||
)}
|
||||
onClick={() => {
|
||||
onOpenMessageLink(link);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineEmojiPopover({
|
||||
alt,
|
||||
resolvedSrc,
|
||||
@@ -1788,10 +1837,24 @@ function createMarkdownComponents(
|
||||
// Intercept `buzz://message?channel=…&id=…` links so a click navigates
|
||||
// in-app instead of opening the URL in the OS browser. http(s) links
|
||||
// continue to use the existing target="_blank" behavior.
|
||||
if (isMessageLink(href)) {
|
||||
const parsed = parseMessageLink(href ?? "");
|
||||
if (parsed.ok) {
|
||||
const target = parsed.value;
|
||||
if (href) {
|
||||
const messageLinkTarget = resolveMessageLinkRenderTarget({
|
||||
href,
|
||||
label: getReactNodeText(children),
|
||||
});
|
||||
if (messageLinkTarget.kind !== "none") {
|
||||
if (messageLinkTarget.kind === "pill") {
|
||||
return (
|
||||
<MessageLinkPill
|
||||
channels={runtimeRef.current.channels}
|
||||
href={href}
|
||||
interactive={interactive}
|
||||
link={messageLinkTarget.link}
|
||||
onOpenMessageLink={onOpenMessageLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
{...props}
|
||||
@@ -1799,7 +1862,7 @@ function createMarkdownComponents(
|
||||
href={href}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
onOpenMessageLink(target);
|
||||
onOpenMessageLink(messageLinkTarget.link);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -2095,36 +2158,14 @@ function createMarkdownComponents(
|
||||
return <span data-message-link="">{href}</span>;
|
||||
}
|
||||
|
||||
const { channelId, messageId } = parsed.value;
|
||||
const channel = channels.find((c) => c.id === channelId);
|
||||
const channelLabel = channel?.name ?? "channel";
|
||||
const shortId = messageId.slice(0, 6);
|
||||
|
||||
if (!interactive) {
|
||||
return (
|
||||
<span data-message-link="">
|
||||
#{channelLabel} · {shortId}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-message-link=""
|
||||
aria-label={`Open message in ${channelLabel}`}
|
||||
title={href}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
MENTION_CHIP_BASE_CLASSES,
|
||||
MENTION_CHIP_HOVER_CLASSES,
|
||||
)}
|
||||
onClick={() => {
|
||||
onOpenMessageLink(parsed.value);
|
||||
}}
|
||||
>
|
||||
#{channelLabel} · {shortId}
|
||||
</button>
|
||||
<MessageLinkPill
|
||||
channels={channels}
|
||||
href={href}
|
||||
interactive={interactive}
|
||||
link={parsed.value}
|
||||
onOpenMessageLink={onOpenMessageLink}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} as Components;
|
||||
|
||||
Reference in New Issue
Block a user