Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
fcd0611
extract action to its own function
elie222 Nov 8, 2024
282cc83
Create bulk categorize endpoint that runs for longer with api call ch…
elie222 Nov 8, 2024
8d0c125
Update turbo env
elie222 Nov 8, 2024
2744554
Merge branch 'main' into bulk-categorize
elie222 Nov 8, 2024
f304d18
Merge branch 'main' into bulk-categorize
elie222 Nov 8, 2024
e5c8cbd
Start multi page categorization
elie222 Nov 8, 2024
9a02b72
Merge branch 'main' into bulk-categorize
elie222 Nov 11, 2024
d8c9713
Smaller batches for bulk categorize
elie222 Nov 12, 2024
f6a50a9
Use Qstash for categorization
elie222 Nov 12, 2024
450d0a0
Store categorization progress in redis
elie222 Nov 12, 2024
e2e831b
Add webhook url env var
elie222 Nov 12, 2024
8371899
categorize progress bar
elie222 Nov 12, 2024
c245e6c
Categorization progress panel
elie222 Nov 15, 2024
b8747d7
Move categorize functions into their own file
elie222 Nov 15, 2024
8693465
Refactor categorize senders
elie222 Nov 15, 2024
b95dce4
Add user email ai type
elie222 Nov 15, 2024
e1d6793
Clean up validate
elie222 Nov 15, 2024
15a50ef
move trigger file
elie222 Nov 15, 2024
896758e
Move categorize email to its own file
elie222 Nov 15, 2024
be36a5e
Update user count
elie222 Nov 15, 2024
839a579
Clean up user validation
elie222 Nov 15, 2024
d3c0977
Add scoped logger
elie222 Nov 15, 2024
07547ad
Adjust logging
elie222 Nov 15, 2024
35b6660
Simplify action logging
elie222 Nov 15, 2024
73ba7a4
Code clean up
elie222 Nov 15, 2024
2c3582a
Add error middleware for GET
elie222 Nov 15, 2024
ca48bcc
Fallback to work even if qstash token not set
elie222 Nov 15, 2024
7b9e99b
Merge branch 'main' into bulk-categorize
elie222 Nov 17, 2024
db12705
expire redis categorization progress in seconds
elie222 Nov 18, 2024
dbff85f
Fixes to categorization progress
elie222 Nov 18, 2024
2787dc4
Set biome as default formatter
elie222 Nov 18, 2024
58487ef
Fix progress expire
elie222 Nov 18, 2024
fda649a
Fix completion step
elie222 Nov 18, 2024
fa25644
Remove fast categorize. Add bulk button to top bar
elie222 Nov 18, 2024
f4b6fec
Date range for bulk categorize
elie222 Nov 19, 2024
d7a91b5
Add migration
elie222 Nov 19, 2024
66ce32b
Put back next auth url
elie222 Nov 21, 2024
b58a533
Merge branch 'main' into bulk-categorize
elie222 Nov 21, 2024
f02c066
Merge branch 'main' into bulk-categorize
elie222 Nov 21, 2024
aa4bcd0
Move getUncategorizedSenders into its own file
elie222 Nov 22, 2024
edc893c
Updated bulk categorize sender
elie222 Nov 22, 2024
d5631e2
Merge branch 'main' into bulk-categorize
elie222 Nov 22, 2024
de95d60
Fix build
elie222 Nov 23, 2024
abccc38
Revert to simple logger
elie222 Nov 24, 2024
f2ae8e7
Merge branch 'main' into bulk-categorize
elie222 Nov 24, 2024
3162f42
Merge branch 'main' into bulk-categorize
elie222 Nov 24, 2024
5ed610d
Fix logger
elie222 Nov 24, 2024
09513c6
Fix build
elie222 Nov 25, 2024
0cd956c
Show if no more senders to categorize
elie222 Nov 25, 2024
6665ce8
Make ai assistant the default screen
elie222 Nov 25, 2024
44e5b76
Remove old file
elie222 Nov 25, 2024
52be6c3
Send batches to qstash
elie222 Nov 25, 2024
0065be3
Remove old code
elie222 Nov 25, 2024
f0d8d3e
Fix log
elie222 Nov 25, 2024
140b828
Categorization progress
elie222 Nov 25, 2024
fadbe8f
Fix categorization progress
elie222 Nov 25, 2024
e61fdd5
useeffect timeout cleanup
elie222 Nov 25, 2024
e19ca4a
Update copy
elie222 Nov 25, 2024
3b5cc4e
Fix NaN bug
elie222 Nov 25, 2024
f856c0f
remove auto archive on approve
elie222 Nov 25, 2024
d607c85
Max iterations for while loop
elie222 Nov 25, 2024
5dac6ec
Remove TS !
elie222 Nov 25, 2024
5902340
structured logging
elie222 Nov 25, 2024
a3006e0
Categorize uncategorized
elie222 Nov 25, 2024
b62a30b
Add commented out recategorize code
elie222 Nov 25, 2024
8e2dc7c
Better validation and env examples
elie222 Nov 25, 2024
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 .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"typescript.preferences.importModuleSpecifier": "non-relative",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
Expand Down
4 changes: 4 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ DIRECT_URL="postgresql://postgres:password@localhost:5432/inboxzero?schema=publi

# Generate a random secret here: https://generate-secret.vercel.app/32
NEXTAUTH_SECRET=
NEXTAUTH_URL=http://localhost:3000

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
Expand All @@ -17,6 +18,9 @@ BEDROCK_REGION=us-west-2
UPSTASH_REDIS_URL="http://localhost:8079"
# Generate a random secret here: https://generate-secret.vercel.app/32
UPSTASH_REDIS_TOKEN=
QSTASH_TOKEN=
QSTASH_CURRENT_SIGNING_KEY=
QSTASH_NEXT_SIGNING_KEY=

TINYBIRD_TOKEN=
TINYBIRD_BASE_URL=https://api.us-east.tinybird.co/
Expand Down
42 changes: 8 additions & 34 deletions apps/web/app/(app)/bulk-unsubscribe/ArchiveProgress.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"use client";

import { memo, useEffect } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ProgressBar } from "@tremor/react";
import { resetTotalThreads, useQueueState } from "@/store/archive-queue";
import { cn } from "@/utils";
import { ProgressPanel } from "@/components/ProgressPanel";

export const ArchiveProgress = memo(() => {
const { totalThreads, activeThreads } = useQueueState();
Expand All @@ -23,37 +21,13 @@ export const ArchiveProgress = memo(() => {
}
}, [isCompleted]);

if (!totalThreads) return null;

return (
<div className="px-4 py-2">
<AnimatePresence mode="wait">
<motion.div
key="progress"
initial={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<ProgressBar
value={progress}
className="w-full"
color={isCompleted ? "green" : "blue"}
/>
<p className="mt-2 flex justify-between text-sm" aria-live="polite">
<span
className={cn(
"text-muted-foreground",
isCompleted ? "text-green-500" : "",
)}
>
{isCompleted ? "Archiving complete!" : "Archiving emails..."}
</span>
<span>
{totalProcessed} of {totalThreads} emails archived
</span>
</p>
</motion.div>
</AnimatePresence>
</div>
<ProgressPanel
totalItems={totalThreads}
remainingItems={threadsRemaining}
inProgressText="Archiving emails..."
completedText="Archiving complete!"
itemLabel="emails"
/>
);
});
1 change: 0 additions & 1 deletion apps/web/app/(app)/bulk-unsubscribe/BulkActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export function BulkActions({
const { bulkAutoArchiveLoading, onBulkAutoArchive } = useBulkAutoArchive({
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
});

Expand Down
14 changes: 11 additions & 3 deletions apps/web/app/(app)/bulk-unsubscribe/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,9 @@ export function useAutoArchive<T extends Row>({
const onDisableAutoArchive = useCallback(async () => {
setAutoArchiveLoading(true);

await onDeleteFilter(item.autoArchived?.id!);
if (item.autoArchived?.id) {
await onDeleteFilter(item.autoArchived?.id);
}
await setNewsletterStatusAction({
newsletterEmail: item.name,
status: null,
Expand Down Expand Up @@ -215,12 +217,10 @@ export function useAutoArchive<T extends Row>({
export function useBulkAutoArchive<T extends Row>({
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium,
}: {
hasUnsubscribeAccess: boolean;
mutate: () => Promise<any>;
posthog: PostHog;
refetchPremium: () => Promise<any>;
}) {
const [bulkAutoArchiveLoading, setBulkAutoArchiveLoading] =
Expand Down Expand Up @@ -257,6 +257,13 @@ export function useApproveButton<T extends Row>({
posthog: PostHog;
}) {
const [approveLoading, setApproveLoading] = React.useState(false);
const { onDisableAutoArchive } = useAutoArchive({
item,
hasUnsubscribeAccess: true,
mutate,
posthog,
refetchPremium: () => Promise.resolve(),
});
Comment on lines +260 to +266
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Security concern: Bypassing access control

The hook is initialized with hasUnsubscribeAccess: true, which bypasses access control checks. This could allow unauthorized users to perform auto-archive operations.

Consider using the actual access control value:

-    hasUnsubscribeAccess: true,
+    hasUnsubscribeAccess,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { onDisableAutoArchive } = useAutoArchive({
item,
hasUnsubscribeAccess: true,
mutate,
posthog,
refetchPremium: () => Promise.resolve(),
});
const { onDisableAutoArchive } = useAutoArchive({
item,
hasUnsubscribeAccess,
mutate,
posthog,
refetchPremium: () => Promise.resolve(),
});


const onApprove = async () => {
setApproveLoading(true);
Expand All @@ -265,6 +272,7 @@ export function useApproveButton<T extends Row>({
newsletterEmail: item.name,
status: NewsletterStatus.APPROVED,
});
await onDisableAutoArchive();
await mutate();

posthog.capture("Clicked Approve Sender");
Expand Down
59 changes: 59 additions & 0 deletions apps/web/app/(app)/smart-categories/CategorizeProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { useEffect } from "react";
import { atom, useAtom } from "jotai";
import useSWR from "swr";
import { ProgressPanel } from "@/components/ProgressPanel";
import type { CategorizeProgress } from "@/app/api/user/categorize/senders/progress/route";

const isCategorizeInProgressAtom = atom(false);

export function useCategorizeProgress() {
const [isBulkCategorizing, setIsBulkCategorizing] = useAtom(
isCategorizeInProgressAtom,
);
return { isBulkCategorizing, setIsBulkCategorizing };
}

export function CategorizeSendersProgress({
refresh = false,
}: {
refresh: boolean;
}) {
const { isBulkCategorizing } = useCategorizeProgress();
const { data } = useSWR<CategorizeProgress>(
"/api/user/categorize/senders/progress",
{
refreshInterval: refresh || isBulkCategorizing ? 1_000 : undefined,
},
);

// If the categorization is complete, wait 3 seconds and then set isBulkCategorizing to false
const { setIsBulkCategorizing } = useCategorizeProgress();
useEffect(() => {
let timeoutId: NodeJS.Timeout | undefined;
if (data?.completedItems === data?.totalItems) {
timeoutId = setTimeout(() => {
setIsBulkCategorizing(false);
}, 3_000);
}
return () => {
if (timeoutId) clearTimeout(timeoutId);
};
}, [data?.completedItems, data?.totalItems, setIsBulkCategorizing]);

if (!data) return null;

const totalItems = data.totalItems || 0;
const completedItems = data.completedItems || 0;

return (
<ProgressPanel
totalItems={totalItems}
remainingItems={totalItems - completedItems}
inProgressText={`Categorizing senders... ${completedItems} categorized`}
completedText={`Categorization complete! ${completedItems} categorized!`}
itemLabel="senders"
/>
);
}
34 changes: 25 additions & 9 deletions apps/web/app/(app)/smart-categories/CategorizeWithAiButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ import { useState } from "react";
import { SparklesIcon } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { categorizeSendersAction } from "@/utils/actions/categorize";
import { bulkCategorizeSendersAction } from "@/utils/actions/categorize";
import { handleActionCall } from "@/utils/server-action";
import { isActionError } from "@/utils/error";
import { PremiumTooltip, usePremium } from "@/components/PremiumAlert";
import { usePremiumModal } from "@/app/(app)/premium/PremiumModal";
import type { ButtonProps } from "@/components/ui/button";
import { useCategorizeProgress } from "@/app/(app)/smart-categories/CategorizeProgress";

export function CategorizeWithAiButton() {
export function CategorizeWithAiButton({
buttonProps,
}: {
buttonProps?: ButtonProps;
}) {
const [isCategorizing, setIsCategorizing] = useState(false);
const { hasAiAccess } = usePremium();
const { PremiumModal, openModal: openPremiumModal } = usePremiumModal();

const { setIsBulkCategorizing } = useCategorizeProgress();

return (
<>
<PremiumTooltip showTooltip={!hasAiAccess} openModal={openPremiumModal}>
Expand All @@ -27,9 +35,10 @@ export function CategorizeWithAiButton() {
toast.promise(
async () => {
setIsCategorizing(true);
setIsBulkCategorizing(true);
const result = await handleActionCall(
"categorizeSendersAction",
categorizeSendersAction,
"bulkCategorizeSendersAction",
bulkCategorizeSendersAction,
);

if (isActionError(result)) {
Expand All @@ -42,19 +51,26 @@ export function CategorizeWithAiButton() {
return result;
},
{
loading: "Categorizing senders...",
success: () => {
return "Senders categorized successfully!";
loading: "Categorizing senders... This might take a while.",
success: ({ totalUncategorizedSenders }) => {
return totalUncategorizedSenders
? `Categorizing ${totalUncategorizedSenders} senders...`
: "There are no more senders to categorize.";
},
error: (err) => {
return `Error categorizing senders: ${err.message}`;
},
},
);
}}
{...buttonProps}
>
<SparklesIcon className="mr-2 size-4" />
Categorize Senders with AI
{buttonProps?.children || (
<>
<SparklesIcon className="mr-2 size-4" />
Categorize Senders with AI
</>
)}
</Button>
</PremiumTooltip>
<PremiumModal />
Expand Down
71 changes: 8 additions & 63 deletions apps/web/app/(app)/smart-categories/Uncategorized.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
"use client";

import useSWRInfinite from "swr/infinite";
import { useMemo, useCallback, useState } from "react";
import {
ChevronsDownIcon,
SparklesIcon,
StopCircleIcon,
ZapIcon,
} from "lucide-react";
import { useMemo, useCallback } from "react";
import { ChevronsDownIcon, SparklesIcon, StopCircleIcon } from "lucide-react";
import { useSession } from "next-auth/react";
import { ClientOnly } from "@/components/ClientOnly";
import { SendersTable } from "@/components/GroupedTable";
Expand All @@ -16,7 +11,7 @@ import { Button } from "@/components/ui/button";
import type { UncategorizedSendersResponse } from "@/app/api/user/categorize/senders/uncategorized/route";
import type { Category } from "@prisma/client";
import { TopBar } from "@/components/TopBar";
import { toastError, toastSuccess } from "@/components/Toast";
import { toastError } from "@/components/Toast";
import {
useHasProcessingItems,
pushToAiCategorizeSenderQueueAtom,
Expand All @@ -27,14 +22,8 @@ import { ButtonLoader } from "@/components/Loading";
import { PremiumTooltip, usePremium } from "@/components/PremiumAlert";
import { usePremiumModal } from "@/app/(app)/premium/PremiumModal";
import { Toggle } from "@/components/Toggle";
import {
fastCategorizeSendersAction,
setAutoCategorizeAction,
} from "@/utils/actions/categorize";
import { setAutoCategorizeAction } from "@/utils/actions/categorize";
import { TooltipExplanation } from "@/components/TooltipExplanation";
import { isActionError } from "@/utils/error";

type FastCategorizeResults = Record<string, string | undefined>;

export function Uncategorized({
categories,
Expand All @@ -48,23 +37,13 @@ export function Uncategorized({

const { data: senderAddresses, loadMore, isLoading, hasMore } = useSenders();
const hasProcessingItems = useHasProcessingItems();
const [isFastCategorizing, setIsFastCategorizing] = useState(false);
const [fastCategorizeResult, setFastCategorizeResult] =
useState<FastCategorizeResults | null>(null);

const senders = useMemo(
() =>
senderAddresses?.map((address) => {
const fastCategorization = fastCategorizeResult?.[address];

if (!fastCategorization) return { address, category: null };

const category =
categories.find((c) => c.name === fastCategorization) || null;

return { address, category };
return { address, category: null };
}),
[senderAddresses, fastCategorizeResult, categories],
[senderAddresses],
);

const session = useSession();
Expand All @@ -80,7 +59,7 @@ export function Uncategorized({
>
<Button
loading={hasProcessingItems}
disabled={!hasAiAccess || isFastCategorizing}
disabled={!hasAiAccess}
onClick={async () => {
if (!senderAddresses?.length) {
toastError({ description: "No senders to categorize" });
Expand All @@ -95,40 +74,6 @@ export function Uncategorized({
</Button>
</PremiumTooltip>

<PremiumTooltip
showTooltip={!hasAiAccess}
openModal={openPremiumModal}
>
<Button
loading={isFastCategorizing}
disabled={!hasAiAccess || hasProcessingItems}
onClick={async () => {
if (!senderAddresses?.length) {
toastError({ description: "No senders to categorize" });
return;
}

setIsFastCategorizing(true);

const result =
await fastCategorizeSendersAction(senderAddresses);

if (isActionError(result)) {
toastError({ description: result.error });
} else {
if (result.results) setFastCategorizeResult(result.results);

toastSuccess({ description: "Categorized senders!" });
}

setIsFastCategorizing(false);
}}
>
<ZapIcon className="mr-2 size-4" />
Fast categorize
</Button>
</PremiumTooltip>

{hasProcessingItems && (
<Button
variant="outline"
Expand All @@ -143,7 +88,7 @@ export function Uncategorized({
</div>

<div className="flex items-center">
<div className="mr-1">
<div className="mr-1.5">
<TooltipExplanation
size="sm"
text="Automatically categorize new senders when they email you"
Expand Down
Loading