Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
5902c4e
Cleaning up the UI/UX of the smart categories page
jshwrnr Jan 5, 2026
ff331d9
Add tabs
jshwrnr Jan 5, 2026
5fa78db
Split pages
jshwrnr Jan 5, 2026
547d5a3
Comment out bulk archive and quick bulk archive from early access page
jshwrnr Jan 5, 2026
3b4b8cf
Add automatic sender categorization
jshwrnr Jan 5, 2026
3791f29
Merge branch 'main' into feat/bulk-archive
jshwrnr Jan 5, 2026
486e797
fix: address PR review comments
jshwrnr Jan 5, 2026
d93dc93
fix: address additional PR review comments
jshwrnr Jan 5, 2026
80ef765
Merge branch 'main' into feat/bulk-archive
jshwrnr Jan 6, 2026
22e00b0
Fixes
jshwrnr Jan 6, 2026
caa93a7
fix: restore marketing submodule reference
jshwrnr Jan 6, 2026
03bb4cc
refactor: extract getCategoryIcon to separate module
jshwrnr Jan 6, 2026
41f8011
Add rel
jshwrnr Jan 6, 2026
b580311
Refactor BulkCategorizeSendersAction
jshwrnr Jan 6, 2026
07f0f7d
Add tests
jshwrnr Jan 6, 2026
b2e6fc2
Fix
jshwrnr Jan 6, 2026
66d8556
Use both gmail and outlook providers for bulk archiving
jshwrnr Jan 6, 2026
61c0cab
Fix error not being used in catch
jshwrnr Jan 6, 2026
b6eda9e
Remove unused code
jshwrnr Jan 6, 2026
590854d
Rename
jshwrnr Jan 6, 2026
aef53b0
Fix
jshwrnr Jan 6, 2026
0c39c6f
Fix
jshwrnr Jan 6, 2026
823d56f
Merge branch 'main' into feat/bulk-archive
elie222 Jan 6, 2026
714aa6b
Remove unnecessary test
jshwrnr Jan 7, 2026
a9645ce
Merge branch 'feat/bulk-archive' of https://github.com/jshwrnr/inbox-…
jshwrnr Jan 7, 2026
60a0d4f
Remove copy from bottom of bulk archive, add onboarding query param f…
jshwrnr Jan 7, 2026
99d7330
Use proper layout plus other styling changes
jshwrnr Jan 7, 2026
81d570b
Remove the gray bar in each bulk archive category
jshwrnr Jan 7, 2026
d02d560
Remove menu and modals from bulk archive categories
jshwrnr Jan 7, 2026
72ff54a
Clean up setup flow for bulk archive
jshwrnr Jan 7, 2026
3868dfe
Reduce sender categories down to only four
jshwrnr Jan 7, 2026
f68d0b8
Fix up bulk archive setup and content
jshwrnr Jan 7, 2026
c4acb68
Fix
jshwrnr Jan 7, 2026
16bc1b3
Fixes
jshwrnr Jan 7, 2026
809035e
Fix
jshwrnr Jan 7, 2026
d2fc9d6
Fix
jshwrnr Jan 7, 2026
f5939e0
Add shrink-0 to icons in bulk archive list
jshwrnr Jan 7, 2026
eec04d0
Fix
jshwrnr Jan 7, 2026
0f4d4c2
Fix
jshwrnr Jan 7, 2026
54f8768
Fix
jshwrnr Jan 7, 2026
7097c53
Fix
jshwrnr Jan 7, 2026
6508671
Fix
jshwrnr Jan 7, 2026
b86508f
Add to bulk archive a description tooltip, colored icons, and a notif…
jshwrnr Jan 7, 2026
9de966d
Swap colors of notification and other categories in bulk archive
jshwrnr Jan 8, 2026
91168da
Use TooltipExplanation in bulk archive
jshwrnr Jan 8, 2026
ab99c67
Fix
jshwrnr Jan 8, 2026
661ca1b
Fix
jshwrnr Jan 8, 2026
175e377
Merge branch 'main' into feat/bulk-archive
elie222 Jan 9, 2026
b44eaf4
remove popsy next config
elie222 Jan 9, 2026
76e5e8f
Simplify categories to 5 primary only, refactor to SWR pattern
elie222 Jan 9, 2026
2713406
Fix: Replace useMemo with useEffect for side effects in BulkArchiveTab
elie222 Jan 9, 2026
de86725
Merge branch 'main' into feat/bulk-archive
elie222 Jan 9, 2026
75234fb
copy
elie222 Jan 9, 2026
a77f6a9
setup card dialog
elie222 Jan 9, 2026
7824f59
Refactor BulkArchive component layout and integrate CategorizeWithAiB…
elie222 Jan 9, 2026
a289dee
Merge branch 'main' into feat/bulk-archive
elie222 Jan 9, 2026
c5b3627
Add error handling for Qstash queue publishing
elie222 Jan 9, 2026
08d37d0
Enhance internal API URL handling
elie222 Jan 9, 2026
3dbab3b
styling
elie222 Jan 9, 2026
0810ea4
Show caret
elie222 Jan 9, 2026
d638572
Merge branch 'main' into feat/bulk-archive
elie222 Jan 9, 2026
ae9d724
feat(bulk-archive): allow expanding archived categories and add sende…
elie222 Jan 9, 2026
63d82e6
faster onboarding
elie222 Jan 9, 2026
8074688
await
elie222 Jan 9, 2026
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
2 changes: 1 addition & 1 deletion apps/web/__tests__/ai-categorize-senders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe.runIf(isAiTest)("AI Sender Categorization", () => {
"should categorize senders for all valid SenderCategory values",
async () => {
const senders = getEnabledCategories()
.filter((category) => category.name !== "Unknown")
.filter((category) => category.name !== "Other")
.map((category) => `${category.name}@example.com`);

const result = await aiCategorizeSenders({
Expand Down
5 changes: 5 additions & 0 deletions apps/web/app/(app)/(redirects)/bulk-archive/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirectToEmailAccountPath } from "@/utils/account";

export default async function BulkArchivePage() {
await redirectToEmailAccountPath("/bulk-archive");
}
5 changes: 5 additions & 0 deletions apps/web/app/(app)/(redirects)/quick-bulk-archive/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { redirectToEmailAccountPath } from "@/utils/account";

export default async function QuickBulkArchivePage() {
await redirectToEmailAccountPath("/quick-bulk-archive");
}
126 changes: 44 additions & 82 deletions apps/web/app/(app)/[emailAccountId]/briefs/Onboarding.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
"use client";

import { CardFooter, Card } from "@/components/ui/card";
import {
MessageText,
SectionDescription,
TypographyH3,
} from "@/components/Typography";
import { ConnectCalendar } from "@/app/(app)/[emailAccountId]/calendars/ConnectCalendar";
import { MailIcon, LightbulbIcon, UserSearchIcon } from "lucide-react";
import { SetupCard } from "@/components/SetupCard";
import { MessageText } from "@/components/Typography";
import { Button } from "@/components/ui/button";
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle,
} from "@/components/ui/item";
import Image from "next/image";
import { ConnectCalendar } from "@/app/(app)/[emailAccountId]/calendars/ConnectCalendar";

const features = [
{
icon: <UserSearchIcon className="size-4 text-blue-500" />,
title: "Attendee research",
description: "Who they are, their company, and role",
},
{
icon: <MailIcon className="size-4 text-blue-500" />,
title: "Email history",
description: "Recent conversations with this person",
},
{
icon: <LightbulbIcon className="size-4 text-blue-500" />,
title: "Key context",
description: "Important details from past discussions",
},
];

export function BriefsOnboarding({
emailAccountId,
Expand All @@ -30,72 +36,28 @@ export function BriefsOnboarding({
isEnabling?: boolean;
}) {
return (
<Card className="mx-4 mt-10 max-w-2xl p-6 md:mx-auto">
<Image
src="/images/illustrations/communication.svg"
alt="Meeting Briefs"
width={200}
height={200}
className="mx-auto dark:brightness-90 dark:invert"
unoptimized
/>

<div className="text-center">
<TypographyH3 className="mt-2">Meeting Briefs</TypographyH3>
<SectionDescription className="mx-auto mt-2 max-w-prose">
Receive email briefings before meetings with external guests.
</SectionDescription>
</div>

<ItemGroup className="mt-6">
<Item>
<UserSearchIcon className="text-blue-500 size-4" />
<ItemContent>
<ItemTitle>Attendee research</ItemTitle>
<ItemDescription>
Who they are, their company, and role
</ItemDescription>
</ItemContent>
</Item>
<Item>
<MailIcon className="text-blue-500 size-4" />
<ItemContent>
<ItemTitle>Email history</ItemTitle>
<ItemDescription>
Recent conversations with this person
</ItemDescription>
</ItemContent>
</Item>
<Item>
<LightbulbIcon className="text-blue-500 size-4" />
<ItemContent>
<ItemTitle>Key context</ItemTitle>
<ItemDescription>
Important details from past discussions
</ItemDescription>
</ItemContent>
</Item>
</ItemGroup>

<CardFooter className="flex flex-col items-center gap-4 mt-6">
{hasCalendarConnected ? (
<>
<MessageText>
You're all set! Enable meeting briefs to get started:
</MessageText>
<Button onClick={onEnable} loading={isEnabling}>
Enable Meeting Briefs
</Button>
</>
) : (
<>
<MessageText>Connect your calendar to get started:</MessageText>
<ConnectCalendar
onboardingReturnPath={`/${emailAccountId}/briefs`}
/>
</>
)}
</CardFooter>
</Card>
<SetupCard
imageSrc="/images/illustrations/communication.svg"
imageAlt="Meeting Briefs"
title="Meeting Briefs"
description="Receive email briefings before meetings with external guests."
features={features}
>
{hasCalendarConnected ? (
<>
<MessageText>
You're all set! Enable meeting briefs to get started:
</MessageText>
<Button onClick={onEnable} loading={isEnabling}>
Enable Meeting Briefs
</Button>
</>
) : (
<>
<MessageText>Connect your calendar to get started:</MessageText>
<ConnectCalendar onboardingReturnPath={`/${emailAccountId}/briefs`} />
</>
)}
</SetupCard>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { useState, useCallback } from "react";
import { toastError, toastSuccess } from "@/components/Toast";
import { ArchiveIcon, RotateCcwIcon, TagsIcon } from "lucide-react";
import { SetupDialog } from "@/components/SetupCard";
import { Button } from "@/components/ui/button";
import { bulkCategorizeSendersAction } from "@/utils/actions/categorize";
import { useAccount } from "@/providers/EmailAccountProvider";
import { useCategorizeProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress";

const features = [
{
icon: <TagsIcon className="size-4 text-blue-500" />,
title: "Sorted automatically",
description:
"We group senders into categories like Newsletters, Receipts, and Marketing",
},
{
icon: <ArchiveIcon className="size-4 text-blue-500" />,
title: "Archive by category",
description:
"Clean up an entire category at once instead of one email at a time",
},
{
icon: <RotateCcwIcon className="size-4 text-blue-500" />,
title: "Always reversible",
description: "Emails are archived, not deleted — you can find them anytime",
},
];

export function AutoCategorizationSetup({ open }: { open: boolean }) {
const { emailAccountId } = useAccount();
const { setIsBulkCategorizing } = useCategorizeProgress();

const [isEnabling, setIsEnabling] = useState(false);

const enableFeature = useCallback(async () => {
setIsEnabling(true);
setIsBulkCategorizing(true);

try {
const result = await bulkCategorizeSendersAction(emailAccountId);

if (result?.serverError) {
throw new Error(result.serverError);
}

if (result?.data?.totalUncategorizedSenders) {
toastSuccess({
description: `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.`,
});
} else {
toastSuccess({ description: "No uncategorized senders found." });
setIsBulkCategorizing(false);
}
} catch (error) {
toastError({
description: `Failed to enable feature: ${error instanceof Error ? error.message : "Unknown error"}`,
});
setIsBulkCategorizing(false);
} finally {
setIsEnabling(false);
}
}, [emailAccountId, setIsBulkCategorizing]);

return (
<SetupDialog
open={open}
imageSrc="/images/illustrations/working-vacation.svg"
imageAlt="Bulk Archive"
title="Bulk Archive"
description="Archive thousands of emails in a few clicks."
features={features}
>
<Button onClick={enableFeature} loading={isEnabling}>
Get Started
</Button>
</SetupDialog>
);
}
69 changes: 69 additions & 0 deletions apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { useMemo, useCallback } from "react";
import useSWR from "swr";
import { parseAsBoolean, useQueryState } from "nuqs";
import { AutoCategorizationSetup } from "@/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup";
import { BulkArchiveProgress } from "@/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveProgress";
import { BulkArchiveCards } from "@/components/BulkArchiveCards";
import { useCategorizeProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress";
import { CategorizeWithAiButton } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeWithAiButton";
import type { CategorizedSendersResponse } from "@/app/api/user/categorize/senders/categorized/route";
import { PageWrapper } from "@/components/PageWrapper";
import { LoadingContent } from "@/components/LoadingContent";
import { TooltipExplanation } from "@/components/TooltipExplanation";
import { PageHeading } from "@/components/Typography";

export function BulkArchive() {
const { isBulkCategorizing } = useCategorizeProgress();
const [onboarding] = useQueryState("onboarding", parseAsBoolean);

// Fetch data with SWR and poll while categorization is in progress
const { data, error, isLoading, mutate } = useSWR<CategorizedSendersResponse>(
"/api/user/categorize/senders/categorized",
{
refreshInterval: isBulkCategorizing ? 2000 : undefined,
},
);

const senders = data?.senders ?? [];
const categories = data?.categories ?? [];
const autoCategorizeSenders = data?.autoCategorizeSenders ?? false;
Comment on lines +29 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Setup dialog is blocked by LoadingContent on loading/error (and can prevent enabling the feature).

Because AutoCategorizationSetup is rendered inside LoadingContent (Line 49-61), it won’t appear while isLoading is true or when error is set—exactly when you may still want to show onboarding/setup (especially if the categorized endpoint errors).

Proposed fix (render setup outside LoadingContent + avoid defaulting autoCategorizeSenders to false)
-  const autoCategorizeSenders = data?.autoCategorizeSenders ?? false;
+  const autoCategorizeSenders = data?.autoCategorizeSenders;

   const handleProgressComplete = useCallback(() => {
     mutate();
   }, [mutate]);

   const shouldShowSetup =
-    onboarding || (!autoCategorizeSenders && !isBulkCategorizing);
+    onboarding || (autoCategorizeSenders === false && !isBulkCategorizing);

   return (
-    <LoadingContent loading={isLoading} error={error}>
-      <PageWrapper>
-        <PageHeader
-          title="Bulk Archive"
-          rightElement={
-            <TooltipExplanation text="Archive emails in bulk by category to quickly clean up your inbox." />
-          }
-        />
-        <BulkArchiveProgress onComplete={handleProgressComplete} />
-        <BulkArchiveCards emailGroups={emailGroups} categories={categories} />
-      </PageWrapper>
-      <AutoCategorizationSetup open={shouldShowSetup} />
-    </LoadingContent>
+    <>
+      <LoadingContent loading={isLoading} error={error}>
+        <PageWrapper>
+          <PageHeader
+            title="Bulk Archive"
+            rightElement={
+              <TooltipExplanation text="Archive emails in bulk by category to quickly clean up your inbox." />
+            }
+          />
+          <BulkArchiveProgress onComplete={handleProgressComplete} />
+          <BulkArchiveCards emailGroups={emailGroups} categories={categories} />
+        </PageWrapper>
+      </LoadingContent>
+      <AutoCategorizationSetup open={shouldShowSetup} />
+    </>
   );

Also applies to: 45-47, 49-61

🤖 Prompt for AI Agents
In @apps/web/app/(app)/[emailAccountId]/bulk-archive/BulkArchive.tsx around
lines 28 - 30, AutoCategorizationSetup is hidden by LoadingContent because
autoCategorizeSenders is defaulted to false and the setup component is rendered
inside the isLoading/error gated block; move the <AutoCategorizationSetup ... />
render outside the LoadingContent/error gating so it always renders (so users
can complete onboarding even when the fetch is loading/errored) and stop
defaulting autoCategorizeSenders to false—use const autoCategorizeSenders =
data?.autoCategorizeSenders; (and similarly avoid forcing senders/categories to
silent defaults that hide intent) so the setup UI can detect absence vs explicit
false.


const emailGroups = useMemo(
() =>
senders.map((sender) => ({
address: sender.email,
name: sender.name ?? null,
category: categories.find((c) => c.id === sender.category?.id) || null,
})),
[senders, categories],
);

const handleProgressComplete = useCallback(() => {
mutate();
}, [mutate]);

// Show setup dialog for first-time setup only
const shouldShowSetup =
onboarding || (!autoCategorizeSenders && !isBulkCategorizing);

return (
<LoadingContent loading={isLoading} error={error}>
<PageWrapper>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<PageHeading>Bulk Archive</PageHeading>
<TooltipExplanation text="Archive emails in bulk by category to quickly clean up your inbox." />
</div>
<CategorizeWithAiButton
buttonProps={{ variant: "outline", size: "sm" }}
/>
</div>
<BulkArchiveProgress onComplete={handleProgressComplete} />
<BulkArchiveCards emailGroups={emailGroups} categories={categories} />
</PageWrapper>
<AutoCategorizationSetup open={shouldShowSetup} />
</LoadingContent>
);
}
Loading
Loading