Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export default defineConfig({
"**/project-pr-review.spec.ts",
"**/persona-model-combobox-screenshots.spec.ts",
"**/drafts-screenshots.spec.ts",
"**/drafts-all-fix-screenshots.spec.ts",
"**/inbox-refactor-screenshots.spec.ts",
"**/buzz-theme-screenshots.spec.ts",
"**/channel-sort.spec.ts",
Expand Down
20 changes: 2 additions & 18 deletions desktop/src/features/home/lib/inboxListRows.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ function inboxItem(
};
}

function draftItem(key, updatedAt, rootStatus = "available") {
return {
entry: {
key,
draft: { createdAt: updatedAt, updatedAt },
},
rootStatus,
};
}

function reminder(
id,
createdAt,
Expand All @@ -46,20 +36,18 @@ function reminder(

test("Inbox All combines rows in latest-first order", () => {
const rows = buildInboxListRows({
drafts: [draftItem("draft", "2026-07-21T12:00:00.000Z")],
items: [inboxItem("message", 1_753_099_300)],
reminders: [reminder("reminder", 1_753_099_100)],
});

assert.deepEqual(
rows.map((row) => row.kind),
["draft", "inbox", "reminder"],
["inbox", "reminder"],
);
});

test("Inbox All excludes completed reminders and deleted-root drafts", () => {
test("Inbox All excludes completed reminders", () => {
const rows = buildInboxListRows({
drafts: [draftItem("deleted", "2026-07-21T12:00:00.000Z", "deleted")],
items: [],
reminders: [reminder("done", 1_753_099_100, "done")],
});
Expand All @@ -69,12 +57,10 @@ test("Inbox All excludes completed reminders and deleted-root drafts", () => {

test("Inbox conversation keys stay stable when the representative changes", () => {
const first = buildInboxListRows({
drafts: [],
items: [inboxItem("reply-1", 1, "thread-root")],
reminders: [],
});
const second = buildInboxListRows({
drafts: [],
items: [inboxItem("reply-2", 2, "thread-root")],
reminders: [],
});
Expand All @@ -87,7 +73,6 @@ test("due reminder enriches its existing conversation instead of duplicating it"
const item = inboxItem("message", 100);
item.groupItems = [{ id: "reminded-reply" }];
const rows = buildInboxListRows({
drafts: [],
items: [item],
reminders: [
reminder("reminder", 50, "pending", {
Expand All @@ -105,7 +90,6 @@ test("due reminder enriches its existing conversation instead of duplicating it"

test("due reminder without a represented conversation sorts at trigger time", () => {
const rows = buildInboxListRows({
drafts: [],
items: [inboxItem("newer-than-creation", 150)],
reminders: [
reminder("reminder", 50, "pending", {
Expand Down
30 changes: 0 additions & 30 deletions desktop/src/features/home/lib/inboxListRows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { InboxItem } from "@/features/home/lib/inbox";
import type { DraftViewItem } from "@/features/messages/ui/DraftsPanel";
import type { Reminder } from "@/features/reminders/lib/reminderTypes";

export type InboxListRow =
Expand All @@ -15,31 +14,12 @@ export type InboxListRow =
kind: "reminder";
reminder: Reminder;
sortAt: number;
}
| {
key: string;
kind: "draft";
item: DraftViewItem;
sortAt: number;
};

function draftActivityAt(item: DraftViewItem): number {
for (const value of [
item.entry.draft.updatedAt,
item.entry.draft.createdAt,
]) {
const timestamp = Date.parse(value);
if (Number.isFinite(timestamp)) return timestamp / 1_000;
}
return 0;
}

export function buildInboxListRows({
drafts,
items,
reminders,
}: {
drafts: readonly DraftViewItem[];
items: readonly InboxItem[];
reminders: readonly Reminder[];
}): InboxListRow[] {
Expand Down Expand Up @@ -98,15 +78,5 @@ export function buildInboxListRows({
sortAt: reminder.notBefore ?? reminder.createdAt,
}),
),
...drafts
.filter((item) => item.rootStatus !== "deleted")
.map(
(item): InboxListRow => ({
key: `draft:${item.entry.key}`,
kind: "draft",
item,
sortAt: draftActivityAt(item),
}),
),
].sort((left, right) => right.sortAt - left.sortAt);
}
77 changes: 18 additions & 59 deletions desktop/src/features/home/ui/InboxListPane.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
Bell,
Clock,
Ellipsis,
ExternalLink,
FileText,
MailOpen,
} from "lucide-react";
import { Bell, Clock, Ellipsis, ExternalLink, MailOpen } from "lucide-react";
import * as React from "react";

import {
Expand All @@ -18,7 +11,6 @@ import { buildInboxListRows } from "@/features/home/lib/inboxListRows";
import { InboxFilterMenu } from "@/features/home/ui/InboxFilterMenu";
import {
DraftsPanel,
getDraftPreview,
type DraftViewItem,
} from "@/features/messages/ui/DraftsPanel";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
Expand Down Expand Up @@ -130,41 +122,36 @@ function formatReminderStatus(notBefore: number | undefined) {

function PersonalItemRow({
id,
kind,
location,
onClick,
preview,
selected,
status,
}: {
id: string;
kind: "drafts" | "reminders";
location: InboxTypeLabel | null;
onClick: () => void;
preview: string;
selected: boolean;
status: string;
}) {
const isDraft = kind === "drafts";
const Icon = isDraft ? FileText : Bell;

return (
<button
aria-current={selected ? "true" : undefined}
className={cn(
"flex w-full items-center gap-3 border-b border-border/45 px-4 py-3 text-left transition-colors hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:outline-hidden",
selected && "bg-muted/40",
)}
data-testid={`home-all-${kind}-${id}`}
data-testid={`home-all-reminders-${id}`}
onClick={onClick}
type="button"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
<Icon className="h-4 w-4" />
<Bell className="h-4 w-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-semibold text-foreground">
{isDraft ? "Draft" : "Reminder"}
Reminder
</span>
{location ? (
<InboxLabel
Expand Down Expand Up @@ -246,15 +233,14 @@ export function InboxListPane({
const inboxRows = React.useMemo(
() =>
buildInboxListRows({
drafts: unreadOnly || !isMixedInboxView ? [] : draftItems,
items,
reminders: unreadOnly
? []
: reminders.filter((reminder) =>
isDue(reminder, Math.floor(Date.now() / 1_000)),
),
}),
[draftItems, isMixedInboxView, items, reminders, unreadOnly],
[items, reminders, unreadOnly],
);
const visibleInboxRows = React.useMemo(
() =>
Expand Down Expand Up @@ -632,57 +618,30 @@ export function InboxListPane({
return renderItem(row.item, row.dueReminder);
}

if (row.kind === "reminder") {
const source = reminderSources.get(row.reminder.id);
return (
<PersonalItemRow
id={row.reminder.id}
kind="reminders"
location={
source?.channel
? source.channel.channelType === "dm"
? {
text: `In DM with ${source.channelLabel}`,
channelLabel: null,
}
: { text: "In", channelLabel: source.channelLabel }
: null
}
onClick={() => {
onSelectReminder(row.reminder.id);
}}
preview={
row.reminder.content.target?.preview ||
row.reminder.content.note ||
"Reminder"
}
selected={selectedReminderId === row.reminder.id}
status={formatReminderStatus(row.reminder.notBefore)}
/>
);
}

const { entry, source } = row.item;
const source = reminderSources.get(row.reminder.id);
return (
<PersonalItemRow
id={entry.key}
kind="drafts"
id={row.reminder.id}
location={
source.channel
source?.channel
? source.channel.channelType === "dm"
? {
text: `In DM with ${source.label}`,
text: `In DM with ${source.channelLabel}`,
channelLabel: null,
}
: { text: "In", channelLabel: source.label }
: { text: "In", channelLabel: source.channelLabel }
: null
}
onClick={() => {
onSelectDraft(entry.key);
onSelectReminder(row.reminder.id);
}}
preview={getDraftPreview(entry.draft)}
selected={selectedDraftKey === entry.key}
status="Draft saved"
preview={
row.reminder.content.target?.preview ||
row.reminder.content.note ||
"Reminder"
}
selected={selectedReminderId === row.reminder.id}
status={formatReminderStatus(row.reminder.notBefore)}
/>
);
}}
Expand Down
4 changes: 3 additions & 1 deletion desktop/src/features/home/useHomePersonalInbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,12 @@ export function useHomePersonalInbox({
viewportWidthPx,
]);

// Drafts are only listed (and selectable) under the dedicated Drafts
// filter — they never appear in the mixed All view.
const drafts = useHomeDrafts({
autoSelect: isDrafts,
isNarrowHomeViewport,
selectionEnabled: isDrafts || allowMixedSelection,
selectionEnabled: isDrafts,
viewportWidthPx,
});

Expand Down
11 changes: 9 additions & 2 deletions desktop/tests/e2e/channels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2629,7 +2629,7 @@ test("Inbox All excludes generic channel traffic", async ({ page }) => {
).toHaveCount(0);
});

test("Inbox unread-only hides reminders and drafts from mixed All", async ({
test("Inbox All never lists drafts and unread-only hides reminders", async ({
page,
}) => {
const draftKey = `channel:${GENERAL_CHANNEL_ID}`;
Expand Down Expand Up @@ -2734,14 +2734,21 @@ test("Inbox unread-only hides reminders and drafts from mixed All", async ({
const draftRow = page.getByTestId(`home-all-drafts-${draftKey}`);
await expect(messageRow).toBeVisible();
await expect(reminderRow).toBeVisible();
await expect(draftRow).toBeVisible();
// Drafts belong to the dedicated Drafts filter — never the mixed All view.
await expect(draftRow).toHaveCount(0);

await page.getByTestId("inbox-options-trigger").click();
await page.getByRole("switch", { name: "Show unread only" }).click();

await expect(messageRow).toBeVisible();
await expect(reminderRow).toHaveCount(0);
await expect(draftRow).toHaveCount(0);

// The draft is still reachable under the Drafts filter.
await page.keyboard.press("Escape");
await page.getByTestId("inbox-filter-trigger").click();
await page.getByRole("menuitemradio", { name: "Drafts" }).click();
await expect(page.getByTestId("home-inbox-drafts")).toBeVisible();
});

test("Inbox merges a due reminder into its represented conversation", async ({
Expand Down
Loading
Loading