mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Simplify the desktop Home feed (#52)
This commit is contained in:
@@ -126,9 +126,6 @@ export function AppShell() {
|
||||
activeChannel,
|
||||
identityQuery.data,
|
||||
);
|
||||
const homeUrgentCount =
|
||||
(homeFeedQuery.data?.feed.mentions.length ?? 0) +
|
||||
(homeFeedQuery.data?.feed.needsAction.length ?? 0);
|
||||
const availableChannelIds = React.useMemo(
|
||||
() => new Set(channels.map((channel) => channel.id)),
|
||||
[channels],
|
||||
@@ -411,7 +408,6 @@ export function AppShell() {
|
||||
: undefined
|
||||
}
|
||||
fallbackDisplayName={identityQuery.data?.displayName}
|
||||
homeUrgentCount={homeUrgentCount}
|
||||
isCreatingChannel={createChannelMutation.isPending}
|
||||
isLoading={channelsQuery.isLoading}
|
||||
selfPresenceStatus={presenceSession.currentStatus}
|
||||
@@ -511,7 +507,6 @@ export function AppShell() {
|
||||
}
|
||||
feed={homeFeedQuery.data}
|
||||
isLoading={homeFeedQuery.isLoading}
|
||||
isRefreshing={homeFeedQuery.isRefetching}
|
||||
onOpenChannel={handleOpenChannel}
|
||||
onRefresh={() => {
|
||||
void homeFeedQuery.refetch();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getHomeFeed } from "@/shared/api/tauri";
|
||||
export function useHomeFeedQuery() {
|
||||
return useQuery({
|
||||
queryKey: ["home-feed"],
|
||||
queryFn: () => getHomeFeed({ limit: 12 }),
|
||||
queryFn: () => getHomeFeed({ limit: 12, types: "mentions,needs_action" }),
|
||||
staleTime: 15_000,
|
||||
gcTime: 5 * 60 * 1_000,
|
||||
refetchInterval: 30_000,
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Activity,
|
||||
AtSign,
|
||||
Bot,
|
||||
CircleAlert,
|
||||
RefreshCcw,
|
||||
Sparkles,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { AtSign, CircleAlert, RefreshCcw, type LucideIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
resolveUserLabel,
|
||||
@@ -14,7 +6,6 @@ import {
|
||||
} from "@/features/profile/lib/identity";
|
||||
import { useUsersBatchQuery } from "@/features/profile/hooks";
|
||||
import type { FeedItem, HomeFeedResponse } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
@@ -54,13 +45,6 @@ function formatRelativeTime(unixSeconds: number) {
|
||||
}).format(new Date(unixSeconds * 1_000));
|
||||
}
|
||||
|
||||
function formatUpdatedAt(unixSeconds: number) {
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}).format(new Date(unixSeconds * 1_000));
|
||||
}
|
||||
|
||||
function feedHeadline(item: FeedItem) {
|
||||
switch (item.kind) {
|
||||
case 40007:
|
||||
@@ -257,65 +241,17 @@ function FeedSection({
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryCard({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
tone,
|
||||
}: {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: LucideIcon;
|
||||
tone: "urgent" | "calm";
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
tone === "urgent"
|
||||
? "border-primary/20 bg-primary/10"
|
||||
: "border-border/80 bg-background/70",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-10 w-10 items-center justify-center rounded-lg",
|
||||
tone === "urgent"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-secondary text-secondary-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-2xl font-semibold tracking-tight">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeLoadingState() {
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<div className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<Skeleton className="h-6 w-44" />
|
||||
<Skeleton className="mt-3 h-4 w-full max-w-xl" />
|
||||
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{["first", "second", "third", "fourth"].map((item) => (
|
||||
<Skeleton className="h-24 rounded-lg" key={item} />
|
||||
))}
|
||||
</div>
|
||||
<Skeleton className="h-6 w-32" />
|
||||
<Skeleton className="mt-3 h-4 w-full max-w-lg" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
{["mentions", "actions", "activity", "agents"].map((section) => (
|
||||
{["mentions", "actions"].map((section) => (
|
||||
<div
|
||||
className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm"
|
||||
key={section}
|
||||
@@ -338,7 +274,6 @@ function HomeLoadingState() {
|
||||
type HomeViewProps = {
|
||||
feed?: HomeFeedResponse;
|
||||
isLoading?: boolean;
|
||||
isRefreshing?: boolean;
|
||||
errorMessage?: string;
|
||||
currentPubkey?: string;
|
||||
availableChannelIds: ReadonlySet<string>;
|
||||
@@ -349,7 +284,6 @@ type HomeViewProps = {
|
||||
export function HomeView({
|
||||
feed,
|
||||
isLoading = false,
|
||||
isRefreshing = false,
|
||||
errorMessage,
|
||||
currentPubkey,
|
||||
availableChannelIds,
|
||||
@@ -357,12 +291,7 @@ export function HomeView({
|
||||
onRefresh,
|
||||
}: HomeViewProps) {
|
||||
const feedItems = feed
|
||||
? [
|
||||
...feed.feed.mentions,
|
||||
...feed.feed.needsAction,
|
||||
...feed.feed.activity,
|
||||
...feed.feed.agentActivity,
|
||||
]
|
||||
? [...feed.feed.mentions, ...feed.feed.needsAction]
|
||||
: [];
|
||||
const feedProfilesQuery = useUsersBatchQuery(
|
||||
feedItems.map((item) => item.pubkey),
|
||||
@@ -397,80 +326,9 @@ export function HomeView({
|
||||
);
|
||||
}
|
||||
|
||||
const totalUrgent = feed.feed.mentions.length + feed.feed.needsAction.length;
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden overscroll-contain px-4 py-4 sm:px-6">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<section className="rounded-xl border border-border/80 bg-card/80 p-5 shadow-sm">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold tracking-tight">
|
||||
Focus queue
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Mentions, reminders, channel activity, and agent work in one
|
||||
feed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage ? (
|
||||
<p className="mt-3 text-sm text-destructive">{errorMessage}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground">
|
||||
Updated {formatUpdatedAt(feed.meta.generatedAt)}
|
||||
</p>
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<RefreshCcw
|
||||
className={cn("h-4 w-4", isRefreshing ? "animate-spin" : "")}
|
||||
/>
|
||||
{isRefreshing ? "Refreshing" : "Refresh"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<SummaryCard
|
||||
icon={CircleAlert}
|
||||
title="Urgent"
|
||||
tone="urgent"
|
||||
value={totalUrgent}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={AtSign}
|
||||
title="Mentions"
|
||||
tone="urgent"
|
||||
value={feed.feed.mentions.length}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Activity}
|
||||
title="Channels"
|
||||
tone="calm"
|
||||
value={feed.feed.activity.length}
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Bot}
|
||||
title="Agents"
|
||||
tone="calm"
|
||||
value={feed.feed.agentActivity.length}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
|
||||
<div className="grid gap-4 xl:grid-cols-2">
|
||||
<FeedSection
|
||||
availableChannelIds={availableChannelIds}
|
||||
@@ -496,30 +354,6 @@ export function HomeView({
|
||||
onOpenChannel={onOpenChannel}
|
||||
title="Needs Action"
|
||||
/>
|
||||
<FeedSection
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={currentPubkey}
|
||||
profiles={feedProfiles}
|
||||
description="Recent updates from channels you can access."
|
||||
emptyDescription="Channel activity will populate here once the relay has recent events."
|
||||
emptyTitle="No recent channel activity"
|
||||
icon={Activity}
|
||||
items={feed.feed.activity}
|
||||
onOpenChannel={onOpenChannel}
|
||||
title="Channel Activity"
|
||||
/>
|
||||
<FeedSection
|
||||
availableChannelIds={availableChannelIds}
|
||||
currentPubkey={currentPubkey}
|
||||
profiles={feedProfiles}
|
||||
description="Agent jobs, progress, and results."
|
||||
emptyDescription="Agent activity appears here once agents start posting into accessible channels."
|
||||
emptyTitle="No agent activity yet"
|
||||
icon={Bot}
|
||||
items={feed.feed.agentActivity}
|
||||
onOpenChannel={onOpenChannel}
|
||||
title="Agent Activity"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,6 @@ type AppSidebarProps = {
|
||||
profile?: Profile;
|
||||
selfPresenceStatus: PresenceStatus;
|
||||
errorMessage?: string;
|
||||
homeUrgentCount?: number;
|
||||
selectedChannelId: string | null;
|
||||
selectedView: "home" | "channel" | "settings";
|
||||
unreadChannelIds: Set<string>;
|
||||
@@ -294,7 +293,6 @@ export function AppSidebar({
|
||||
profile,
|
||||
selfPresenceStatus,
|
||||
errorMessage,
|
||||
homeUrgentCount,
|
||||
selectedChannelId,
|
||||
selectedView,
|
||||
unreadChannelIds,
|
||||
@@ -435,11 +433,6 @@ export function AppSidebar({
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span>Home</span>
|
||||
{homeUrgentCount && homeUrgentCount > 0 ? (
|
||||
<span className="ml-auto rounded-full bg-primary px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-primary-foreground">
|
||||
{homeUrgentCount}
|
||||
</span>
|
||||
) : null}
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -76,9 +76,7 @@ test("opens a mocked channel from the home feed", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("Home");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Focus queue" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "@Mentions" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Please review the release checklist."),
|
||||
).toBeVisible();
|
||||
@@ -91,20 +89,15 @@ test("opens a mocked channel from the home feed", async ({ page }) => {
|
||||
);
|
||||
});
|
||||
|
||||
test("home feed uses your resolved profile label instead of You", async ({
|
||||
page,
|
||||
}) => {
|
||||
const activitySection = page.locator("section").filter({
|
||||
has: page.getByRole("heading", { name: "Channel Activity" }),
|
||||
test("home feed renders resolved author labels", async ({ page }) => {
|
||||
const mentionsSection = page.locator("section").filter({
|
||||
has: page.getByRole("heading", { name: "@Mentions" }),
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
await expect(activitySection).toContainText(
|
||||
"I posted a note about the launch checklist.",
|
||||
);
|
||||
await expect(activitySection).toContainText("npub1mock...");
|
||||
await expect(activitySection).not.toContainText("You");
|
||||
await expect(mentionsSection).toContainText("alice");
|
||||
await expect(mentionsSection).not.toContainText("You");
|
||||
});
|
||||
|
||||
test("opens relay-backed search from the sidebar and loads the exact result", async ({
|
||||
|
||||
@@ -102,13 +102,13 @@ test("loads the home feed from the relay", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("Home");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Focus queue" }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "@Mentions" })).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Needs Action" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Channel Activity" }),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("creates a relay-backed stream", async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user