fix(desktop): support channel message path links (#5889)

## Summary

- accept `buzz://channel/<uuid>/<64-hex-event-id>` as a compatibility
message deep link
- activate the desktop window and route path-form message links through
the existing durable message-navigation queue
- support the same path form when rendered or pasted inside Buzz, while
canonicalizing composer output to `buzz://message?...`
- retain the existing one-segment channel-link behavior and reject
malformed event IDs or extra segments

## Context

Buzz Desktop 0.5.11 has no native `channel` route. The recently merged
channel-link handling on main recognizes `buzz://channel/<uuid>`, but
rejects the externally shared `<channel>/<event-id>` form before window
activation. On macOS that presents as Buzz taking the menu bar while its
window neither foregrounds nor navigates.

## Test plan

- `cargo test --manifest-path desktop/src-tauri/Cargo.toml
parse_channel_deep_link`
- focused channel-link, composer-link, and markdown unit tests
- `pnpm typecheck`
- mandatory pre-push hook: desktop checks, full desktop unit tests, and
Tauri/Rust checks

Installed-app external-open behavior requires a build containing this
change; 0.5.11 cannot exercise it because that release predates native
channel-link handling.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-14 18:25:23 +00:00
committed by GitHub
co-authored by Carl
parent 5ddf23d700
commit 207154706c
7 changed files with 152 additions and 20 deletions
+42 -4
View File
@@ -192,16 +192,32 @@ fn activate_main_window(app: &tauri::AppHandle) {
}
fn parse_channel_deep_link(url: &Url) -> Option<serde_json::Value> {
if url.query().is_some() || url.fragment().is_some() || !url.username().is_empty() {
if url.query().is_some()
|| url.fragment().is_some()
|| !url.username().is_empty()
|| url.password().is_some()
{
return None;
}
let mut segments = url.path_segments()?;
let channel_id = segments.next()?;
let message_id = segments.next();
if segments.next().is_some() {
return None;
}
let channel_id = uuid::Uuid::parse_str(channel_id).ok()?.to_string();
Some(serde_json::json!({ "channelId": channel_id }))
if message_id.is_some_and(|value| {
value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
}) {
return None;
}
Some(match message_id {
Some(message_id) => serde_json::json!({
"channelId": channel_id,
"messageId": message_id.to_ascii_lowercase(),
}),
None => serde_json::json!({ "channelId": channel_id }),
})
}
/// Parse the query string of a `buzz://message?…` URL into the JSON
@@ -456,8 +472,13 @@ pub(crate) fn handle_deep_link_url(app: &tauri::AppHandle, url_str: &str) {
return;
};
activate_main_window(app);
queue_navigation_deep_link(app, "channel", &payload);
let _ = app.emit("deep-link-channel", payload);
if payload["messageId"].is_string() {
queue_navigation_deep_link(app, "message", &payload);
let _ = app.emit("deep-link-message", payload);
} else {
queue_navigation_deep_link(app, "channel", &payload);
let _ = app.emit("deep-link-channel", payload);
}
}
Some("message") => {
// `buzz://message?channel=<uuid>&id=<eventId>[&thread=<rootId>]`
@@ -689,6 +710,18 @@ mod tests {
assert_eq!(payload["channelId"], "580ca78b-9dae-46f3-8854-bd671853ba32");
}
#[test]
fn parse_channel_deep_link_accepts_message_path() {
let message_id = "8455293f0123456789abcdef0123456789abcdef0123456789abcdef01234567";
let url = Url::parse(&format!(
"buzz://channel/a372f080-5961-4535-b1a3-edffface377d/{message_id}"
))
.unwrap();
let payload = parse_channel_deep_link(&url).unwrap();
assert_eq!(payload["channelId"], "a372f080-5961-4535-b1a3-edffface377d");
assert_eq!(payload["messageId"], message_id);
}
#[test]
fn parse_channel_deep_link_accepts_v7_and_normalizes_uppercase() {
for (raw, expected) in [
@@ -712,8 +745,13 @@ mod tests {
"buzz://channel",
"buzz://channel/",
"buzz://channel/one/two",
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/not-hex",
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/extra",
"buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32/",
"buzz://channel/one?extra=true",
"buzz://channel/one#fragment",
"buzz://:pass@channel/580ca78b-9dae-46f3-8854-bd671853ba32/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"buzz://channel/not-a-uuid",
"buzz://channel/%2F",
"buzz://channel/%00",
@@ -3,12 +3,23 @@ import test from "node:test";
import { isChannelLink, parseChannelLink } from "./channelLink.ts";
const CHANNEL_ID = "580ca78b-9dae-46f3-8854-bd671853ba32";
const MESSAGE_ID =
"8455293f0123456789abcdef0123456789abcdef0123456789abcdef01234567";
test("parseChannelLink accepts the canonical channel path", () => {
assert.deepEqual(parseChannelLink(`buzz://channel/${CHANNEL_ID}`), {
ok: true,
value: { channelId: CHANNEL_ID },
});
});
test("parseChannelLink accepts a channel message path", () => {
assert.deepEqual(
parseChannelLink("buzz://channel/580ca78b-9dae-46f3-8854-bd671853ba32"),
parseChannelLink(`buzz://channel/${CHANNEL_ID}/${MESSAGE_ID}`),
{
ok: true,
value: { channelId: "580ca78b-9dae-46f3-8854-bd671853ba32" },
value: { channelId: CHANNEL_ID, messageId: MESSAGE_ID },
},
);
});
@@ -35,6 +46,10 @@ test("parseChannelLink rejects malformed channel links", () => {
"buzz://channel",
"buzz://channel/",
"buzz://channel/one/two",
`buzz://channel/${CHANNEL_ID}/not-hex`,
`buzz://channel/${CHANNEL_ID}/${"a".repeat(63)}`,
`buzz://channel/${CHANNEL_ID}/${MESSAGE_ID}/extra`,
`buzz://channel/${CHANNEL_ID}/`,
"buzz://channel/one?extra=true",
"buzz://channel/one#fragment",
"https://channel/one",
@@ -1,11 +1,15 @@
/** `buzz://channel/<uuid>` link encoding and parsing. */
/** `buzz://channel/<uuid>[/<event-id>]` link encoding and parsing. */
const CHANNEL_LINK_SCHEME = "buzz:";
const CHANNEL_LINK_HOST = "channel";
const CHANNEL_UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
const EVENT_ID_PATTERN = /^[0-9a-f]{64}$/iu;
export type ParsedChannelLink = { channelId: string };
export type ParsedChannelLink = {
channelId: string;
messageId?: string;
};
export type ChannelLinkParseResult =
| { ok: true; value: ParsedChannelLink }
@@ -34,20 +38,31 @@ export function parseChannelLink(url: string): ChannelLinkParseResult {
if (parsed.search || parsed.hash || parsed.username || parsed.password) {
return { ok: false, reason: "unexpected-components" };
}
const segments = parsed.pathname.split("/").filter(Boolean);
if (segments.length !== 1) {
const segments = parsed.pathname.split("/").slice(1);
if (segments.length < 1 || segments.length > 2 || segments.includes("")) {
return { ok: false, reason: "missing-or-extra-channel" };
}
let channelId: string;
let messageId: string | null = null;
try {
channelId = decodeURIComponent(segments[0]);
messageId = segments[1] ? decodeURIComponent(segments[1]) : null;
} catch {
return { ok: false, reason: "invalid-channel-encoding" };
}
if (!CHANNEL_UUID_PATTERN.test(channelId)) {
return { ok: false, reason: "invalid-channel-uuid" };
}
return { ok: true, value: { channelId: channelId.toLowerCase() } };
if (messageId !== null && !EVENT_ID_PATTERN.test(messageId)) {
return { ok: false, reason: "invalid-message-id" };
}
return {
ok: true,
value: {
channelId: channelId.toLowerCase(),
...(messageId ? { messageId: messageId.toLowerCase() } : {}),
},
};
}
export function isChannelLink(href: string | undefined | null): boolean {
@@ -15,6 +15,8 @@ const CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const MESSAGE_ID = "root-event";
const HREF = `buzz://message?channel=${CHANNEL_ID}&id=${MESSAGE_ID}`;
const CHANNEL_HREF = `buzz://channel/${CHANNEL_ID}`;
const CHANNEL_MESSAGE_ID = "a".repeat(64);
const CHANNEL_MESSAGE_HREF = `buzz://channel/${CHANNEL_ID}/${CHANNEL_MESSAGE_ID}`;
const OWNER = "a".repeat(64);
const REPO_HREF = `buzz://repo?owner=${OWNER}&d=buzz-world`;
const ISSUE_ID = "b".repeat(64);
@@ -47,6 +49,15 @@ test("resolves channel and entity links as composer chips", () => {
),
{ channelName: "general", href: CHANNEL_HREF },
);
assert.deepEqual(
resolveComposerMessageLinkAttributes(CHANNEL_MESSAGE_HREF, (channelId) =>
channelId === CHANNEL_ID ? "general" : undefined,
),
{
channelName: "general",
href: `buzz://message?channel=${CHANNEL_ID}&id=${CHANNEL_MESSAGE_ID}`,
},
);
assert.deepEqual(
resolveComposerMessageLinkAttributes(REPO_HREF, () => undefined),
{ channelName: "", href: REPO_HREF },
@@ -66,7 +66,12 @@ export function resolveComposerMessageLinkAttributes(
if (channel.ok) {
return {
channelName: resolveChannelName(channel.value.channelId) ?? "",
href: buildChannelLink(channel.value.channelId),
href: channel.value.messageId
? buildMessageLink({
channelId: channel.value.channelId,
messageId: channel.value.messageId,
})
: buildChannelLink(channel.value.channelId),
};
}
+11 -4
View File
@@ -1063,9 +1063,11 @@ test("nudgeGuard_noSentinel_proseRenderedCardAbsent", () => {
test("bare Buzz permalinks render cohesive icon-prefixed chips", () => {
const channelId = "580ca78b-9dae-46f3-8854-bd671853ba32";
const messageLink = `buzz://message?channel=${channelId}&id=${EVENT_HEX}`;
const compatibilityMessageLink = `buzz://channel/${channelId}/${EVENT_HEX}`;
const channelLink = `buzz://channel/${channelId}`;
const links = [
messageLink,
compatibilityMessageLink,
channelLink,
`buzz://pr?id=${EVENT_HEX}&owner=${OWNER_HEX}&d=buzz-world`,
`buzz://issue?id=${EVENT_HEX}&owner=${OWNER_HEX}&d=buzz-world`,
@@ -1092,9 +1094,11 @@ test("bare Buzz permalinks render cohesive icon-prefixed chips", () => {
),
);
assert.equal((html.match(/data-buzz-link=""/g) ?? []).length, 5);
assert.match(html, /inline-chip-icon-message/);
assert.match(html, />engineering · c3b589fa</);
assert.equal((html.match(/data-buzz-link=""/g) ?? []).length, 6);
assert.equal((html.match(/inline-chip-icon-message/g) ?? []).length, 2);
assert.equal((html.match(/>engineering · c3b589fa</g) ?? []).length, 2);
assert.equal((html.match(/data-message-link=""/g) ?? []).length, 2);
assert.equal((html.match(/data-channel-deep-link=""/g) ?? []).length, 1);
assert.match(html, /inline-chip-icon-channel/);
assert.match(html, />engineering</);
assert.match(html, /inline-chip-icon-pr/);
@@ -1108,6 +1112,7 @@ test("authored Buzz permalink labels remain ordinary links", () => {
const channelId = "580ca78b-9dae-46f3-8854-bd671853ba32";
const links = [
`[the message](buzz://message?channel=${channelId}&id=${EVENT_HEX})`,
`[the compatibility message](buzz://channel/${channelId}/${EVENT_HEX})`,
`[**design discussion**](buzz://channel/${channelId})`,
`[the issue](buzz://issue?id=${EVENT_HEX}&owner=${OWNER_HEX}&d=buzz-world)`,
];
@@ -1134,11 +1139,13 @@ test("authored Buzz permalink labels remain ordinary links", () => {
assert.equal((html.match(/data-buzz-link=""/g) ?? []).length, 0);
assert.match(html, />the message</);
assert.match(html, />the compatibility message</);
assert.match(html, /aria-label="Open message: the compatibility message"/);
assert.match(html, />design discussion</);
assert.match(html, /aria-label="Open channel: design discussion"/);
assert.doesNotMatch(html, /\[object Object\]/);
assert.match(html, />the issue</);
assert.equal((html.match(/underline-offset-4/g) ?? []).length, 3);
assert.equal((html.match(/underline-offset-4/g) ?? []).length, 4);
});
test("bare Buzz permalinks shorten unavailable channel identifiers", () => {
@@ -6,6 +6,7 @@ import {
} from "@/features/messages/lib/channelLink";
import { BuzzInlineLink, BuzzLinkChip } from "./BuzzLinkChip";
import { MessageLinkPill } from "./MessageLinkPill";
import { useMarkdownRuntime } from "./runtimeContext";
import { getReactNodeText } from "./utils";
@@ -24,24 +25,46 @@ export function ChannelDeepLinkAnchor({
href,
interactive,
}: React.ComponentPropsWithoutRef<"a"> & { interactive: boolean }) {
const { channels, onOpenChannel } = useMarkdownRuntime();
const { channels, onOpenChannel, onOpenMessageLink } = useMarkdownRuntime();
if (!href) return <>{children}</>;
const parsed = parseChannelLink(href);
if (!parsed.ok) return <>{children}</>;
const messageLink = parsed.value.messageId
? {
channelId: parsed.value.channelId,
messageId: parsed.value.messageId,
threadRootId: null,
}
: null;
const openLink = () =>
messageLink
? onOpenMessageLink(messageLink)
: onOpenChannel(parsed.value.channelId);
const authoredLabel = getReactNodeText(children);
if (authoredLabel !== href) {
return (
<BuzzInlineLink
href={href}
title={href}
aria-label={`Open channel: ${authoredLabel}`}
aria-label={`${messageLink ? "Open message" : "Open channel"}: ${authoredLabel}`}
interactive={interactive}
onOpenLink={() => onOpenChannel(parsed.value.channelId)}
onOpenLink={openLink}
>
{children}
</BuzzInlineLink>
);
}
if (messageLink) {
return (
<MessageLinkPill
channels={channels}
href={href}
interactive={interactive}
link={messageLink}
onOpenMessageLink={onOpenMessageLink}
/>
);
}
const label = channelPermalinkLabel(channels, parsed.value.channelId);
return (
<BuzzLinkChip
@@ -64,10 +87,28 @@ export function MarkdownChannelDeepLink({
children?: React.ReactNode;
interactive: boolean;
}) {
const { channels, onOpenChannel } = useMarkdownRuntime();
const { channels, onOpenChannel, onOpenMessageLink } = useMarkdownRuntime();
const href = String(children ?? "");
const parsed = parseChannelLink(href);
if (!parsed.ok) return <span data-channel-deep-link="">{href}</span>;
const messageLink = parsed.value.messageId
? {
channelId: parsed.value.channelId,
messageId: parsed.value.messageId,
threadRootId: null,
}
: null;
if (messageLink) {
return (
<MessageLinkPill
channels={channels}
href={href}
interactive={interactive}
link={messageLink}
onOpenMessageLink={onOpenMessageLink}
/>
);
}
const label = channelPermalinkLabel(channels, parsed.value.channelId);
return (
<BuzzLinkChip