mirror of
https://github.com/block/buzz.git
synced 2026-10-30 05:51:46 +00:00
fix(desktop): polish live Bestie feed cards
Co-authored-by: Arjun Mahanti <arjun@squareup.com> Signed-off-by: Arjun Mahanti <arjun@squareup.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
bestieItemSourceLabel,
|
||||
bestieItemSummary,
|
||||
filterBestieFeedItems,
|
||||
getVisibleBestieFeedItems,
|
||||
reduceBestiePanelState,
|
||||
@@ -96,6 +97,35 @@ test("bestieItemSourceLabel uses only source metadata with a neutral fallback",
|
||||
);
|
||||
});
|
||||
|
||||
test("bestieItemSummary extracts a concise headline from actual Buzz text", () => {
|
||||
const actualContent =
|
||||
"Reminder: Review the launch plan before lunch. This second sentence contains implementation details that do not belong in the card headline.";
|
||||
assert.equal(
|
||||
bestieItemSummary(
|
||||
item({
|
||||
groupItems: [{ ...item().item, content: actualContent }],
|
||||
preview: actualContent,
|
||||
subject: "Mention",
|
||||
}),
|
||||
),
|
||||
"Review the launch plan before lunch.",
|
||||
);
|
||||
});
|
||||
|
||||
test("bestieItemSummary strips media and bounds long source text", () => {
|
||||
const summary = bestieItemSummary(
|
||||
item({
|
||||
preview:
|
||||
"@arjun Here is the important source update with enough additional context to exceed the compact Bestie headline limit by a wide margin ",
|
||||
subject: "Mention",
|
||||
}),
|
||||
);
|
||||
assert.ok(summary.startsWith("Here is the important source update"));
|
||||
assert.ok(summary.endsWith("…"));
|
||||
assert.ok(Array.from(summary).length <= 96);
|
||||
assert.doesNotMatch(summary, /proof|https/);
|
||||
});
|
||||
|
||||
test("reduceBestiePanelState keeps only one floating panel open", () => {
|
||||
const reply = reduceBestiePanelState(
|
||||
{ mode: "closed" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isProjectInboxItem } from "@/features/home/lib/projectInbox";
|
||||
import type { InboxItem } from "@/features/home/lib/inbox";
|
||||
import { summarizeMessageLinkContent } from "@/features/messages/lib/messageLinkMetadata";
|
||||
|
||||
export type BestieFeedFilter = "all" | "messages" | "tasks";
|
||||
|
||||
@@ -60,6 +61,41 @@ export function bestieItemSourceLabel(item: InboxItem) {
|
||||
return item.channelLabel ?? (item.item.channelName.trim() || "Buzz");
|
||||
}
|
||||
|
||||
const BESTIE_SUMMARY_MAX_LENGTH = 96;
|
||||
|
||||
function truncateBestieSummary(value: string) {
|
||||
const characters = Array.from(value);
|
||||
if (characters.length <= BESTIE_SUMMARY_MAX_LENGTH) return value;
|
||||
|
||||
const clipped = characters.slice(0, BESTIE_SUMMARY_MAX_LENGTH - 1).join("");
|
||||
const lastSpace = clipped.lastIndexOf(" ");
|
||||
const summary = lastSpace > 56 ? clipped.slice(0, lastSpace) : clipped;
|
||||
return `${summary.trimEnd()}…`;
|
||||
}
|
||||
|
||||
/** Builds a concise extractive headline using only text from the Buzz item. */
|
||||
export function bestieItemSummary(item: InboxItem) {
|
||||
if (isProjectInboxItem(item.item)) return item.subject;
|
||||
|
||||
const latestContent = [...item.groupItems]
|
||||
.sort((left, right) => right.createdAt - left.createdAt)
|
||||
.find((event) => event.content.trim().length > 0)?.content;
|
||||
const normalized = summarizeMessageLinkContent(latestContent ?? item.preview)
|
||||
.replace(/^(?:agent progress|mention|reminder)\s*:\s*/i, "")
|
||||
.replace(/^@[\p{L}\p{N}._-]+[:,]?\s*/u, "")
|
||||
.trim();
|
||||
if (!normalized || normalized === "No message text") return item.subject;
|
||||
|
||||
const sentenceEnd = normalized.search(/[.!?](?:\s|$)/);
|
||||
const firstSentence =
|
||||
sentenceEnd >= 0 ? normalized.slice(0, sentenceEnd + 1).trim() : normalized;
|
||||
const [firstCharacter = "", ...remainingCharacters] =
|
||||
Array.from(firstSentence);
|
||||
return truncateBestieSummary(
|
||||
`${firstCharacter.toLocaleUpperCase()}${remainingCharacters.join("")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function reduceBestiePanelState(
|
||||
_state: BestiePanelState,
|
||||
action: BestiePanelAction,
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
import {
|
||||
Bot,
|
||||
Clock3,
|
||||
GitPullRequest,
|
||||
MessageSquareReply,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import { Clock3, MessageSquareReply, Sparkles } from "lucide-react";
|
||||
|
||||
import { bestieItemSourceLabel } from "@/features/bestie/lib/bestieFeed";
|
||||
import { bestieItemSummary } from "@/features/bestie/lib/bestieFeed";
|
||||
import type { InboxItem } from "@/features/home/lib/inbox";
|
||||
import { isProjectInboxItem } from "@/features/home/lib/projectInbox";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown";
|
||||
|
||||
type BestieFeedCardProps = {
|
||||
item: InboxItem;
|
||||
@@ -20,12 +16,12 @@ type BestieFeedCardProps = {
|
||||
onSnooze: () => void;
|
||||
};
|
||||
|
||||
function actualSupportingContent(item: InboxItem) {
|
||||
function actualSupportingItems(item: InboxItem) {
|
||||
const seen = new Set<string>();
|
||||
return [...item.groupItems]
|
||||
.sort((left, right) => left.createdAt - right.createdAt)
|
||||
.map((event) => event.content.trim())
|
||||
.filter((content) => {
|
||||
.filter((event) => {
|
||||
const content = event.content.trim();
|
||||
if (!content || seen.has(content)) return false;
|
||||
seen.add(content);
|
||||
return true;
|
||||
@@ -34,42 +30,31 @@ function actualSupportingContent(item: InboxItem) {
|
||||
}
|
||||
|
||||
function SupportingVisual({ item }: { item: InboxItem }) {
|
||||
const content = actualSupportingContent(item);
|
||||
const projectItem = isProjectInboxItem(item.item);
|
||||
const sourceItems = actualSupportingItems(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-2xl border border-border/60 bg-muted/25 p-4"
|
||||
data-testid="bestie-source-visual"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{projectItem ? (
|
||||
<GitPullRequest className="h-4 w-4 text-violet-500" />
|
||||
) : item.categories.includes("agent_activity") ? (
|
||||
<Bot className="h-4 w-4" />
|
||||
) : (
|
||||
<MessageSquareReply className="h-4 w-4" />
|
||||
)}
|
||||
<span className="font-medium text-foreground">
|
||||
{bestieItemSourceLabel(item)}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{item.categoryLabel}</span>
|
||||
{item.unreadCount > 0 ? <span>· {item.unreadCount} unread</span> : null}
|
||||
</div>
|
||||
{content.length > 0 ? (
|
||||
<div className="mt-3 flex flex-col items-start gap-1.5">
|
||||
{content.map((message) => (
|
||||
{sourceItems.length > 0 ? (
|
||||
<div className="flex flex-col items-start gap-1.5">
|
||||
{sourceItems.map((sourceItem) => (
|
||||
<div
|
||||
className="max-w-full rounded-2xl bg-muted/70 px-4 py-2.5 text-sm leading-relaxed text-foreground"
|
||||
key={message}
|
||||
className="max-w-full overflow-hidden rounded-2xl bg-muted/70 px-4 py-2.5"
|
||||
key={sourceItem.id}
|
||||
>
|
||||
{message}
|
||||
<VideoReviewCommentMarkdown
|
||||
className="max-w-full text-sm leading-relaxed text-foreground [&_p]:line-clamp-4"
|
||||
content={sourceItem.content}
|
||||
imetaByUrl={parseImetaTags(sourceItem.tags)}
|
||||
messageId={sourceItem.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-3 text-sm text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This Buzz event has no text content.
|
||||
</p>
|
||||
)}
|
||||
@@ -115,6 +100,7 @@ export function BestieFeedCard({
|
||||
onSnooze,
|
||||
}: BestieFeedCardProps) {
|
||||
const headingId = `bestie-card-heading-${item.id}`;
|
||||
const summary = bestieItemSummary(item);
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -147,37 +133,21 @@ export function BestieFeedCard({
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex items-center gap-3">
|
||||
{item.avatarUrl ? (
|
||||
<img
|
||||
alt=""
|
||||
className="h-11 w-11 shrink-0 rounded-2xl object-cover"
|
||||
src={item.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl bg-foreground text-xs font-semibold text-background"
|
||||
>
|
||||
{item.senderLabel.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{item.senderLabel}</p>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{bestieItemSourceLabel(item)}
|
||||
</p>
|
||||
</div>
|
||||
<UserAvatar
|
||||
avatarUrl={item.avatarUrl}
|
||||
className="h-11 w-11 shrink-0"
|
||||
displayName={item.senderLabel}
|
||||
fallbackDelayMs={0}
|
||||
testId={`bestie-avatar-${item.id}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2
|
||||
className="mt-5 text-balance text-xl font-semibold leading-tight tracking-tight sm:text-2xl"
|
||||
id={headingId}
|
||||
>
|
||||
{item.subject}
|
||||
{summary}
|
||||
</h2>
|
||||
<p className="mt-2 max-w-2xl whitespace-pre-wrap text-sm leading-relaxed text-muted-foreground">
|
||||
{item.preview}
|
||||
</p>
|
||||
|
||||
<div className="mt-5">
|
||||
<SupportingVisual item={item} />
|
||||
@@ -186,14 +156,14 @@ export function BestieFeedCard({
|
||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<IconAction
|
||||
label={`Snooze ${item.subject}`}
|
||||
label={`Snooze ${summary}`}
|
||||
onClick={onSnooze}
|
||||
testId={`bestie-snooze-${item.id}`}
|
||||
>
|
||||
<Clock3 />
|
||||
</IconAction>
|
||||
<IconAction
|
||||
label={`Reply about ${item.subject}`}
|
||||
label={`Reply about ${summary}`}
|
||||
onClick={onReply}
|
||||
testId={`bestie-reply-${item.id}`}
|
||||
>
|
||||
|
||||
@@ -5,12 +5,34 @@ import { installMockBridge } from "../helpers/bridge";
|
||||
|
||||
const LIVE_MENTION = "Please review the release checklist.";
|
||||
const LIVE_REMINDER = "Reminder: update the launch plan before lunch.";
|
||||
const MEDIA_SUMMARY = "The design reference is ready for review.";
|
||||
|
||||
test("Bestie Feed projects only events returned by the Home feed", async ({
|
||||
page,
|
||||
}) => {
|
||||
await installMockBridge(page);
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(
|
||||
() => typeof window.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function",
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
window.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?.({
|
||||
id: "bestie-media-evidence",
|
||||
kind: 9,
|
||||
pubkey: "a".repeat(64),
|
||||
content:
|
||||
"The design reference is ready for review. This additional implementation detail should remain supporting evidence rather than becoming the headline.\n\n",
|
||||
created_at: Math.floor(Date.now() / 1_000) + 1,
|
||||
channel_id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
|
||||
channel_name: "general",
|
||||
channel_type: "stream",
|
||||
tags: [
|
||||
["h", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"],
|
||||
["p", "deadbeef".repeat(8)],
|
||||
],
|
||||
category: "mention",
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByTestId("open-bestie-feed").click();
|
||||
await expect(page).toHaveURL(/#\/\?view=bestie$/);
|
||||
@@ -24,6 +46,15 @@ test("Bestie Feed projects only events returned by the Home feed", async ({
|
||||
await expect(
|
||||
page.getByText(LIVE_REMINDER, { exact: true }).first(),
|
||||
).toBeVisible();
|
||||
const mediaCard = page
|
||||
.getByRole("heading", { name: MEDIA_SUMMARY, exact: true })
|
||||
.locator("xpath=ancestor::article");
|
||||
await expect(mediaCard).toBeVisible();
|
||||
await expect(
|
||||
mediaCard.getByRole("button", {
|
||||
name: "Zoom image: Reference image",
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/Simon asked for the latest/)).toHaveCount(0);
|
||||
await expect(page.getByText(/Cmd\+K is ready/)).toHaveCount(0);
|
||||
|
||||
@@ -38,8 +69,20 @@ test("Bestie Feed projects only events returned by the Home feed", async ({
|
||||
.getByText(LIVE_MENTION, { exact: true })
|
||||
.first()
|
||||
.locator("xpath=ancestor::article");
|
||||
await expect(
|
||||
mentionCard.getByRole("heading", { name: LIVE_MENTION, exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(mentionCard.getByText("Alice", { exact: true })).toHaveCount(0);
|
||||
await expect(mentionCard.getByText("general", { exact: true })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(
|
||||
mentionCard.locator(
|
||||
'[data-testid^="bestie-avatar-"][data-testid$="-fallback"]',
|
||||
),
|
||||
).toBeVisible();
|
||||
const snoozeTrigger = mentionCard.getByRole("button", {
|
||||
name: /Snooze Mention/,
|
||||
name: `Snooze ${LIVE_MENTION}`,
|
||||
});
|
||||
await snoozeTrigger.click();
|
||||
await expect(page.getByText(LIVE_MENTION, { exact: true })).toHaveCount(0);
|
||||
@@ -52,7 +95,7 @@ test("Bestie Feed projects only events returned by the Home feed", async ({
|
||||
.getByText(LIVE_MENTION, { exact: true })
|
||||
.first()
|
||||
.locator("xpath=ancestor::article")
|
||||
.getByRole("button", { name: /Reply about Mention/ });
|
||||
.getByRole("button", { name: `Reply about ${LIVE_MENTION}` });
|
||||
await replyTrigger.click();
|
||||
const replyPanel = page.getByTestId("bestie-reply-panel");
|
||||
await expect(replyPanel).toContainText(LIVE_MENTION);
|
||||
|
||||
Reference in New Issue
Block a user