Skip to content
Merged
Show file tree
Hide file tree
Changes from 37 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,76 @@
"use client";

import { useState, useCallback } from "react";
import { toast } from "sonner";
import { ArchiveIcon, TagsIcon, ZapIcon } from "lucide-react";
import { SetupCard } 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: "Smart categorization",
description:
"Automatically group senders into categories like Newsletters, Receipts, and Marketing",
},
{
icon: <ArchiveIcon className="size-4 text-blue-500" />,
title: "Bulk actions",
description: "Archive entire categories at once instead of email by email",
},
{
icon: <ZapIcon className="size-4 text-blue-500" />,
title: "Instant cleanup",
description: "Clear hundreds of emails in seconds, not hours",
},
];

export function AutoCategorizationSetup() {
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);
}

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

return (
<SetupCard
imageSrc="/images/illustrations/working-vacation.svg"
imageAlt="Bulk Archive"
title="Bulk Archive"
description="Clean up your inbox by archiving emails in bulk by category."
features={features}
>
<Button onClick={enableFeature} loading={isEnabling}>
Get Started
</Button>
</SetupCard>
);
}
72 changes: 72 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,72 @@
"use client";

import { useMemo, useCallback } from "react";
import useSWR from "swr";
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 type { CategorizedSendersResponse } from "@/app/api/user/categorize/senders/categorized/route";
import type { CategoryWithRules } from "@/utils/category.server";
import { PageWrapper } from "@/components/PageWrapper";
import { PageHeader } from "@/components/PageHeader";

type Sender = {
id: string;
email: string;
category: { id: string; description: string | null; name: string } | null;
};

export function BulkArchive({
initialSenders,
initialCategories,
autoCategorizeSenders,
}: {
initialSenders: Sender[];
initialCategories: CategoryWithRules[];
autoCategorizeSenders: boolean;
}) {
const { isBulkCategorizing } = useCategorizeProgress();

// Poll for updates while categorization is in progress
const { data, mutate } = useSWR<CategorizedSendersResponse>(
"/api/user/categorize/senders/categorized",
{
refreshInterval: isBulkCategorizing ? 2000 : undefined,
fallbackData: { senders: initialSenders, categories: initialCategories },
},
);

// Use SWR data if available, otherwise fall back to initial server data
const senders = data?.senders ?? initialSenders;
const categories = data?.categories ?? initialCategories;

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

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

const shouldShowSetup = !autoCategorizeSenders && !isBulkCategorizing;

return (
<>
{shouldShowSetup ? (
<AutoCategorizationSetup />
) : (
<PageWrapper>
<PageHeader title="Bulk Archive" />
<BulkArchiveProgress onComplete={handleProgressComplete} />
<BulkArchiveCards emailGroups={emailGroups} categories={categories} />
</PageWrapper>
)}
</>
);
}
Loading
Loading