mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
fix(composer): preserve completed preview sends
Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
parent
8c2e0b6c92
commit
975323e2e8
@@ -113,36 +113,12 @@ export function useLinkEditor(richText: UseRichTextEditorResult) {
|
||||
[richText.editor],
|
||||
);
|
||||
|
||||
const showCard = React.useCallback(
|
||||
(info: LinkSelectionInfo | null) => {
|
||||
if (!info) {
|
||||
setCardState(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = getLinkRect(info);
|
||||
if (!rect) {
|
||||
setCardState(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setCardState((prev) => {
|
||||
const sameLink =
|
||||
prev?.info.href === info.href &&
|
||||
prev.info.from === info.from &&
|
||||
prev.info.to === info.to &&
|
||||
prev.info.text === info.text;
|
||||
const sameRect =
|
||||
prev?.rect.left === rect.left &&
|
||||
prev.rect.top === rect.top &&
|
||||
prev.rect.width === rect.width &&
|
||||
prev.rect.height === rect.height;
|
||||
if (sameLink && sameRect) return prev;
|
||||
return { info, rect };
|
||||
});
|
||||
},
|
||||
[getLinkRect],
|
||||
);
|
||||
const showCard = React.useCallback((_info: LinkSelectionInfo | null) => {
|
||||
// Keep clicks and caret movement inside composer links focused in the
|
||||
// editor without opening contextual controls. Link editing remains
|
||||
// available through the formatting toolbar and Cmd/Ctrl+K.
|
||||
setCardState(null);
|
||||
}, []);
|
||||
|
||||
const openDialogFromInfo = React.useCallback((info: LinkSelectionInfo) => {
|
||||
setDraft({
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { parseLinkPreviewSnapshots } from "./linkPreviewSnapshot.ts";
|
||||
import {
|
||||
buildLinkPreviewSnapshotTag,
|
||||
parseLinkPreviewSnapshots,
|
||||
} from "./linkPreviewSnapshot.ts";
|
||||
|
||||
const HASH = "a".repeat(64);
|
||||
const ORIGIN = "https://relay.example";
|
||||
@@ -55,6 +58,31 @@ test("authored snapshots reject remote, malformed, credentialed, and hash-mismat
|
||||
);
|
||||
});
|
||||
|
||||
test("snapshot tags sanitize control characters and enforce UTF-8 byte limits", () => {
|
||||
const tag = buildLinkPreviewSnapshotTag({
|
||||
canonicalUrl: URL,
|
||||
title: `Title\n${"😀".repeat(100)}`,
|
||||
siteName: `Linear\u0000${"é".repeat(100)}`,
|
||||
description: `First\nSecond${"😀".repeat(300)}`,
|
||||
imageUrl: "",
|
||||
imageSha256: "",
|
||||
faviconUrl: "",
|
||||
faviconSha256: "",
|
||||
});
|
||||
|
||||
for (const value of tag.slice(4, 7)) {
|
||||
assert.ok(
|
||||
Array.from(value).every((char) => {
|
||||
const code = char.charCodeAt(0);
|
||||
return code > 0x1f && code !== 0x7f;
|
||||
}),
|
||||
);
|
||||
}
|
||||
assert.ok(Buffer.byteLength(tag[4], "utf8") <= 300);
|
||||
assert.ok(Buffer.byteLength(tag[5], "utf8") <= 100);
|
||||
assert.ok(Buffer.byteLength(tag[6], "utf8") <= 1000);
|
||||
});
|
||||
|
||||
test("messages without authored snapshots never create recipient previews", () => {
|
||||
assert.deepEqual(parseLinkPreviewSnapshots([], CONTENT, ORIGIN), []);
|
||||
assert.deepEqual(parseLinkPreviewSnapshots(undefined, CONTENT, ORIGIN), []);
|
||||
|
||||
@@ -20,6 +20,24 @@ export type LinkPreviewSnapshot = {
|
||||
faviconSha256: string;
|
||||
};
|
||||
|
||||
function isControlCharacter(char: string): boolean {
|
||||
const code = char.charCodeAt(0);
|
||||
return code <= 0x1f || code === 0x7f;
|
||||
}
|
||||
|
||||
function sanitizeSnapshotText(value: string, maxBytes: number): string {
|
||||
let result = "";
|
||||
let byteLength = 0;
|
||||
for (const rawChar of value) {
|
||||
const char = isControlCharacter(rawChar) ? " " : rawChar;
|
||||
const charBytes = new TextEncoder().encode(char).length;
|
||||
if (byteLength + charBytes > maxBytes) break;
|
||||
result += char;
|
||||
byteLength += charBytes;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function validText(value: string, max: number): boolean {
|
||||
return (
|
||||
value.length <= max &&
|
||||
@@ -132,9 +150,9 @@ export function buildLinkPreviewSnapshotTag(
|
||||
"snapshot",
|
||||
LINK_PREVIEW_SNAPSHOT_VERSION,
|
||||
snapshot.canonicalUrl,
|
||||
snapshot.title,
|
||||
snapshot.siteName,
|
||||
snapshot.description,
|
||||
sanitizeSnapshotText(snapshot.title, 300),
|
||||
sanitizeSnapshotText(snapshot.siteName, 100),
|
||||
sanitizeSnapshotText(snapshot.description, 1000),
|
||||
snapshot.imageUrl,
|
||||
snapshot.imageSha256,
|
||||
snapshot.faviconUrl,
|
||||
|
||||
@@ -61,10 +61,11 @@ test("⌘K with caret inside an existing link opens the edit-link dialog", async
|
||||
"docs",
|
||||
);
|
||||
|
||||
// Click into the linked text to place the caret inside it, then re-trigger
|
||||
// the shortcut. (The click also surfaces the composer link hover card —
|
||||
// ⌘K must open the full dialog from that state.)
|
||||
// Click into the linked text to place the caret inside it. This must not
|
||||
// surface contextual controls; editing stays accessible through ⌘K.
|
||||
await input.locator('a[href="https://example.com"]').click();
|
||||
await expect(page.getByRole("button", { name: "Edit link" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "Unlink" })).toHaveCount(0);
|
||||
await page.keyboard.press("ControlOrMeta+k");
|
||||
|
||||
const editDialog = page.getByRole("dialog", { name: "Edit link" });
|
||||
|
||||
@@ -472,6 +472,36 @@ test("link preview style defaults to compact and Rich unfurls descriptions", asy
|
||||
await page.getByTestId("link-preview-style-compact").click();
|
||||
});
|
||||
|
||||
test("completed link preview sends once and leaves the composer cleared", async ({
|
||||
page,
|
||||
}) => {
|
||||
const previewUrl = "https://github.com/block/buzz/pull/3246?send=complete";
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await page.getByTestId("message-input").fill(previewUrl);
|
||||
await waitForReadyComposerSnapshots(page);
|
||||
|
||||
const send = page.getByTestId("send-message");
|
||||
await expect(send).toBeEnabled();
|
||||
await send.click();
|
||||
await expect(page.getByTestId("message-input")).toHaveText("");
|
||||
await expect(page.locator("[data-composer-link-previews]")).toHaveCount(0);
|
||||
await page.waitForTimeout(250);
|
||||
await expect(page.getByTestId("message-input")).toHaveText("");
|
||||
|
||||
const calls = await page.evaluate(() =>
|
||||
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
||||
(entry) => entry.command === "send_channel_message",
|
||||
),
|
||||
);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(
|
||||
(
|
||||
calls[0]?.payload as { linkPreviewTags?: string[][] | null } | undefined
|
||||
)?.linkPreviewTags?.[0]?.slice(0, 4),
|
||||
).toEqual(["link-preview", "snapshot", "1", previewUrl]);
|
||||
});
|
||||
|
||||
test("send does not wait for a pending link preview snapshot", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user