Simplify the desktop Home feed (#52)

This commit is contained in:
Wes
2026-03-13 09:17:53 -07:00
committed by GitHub
parent a516b16eda
commit 771ec203e5
6 changed files with 16 additions and 201 deletions
-5
View File
@@ -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();
+1 -1
View File
@@ -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,
+6 -172
View File
@@ -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>
+6 -13
View File
@@ -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 ({
+3 -3
View File
@@ -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 }) => {