-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Bulk Archive #1198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bulk Archive #1198
Changes from 58 commits
5902c4e
ff331d9
5fa78db
547d5a3
3b4b8cf
3791f29
486e797
d93dc93
80ef765
22e00b0
caa93a7
03bb4cc
41f8011
b580311
07f0f7d
b2e6fc2
66d8556
61c0cab
b6eda9e
590854d
aef53b0
0c39c6f
823d56f
714aa6b
a9645ce
60a0d4f
99d7330
81d570b
d02d560
72ff54a
3868dfe
f68d0b8
c4acb68
16bc1b3
809035e
d2fc9d6
f5939e0
eec04d0
0f4d4c2
54f8768
7097c53
6508671
b86508f
9de966d
91168da
ab99c67
661ca1b
175e377
b44eaf4
76e5e8f
2713406
de86725
75234fb
a77f6a9
7824f59
a289dee
c5b3627
08d37d0
3dbab3b
0810ea4
d638572
ae9d724
63d82e6
8074688
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"); | ||
| } |
| 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"); | ||
| } |
| 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> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| "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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setup dialog is blocked by Because 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 |
||
|
|
||
| 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]); | ||
|
|
||
| // 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> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.