From e753378769060d04f44e7fdaa6db7a94940b8ac7 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:58:50 +0200 Subject: [PATCH 01/10] Use query params for open email id --- apps/web/components/CommandK.tsx | 17 ++++++----- apps/web/components/EmailViewer.tsx | 29 ++++++++++++++++++ apps/web/components/email-list/EmailList.tsx | 19 ++++++------ apps/web/hooks/useDisplayedEmail.ts | 31 ++++++++++++++++++++ apps/web/store/email.ts | 1 - 5 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 apps/web/components/EmailViewer.tsx create mode 100644 apps/web/hooks/useDisplayedEmail.ts diff --git a/apps/web/components/CommandK.tsx b/apps/web/components/CommandK.tsx index d522389c..b7debcdd 100644 --- a/apps/web/components/CommandK.tsx +++ b/apps/web/components/CommandK.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import { useRouter } from "next/navigation"; import { ArchiveIcon, PenLineIcon } from "lucide-react"; -import { useAtom, useAtomValue } from "jotai"; +import { useAtomValue } from "jotai"; import { CommandDialog, CommandEmpty, @@ -15,28 +15,29 @@ import { } from "@/components/ui/command"; import { useNavigation } from "@/components/SideNav"; import { useComposeModal } from "@/providers/ComposeModalProvider"; -import { refetchEmailListAtom, selectedEmailAtom } from "@/store/email"; +import { refetchEmailListAtom } from "@/store/email"; import { archiveEmails } from "@/store/archive-queue"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function CommandK() { const [open, setOpen] = React.useState(false); const router = useRouter(); - const [selectedEmail, setSelectedEmail] = useAtom(selectedEmailAtom); + const { threadId, showEmail } = useDisplayedEmail(); const refreshEmailList = useAtomValue(refetchEmailListAtom); const { onOpen: onOpenComposeModal } = useComposeModal(); const onArchive = React.useCallback(() => { - if (selectedEmail) { - const threadIds = [selectedEmail]; + if (threadId) { + const threadIds = [threadId]; archiveEmails(threadIds, undefined, () => { return refreshEmailList?.refetch({ removedThreadIds: threadIds }); }); - setSelectedEmail(undefined); + showEmail(null); } - }, [refreshEmailList, selectedEmail, setSelectedEmail]); + }, [refreshEmailList, threadId, showEmail]); React.useEffect(() => { const down = (e: KeyboardEvent) => { @@ -89,7 +90,7 @@ export function CommandK() { No results found. - {selectedEmail && ( + {threadId && ( { onArchive(); diff --git a/apps/web/components/EmailViewer.tsx b/apps/web/components/EmailViewer.tsx new file mode 100644 index 00000000..b770b035 --- /dev/null +++ b/apps/web/components/EmailViewer.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useCallback } from "react"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; + +export function EmailViewer() { + const { messageId, showEmail } = useDisplayedEmail(); + + const hideEmail = useCallback(() => showEmail(null), [showEmail]); + + return ( + + + {messageId && } + + + ); +} + +function EmailContent({ messageId }: { messageId: string }) { + // Fetch and display email content + return ( +
+ {/* Email content */} +

Email {messageId}

+
+ ); +} diff --git a/apps/web/components/email-list/EmailList.tsx b/apps/web/components/email-list/EmailList.tsx index d11de059..1b5a9a60 100644 --- a/apps/web/components/email-list/EmailList.tsx +++ b/apps/web/components/email-list/EmailList.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useRef, useState, useMemo } from "react"; -import { useSearchParams } from "next/navigation"; +import { useQueryState } from "nuqs"; import countBy from "lodash/countBy"; import { capitalCase } from "capital-case"; import Link from "next/link"; @@ -27,7 +27,6 @@ import { ResizablePanelGroup, } from "@/components/ui/resizable"; import { runAiRules } from "@/utils/queue/email-actions"; -import { selectedEmailAtom } from "@/store/email"; import { categorizeEmailAction } from "@/utils/actions/categorize-email"; import { Button } from "@/components/ui/button"; import { ButtonLoader } from "@/components/Loading"; @@ -36,6 +35,7 @@ import { deleteEmails, markReadThreads, } from "@/store/archive-queue"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function List({ emails, @@ -52,8 +52,7 @@ export function List({ isLoadingMore?: boolean; handleLoadMore?: () => void; }) { - const params = useSearchParams(); - const selectedTab = params.get("tab") || "all"; + const [selectedTab] = useQueryState("tab", { defaultValue: "all" }); const categories = useMemo(() => { return countBy( @@ -182,11 +181,8 @@ export function EmailList({ }) { const session = useSession(); // if right panel is open - const [openedRowId, setOpenedRowId] = useAtom(selectedEmailAtom); - const closePanel = useCallback( - () => setOpenedRowId(undefined), - [setOpenedRowId], - ); + const { threadId: openedRowId, showEmail } = useDisplayedEmail(); + const closePanel = useCallback(() => showEmail(null), [showEmail]); const openedRow = useMemo( () => threads.find((thread) => thread.id === openedRowId), @@ -486,7 +482,10 @@ export function EmailList({ {threads.map((thread) => { const onOpen = () => { const alreadyOpen = !!openedRowId; - setOpenedRowId(thread.id); + showEmail({ + messageId: thread.id, + threadId: thread.id, + }); if (!alreadyOpen) scrollToId(thread.id); diff --git a/apps/web/hooks/useDisplayedEmail.ts b/apps/web/hooks/useDisplayedEmail.ts new file mode 100644 index 00000000..02132e8a --- /dev/null +++ b/apps/web/hooks/useDisplayedEmail.ts @@ -0,0 +1,31 @@ +import { useCallback } from "react"; +import { useQueryState } from "nuqs"; + +export const useDisplayedEmail = () => { + const [messageId, setMessageId] = useQueryState("messageId"); + const [threadId, setThreadId] = useQueryState("threadId"); + + const showEmail = useCallback( + ( + options: { + messageId: string; + threadId: string; + } | null, + ) => { + if (options) { + setMessageId(options.messageId); + setThreadId(options.threadId); + } else { + setMessageId(null); + setThreadId(null); + } + }, + [setMessageId, setThreadId], + ); + + return { + messageId, + threadId, + showEmail, + }; +}; diff --git a/apps/web/store/email.ts b/apps/web/store/email.ts index a73025ba..10614501 100644 --- a/apps/web/store/email.ts +++ b/apps/web/store/email.ts @@ -1,6 +1,5 @@ import { atom } from "jotai"; -export const selectedEmailAtom = atom(undefined); export const refetchEmailListAtom = atom< { refetch: (options?: { removedThreadIds?: string[] }) => void } | undefined >(undefined); From 62feb92ceb5232d3809419a1ca491b546c70ea82 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Fri, 10 Jan 2025 18:01:49 +0200 Subject: [PATCH 02/10] load email in side panel --- .../(app)/automation/ExecutedRulesTable.tsx | 27 +++++--- apps/web/app/(app)/automation/History.tsx | 2 + apps/web/app/(app)/automation/Pending.tsx | 3 +- apps/web/app/(app)/layout.tsx | 2 + .../setup/SetUpCategories.tsx | 2 +- apps/web/components/EmailViewer.tsx | 28 +++++--- apps/web/components/email-list/EmailList.tsx | 32 ++++----- apps/web/components/email-list/EmailPanel.tsx | 2 +- apps/web/components/ui/sheet.tsx | 68 +++++++++++++------ apps/web/hooks/useDisplayedEmail.ts | 17 ++--- apps/web/hooks/useThread.ts | 10 +++ 11 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 apps/web/hooks/useThread.ts diff --git a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx index 317eb8df..7b10d1c1 100644 --- a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx +++ b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { ExternalLinkIcon, EyeIcon } from "lucide-react"; +import { ExternalLinkIcon, EyeIcon, MailIcon } from "lucide-react"; import type { PendingExecutedRules } from "@/app/api/user/planned/route"; import { decodeSnippet } from "@/utils/gmail/decode"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -14,29 +14,34 @@ import { conditionsToString, conditionTypesToString } from "@/utils/condition"; import { MessageText } from "@/components/Typography"; import { ReportMistake } from "@/app/(app)/automation/ReportMistake"; import type { ParsedMessage } from "@/utils/types"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function EmailCell({ from, subject, snippet, + threadId, messageId, userEmail, }: { from: string; subject: string; snippet: string; + threadId: string; messageId: string; userEmail: string; }) { // use regex to find first letter const firstLetter = from.match(/[a-zA-Z]/)?.[0] || "-"; + const { showEmail } = useDisplayedEmail(); + return (
{firstLetter} -
+
{from}
{subject}{" "} @@ -46,6 +51,14 @@ export function EmailCell({ {decodeSnippet(snippet)}
+
); } @@ -136,14 +149,12 @@ function OpenInGmailButton({ userEmail: string; }) { return ( - + ); } diff --git a/apps/web/app/(app)/automation/History.tsx b/apps/web/app/(app)/automation/History.tsx index 21fba80a..21563e17 100644 --- a/apps/web/app/(app)/automation/History.tsx +++ b/apps/web/app/(app)/automation/History.tsx @@ -24,6 +24,7 @@ import { import { TablePagination } from "@/components/TablePagination"; import { Badge } from "@/components/Badge"; import { RulesSelect } from "@/app/(app)/automation/RulesSelect"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function History() { const [page] = useQueryState("page", parseAsInteger.withDefault(1)); @@ -95,6 +96,7 @@ function HistoryTable({ from={p.message.headers.from} subject={p.message.headers.subject} snippet={p.message.snippet} + threadId={p.message.threadId} messageId={p.message.id} userEmail={userEmail} /> diff --git a/apps/web/app/(app)/automation/Pending.tsx b/apps/web/app/(app)/automation/Pending.tsx index 695b2503..a3ee7f3b 100644 --- a/apps/web/app/(app)/automation/Pending.tsx +++ b/apps/web/app/(app)/automation/Pending.tsx @@ -35,7 +35,7 @@ import { RulesSelect } from "@/app/(app)/automation/RulesSelect"; export function Pending() { const [page] = useQueryState("page", parseAsInteger.withDefault(1)); const [ruleId, setRuleId] = useQueryState( - "ruleId", + "rule-id", parseAsString.withDefault("all"), ); @@ -179,6 +179,7 @@ function PendingTable({ from={p.message.headers.from} subject={p.message.headers.subject} snippet={p.message.snippet} + threadId={p.message.threadId} messageId={p.message.id} userEmail={userEmail} /> diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 78e3bd9c..9560368b 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -15,6 +15,7 @@ import { SentryIdentify } from "@/app/(app)/sentry-identify"; import { ErrorMessages } from "@/app/(app)/ErrorMessages"; import { QueueInitializer } from "@/store/QueueInitializer"; import { ErrorBoundary } from "@/components/ErrorBoundary"; +import { EmailViewer } from "@/components/EmailViewer"; export const viewport = { themeColor: "#FFF", @@ -43,6 +44,7 @@ export default async function AppLayout({ {children} + diff --git a/apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx b/apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx index ad5b80bd..cc716bfc 100644 --- a/apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx +++ b/apps/web/app/(app)/smart-categories/setup/SetUpCategories.tsx @@ -47,7 +47,7 @@ export function SetUpCategories({ const [isCreating, setIsCreating] = useState(false); const router = useRouter(); const [selectedCategoryName, setSelectedCategoryName] = - useQueryState("categoryName"); + useQueryState("category-name"); const combinedCategories = uniqBy( [ diff --git a/apps/web/components/EmailViewer.tsx b/apps/web/components/EmailViewer.tsx index b770b035..23059a20 100644 --- a/apps/web/components/EmailViewer.tsx +++ b/apps/web/components/EmailViewer.tsx @@ -3,27 +3,35 @@ import { useCallback } from "react"; import { Sheet, SheetContent } from "@/components/ui/sheet"; import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; +import { EmailThread } from "@/components/email-list/EmailPanel"; +import { useThread } from "@/hooks/useThread"; +import { LoadingContent } from "@/components/LoadingContent"; export function EmailViewer() { - const { messageId, showEmail } = useDisplayedEmail(); + const { threadId, showEmail } = useDisplayedEmail(); const hideEmail = useCallback(() => showEmail(null), [showEmail]); return ( - - - {messageId && } + + + {threadId && } ); } -function EmailContent({ messageId }: { messageId: string }) { - // Fetch and display email content +function EmailContent({ threadId }: { threadId: string }) { + const { data, isLoading, error, mutate } = useThread({ id: threadId }); + return ( -
- {/* Email content */} -

Email {messageId}

-
+ + {data && } + ); } diff --git a/apps/web/components/email-list/EmailList.tsx b/apps/web/components/email-list/EmailList.tsx index 1b5a9a60..f670c823 100644 --- a/apps/web/components/email-list/EmailList.tsx +++ b/apps/web/components/email-list/EmailList.tsx @@ -6,7 +6,6 @@ import countBy from "lodash/countBy"; import { capitalCase } from "capital-case"; import Link from "next/link"; import { toast } from "sonner"; -import { useAtom } from "jotai"; import { ChevronsDownIcon } from "lucide-react"; import { ActionButtonsBulk } from "@/components/ActionButtonsBulk"; import { Celebration } from "@/components/Celebration"; @@ -35,7 +34,6 @@ import { deleteEmails, markReadThreads, } from "@/store/archive-queue"; -import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function List({ emails, @@ -181,12 +179,15 @@ export function EmailList({ }) { const session = useSession(); // if right panel is open - const { threadId: openedRowId, showEmail } = useDisplayedEmail(); - const closePanel = useCallback(() => showEmail(null), [showEmail]); + const [openThreadId, setOpenThreadId] = useQueryState("thread-id"); + const closePanel = useCallback( + () => setOpenThreadId(null), + [setOpenThreadId], + ); const openedRow = useMemo( - () => threads.find((thread) => thread.id === openedRowId), - [openedRowId, threads], + () => threads.find((thread) => thread.id === openThreadId), + [openThreadId, threads], ); // if checkbox for a row has been checked @@ -481,11 +482,8 @@ export function EmailList({ > {threads.map((thread) => { const onOpen = () => { - const alreadyOpen = !!openedRowId; - showEmail({ - messageId: thread.id, - threadId: thread.id, - }); + const alreadyOpen = !!openThreadId; + setOpenThreadId(thread.id); if (!alreadyOpen) scrollToId(thread.id); @@ -505,11 +503,11 @@ export function EmailList({ }} userEmailAddress={session.data?.user.email || ""} thread={thread} - opened={openedRowId === thread.id} + opened={openThreadId === thread.id} closePanel={closePanel} selected={selectedRows[thread.id]} onSelected={onSetSelectedRow} - splitView={!!openedRowId} + splitView={!!openThreadId} onClick={onOpen} isCategorizing={isCategorizing[thread.id]} onPlanAiAction={onPlanAiAction} @@ -546,18 +544,18 @@ export function EmailList({ } right={ - !!(openedRowId && openedRow) && ( + !!(openThreadId && openedRow) && ( ) diff --git a/apps/web/components/email-list/EmailPanel.tsx b/apps/web/components/email-list/EmailPanel.tsx index 7a3815c7..742266ca 100644 --- a/apps/web/components/email-list/EmailPanel.tsx +++ b/apps/web/components/email-list/EmailPanel.tsx @@ -92,7 +92,7 @@ export function EmailPanel(props: { ); } -function EmailThread(props: { +export function EmailThread(props: { messages: Thread["messages"]; refetch: () => void; }) { diff --git a/apps/web/components/ui/sheet.tsx b/apps/web/components/ui/sheet.tsx index 222c7e3f..bb91cd23 100644 --- a/apps/web/components/ui/sheet.tsx +++ b/apps/web/components/ui/sheet.tsx @@ -15,13 +15,20 @@ const SheetClose = SheetPrimitive.Close; const SheetPortal = SheetPrimitive.Portal; +interface SheetOverlayProps + extends React.ComponentPropsWithoutRef { + overlay?: "default" | "transparent"; +} + const SheetOverlay = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + SheetOverlayProps +>(({ className, overlay = "default", ...props }, ref) => ( , - SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)); + SheetContentProps & { overlay?: "default" | "transparent" } +>( + ( + { + side = "right", + size = "sm", + overlay = "default", + className, + children, + ...props + }, + ref, + ) => ( + + + + {children} + + + Close + + + + ), +); SheetContent.displayName = SheetPrimitive.Content.displayName; const SheetHeader = ({ diff --git a/apps/web/hooks/useDisplayedEmail.ts b/apps/web/hooks/useDisplayedEmail.ts index 02132e8a..861403dd 100644 --- a/apps/web/hooks/useDisplayedEmail.ts +++ b/apps/web/hooks/useDisplayedEmail.ts @@ -2,30 +2,25 @@ import { useCallback } from "react"; import { useQueryState } from "nuqs"; export const useDisplayedEmail = () => { - const [messageId, setMessageId] = useQueryState("messageId"); - const [threadId, setThreadId] = useQueryState("threadId"); + const [threadId, setThreadId] = useQueryState("side-panel-thread-id"); + const [messageId, setMessageId] = useQueryState("side-panel-message-id"); const showEmail = useCallback( ( options: { - messageId: string; threadId: string; + messageId?: string; } | null, ) => { - if (options) { - setMessageId(options.messageId); - setThreadId(options.threadId); - } else { - setMessageId(null); - setThreadId(null); - } + setThreadId(options?.threadId ?? null); + setMessageId(options?.messageId ?? null); }, [setMessageId, setThreadId], ); return { - messageId, threadId, + messageId, showEmail, }; }; diff --git a/apps/web/hooks/useThread.ts b/apps/web/hooks/useThread.ts new file mode 100644 index 00000000..f3540ea2 --- /dev/null +++ b/apps/web/hooks/useThread.ts @@ -0,0 +1,10 @@ +import useSWR from "swr"; +import type { + ThreadQuery, + ThreadResponse, +} from "@/app/api/google/threads/[id]/route"; + +export function useThread({ id }: ThreadQuery) { + const url = `/api/google/threads/${id}`; + return useSWR(url); +} From 0454db608792ebf4166ce057926cf50c47d1917a Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 18:34:43 +0200 Subject: [PATCH 03/10] Add side panel to process rules tab --- apps/web/app/(app)/automation/ProcessRules.tsx | 5 +++++ .../app/(app)/cold-email-blocker/TestRules.tsx | 1 + .../cold-email-blocker/TestRulesMessage.tsx | 17 ++++++++++++++++- apps/web/components/email-list/EmailPanel.tsx | 16 ---------------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/apps/web/app/(app)/automation/ProcessRules.tsx b/apps/web/app/(app)/automation/ProcessRules.tsx index b3bf618c..bd297bc3 100644 --- a/apps/web/app/(app)/automation/ProcessRules.tsx +++ b/apps/web/app/(app)/automation/ProcessRules.tsx @@ -12,6 +12,7 @@ import { PauseIcon, ChevronsDownIcon, RefreshCcwIcon, + MailIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toastError } from "@/components/Toast"; @@ -38,6 +39,7 @@ import { BulkRunRules } from "@/app/(app)/automation/BulkRunRules"; import { cn } from "@/utils"; import { TestCustomEmailForm } from "@/app/(app)/automation/TestCustomEmailForm"; import { ProcessResultDisplay } from "@/app/(app)/automation/ProcessResultDisplay"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; type Message = MessagesResponse["messages"][number]; @@ -245,6 +247,8 @@ function ProcessRulesRow({ onRun: (rerun?: boolean) => void; testMode: boolean; }) { + const { showEmail } = useDisplayedEmail(); + return (
diff --git a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx index 3ff25228..ecb2eea9 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx @@ -175,6 +175,7 @@ function TestRulesContentRow(props: { subject={message.headers.subject} snippet={decodeSnippet(message.snippet)} userEmail={props.userEmail} + threadId={message.threadId} messageId={message.id} />
diff --git a/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx b/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx index 161cb7c9..0842c859 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx @@ -1,24 +1,30 @@ "use client"; -import { ExternalLinkIcon } from "lucide-react"; +import { ExternalLinkIcon, MailIcon } from "lucide-react"; import Link from "next/link"; import { MessageText } from "@/components/Typography"; import { getGmailUrl } from "@/utils/url"; import { decodeSnippet } from "@/utils/gmail/decode"; +import { Button } from "@/components/ui/button"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; export function TestRulesMessage({ from, userEmail, subject, snippet, + threadId, messageId, }: { from: string; userEmail: string; subject: string; snippet: string; + threadId: string; messageId: string; }) { + const { showEmail } = useDisplayedEmail(); + return (
@@ -30,6 +36,15 @@ export function TestRulesMessage({ > + {subject} diff --git a/apps/web/components/email-list/EmailPanel.tsx b/apps/web/components/email-list/EmailPanel.tsx index 742266ca..722e636b 100644 --- a/apps/web/components/email-list/EmailPanel.tsx +++ b/apps/web/components/email-list/EmailPanel.tsx @@ -1,6 +1,5 @@ import { type SyntheticEvent, useCallback, useMemo, useState } from "react"; import Link from "next/link"; -import { useAtomValue } from "jotai"; import { DownloadIcon, ForwardIcon, ReplyIcon, XIcon } from "lucide-react"; import { ActionButtons } from "@/components/ActionButtons"; import { Tooltip } from "@/components/Tooltip"; @@ -178,21 +177,6 @@ function EmailMessage(props: { Forward - - {/* - - - - - Delete this message - Report spam - Mark as unread - Open in Gmail - - */}
From 97f2361cf5a50074bda70b835a4706d9179e148f Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:03:13 +0200 Subject: [PATCH 04/10] Switch to eye icon --- apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx b/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx index 0842c859..1cb0d5bb 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx @@ -1,6 +1,6 @@ "use client"; -import { ExternalLinkIcon, MailIcon } from "lucide-react"; +import { ExternalLinkIcon, EyeIcon } from "lucide-react"; import Link from "next/link"; import { MessageText } from "@/components/Typography"; import { getGmailUrl } from "@/utils/url"; @@ -42,7 +42,7 @@ export function TestRulesMessage({ size="xs" onClick={() => showEmail({ threadId, messageId })} > - + View email From 14282f30259362e51e0c3182fd5dc25423098a1a Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:03:47 +0200 Subject: [PATCH 05/10] Adjust cold email page --- ...romptModal.tsx => ColdEmailPromptForm.tsx} | 40 ++----------------- .../cold-email-blocker/ColdEmailRejected.tsx | 4 +- .../cold-email-blocker/ColdEmailSettings.tsx | 12 +++--- .../(app)/cold-email-blocker/TestRules.tsx | 2 +- .../web/app/(app)/cold-email-blocker/page.tsx | 2 +- 5 files changed, 13 insertions(+), 47 deletions(-) rename apps/web/app/(app)/cold-email-blocker/{ColdEmailPromptModal.tsx => ColdEmailPromptForm.tsx} (67%) diff --git a/apps/web/app/(app)/cold-email-blocker/ColdEmailPromptModal.tsx b/apps/web/app/(app)/cold-email-blocker/ColdEmailPromptForm.tsx similarity index 67% rename from apps/web/app/(app)/cold-email-blocker/ColdEmailPromptModal.tsx rename to apps/web/app/(app)/cold-email-blocker/ColdEmailPromptForm.tsx index e31a6c6c..ceea4959 100644 --- a/apps/web/app/(app)/cold-email-blocker/ColdEmailPromptModal.tsx +++ b/apps/web/app/(app)/cold-email-blocker/ColdEmailPromptForm.tsx @@ -1,8 +1,6 @@ import { useCallback } from "react"; import { type SubmitHandler, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { PenIcon } from "lucide-react"; -import { Modal, useModal } from "@/components/Modal"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/Input"; import { @@ -15,39 +13,7 @@ import { DEFAULT_COLD_EMAIL_PROMPT } from "@/app/api/ai/cold-email/prompt"; import { toastError, toastSuccess } from "@/components/Toast"; import { isErrorMessage } from "@/utils/error"; -export function ColdEmailPromptModal(props: { - coldEmailPrompt?: string | null; - refetch: () => void; -}) { - const { isModalOpen, openModal, closeModal } = useModal(); - const { refetch } = props; - - const onSuccess = useCallback(() => { - refetch(); - closeModal(); - }, [closeModal, refetch]); - - return ( - <> - - - - - - ); -} - -function ColdEmailPromptForm(props: { +export function ColdEmailPromptForm(props: { coldEmailPrompt?: string | null; onSuccess: () => void; }) { @@ -95,10 +61,10 @@ function ColdEmailPromptForm(props: { autosizeTextarea rows={10} name="coldEmailPrompt" - label="Prompt to classify cold emails." + label="Prompt to classify cold emails" registerProps={register("coldEmailPrompt")} error={errors.coldEmailPrompt} - explainText=" The default prompt we use is shown above if none set. Use a similar style for best results. Delete your prompt to revert to the default prompt." + explainText="Adjust to your needs.Use a similar style for best results. Delete your prompt to revert to the default prompt." />
diff --git a/apps/web/app/(app)/cold-email-blocker/ColdEmailRejected.tsx b/apps/web/app/(app)/cold-email-blocker/ColdEmailRejected.tsx index dc38943e..6238a9b5 100644 --- a/apps/web/app/(app)/cold-email-blocker/ColdEmailRejected.tsx +++ b/apps/web/app/(app)/cold-email-blocker/ColdEmailRejected.tsx @@ -112,8 +112,8 @@ function NoRejectedColdEmails() { return (
); diff --git a/apps/web/app/(app)/cold-email-blocker/ColdEmailSettings.tsx b/apps/web/app/(app)/cold-email-blocker/ColdEmailSettings.tsx index eb9e97a5..f6a3e2e1 100644 --- a/apps/web/app/(app)/cold-email-blocker/ColdEmailSettings.tsx +++ b/apps/web/app/(app)/cold-email-blocker/ColdEmailSettings.tsx @@ -15,7 +15,7 @@ import { updateColdEmailSettingsBody, } from "@/app/api/user/settings/cold-email/validation"; import { TestRules } from "@/app/(app)/cold-email-blocker/TestRules"; -import { ColdEmailPromptModal } from "@/app/(app)/cold-email-blocker/ColdEmailPromptModal"; +import { ColdEmailPromptForm } from "@/app/(app)/cold-email-blocker/ColdEmailPromptForm"; import { RadioGroup } from "@/components/RadioGroup"; import { useUser } from "@/hooks/useUser"; @@ -26,12 +26,12 @@ export function ColdEmailSettings() { {data && ( <> - -
- - +
+ +
diff --git a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx index ecb2eea9..1d2da27d 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRules.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRules.tsx @@ -33,7 +33,7 @@ export function TestRules() { description="Test which emails are flagged as cold emails. We also check if the sender has emailed you before and if it includes unsubscribe links." content={} > - diff --git a/apps/web/app/(app)/cold-email-blocker/page.tsx b/apps/web/app/(app)/cold-email-blocker/page.tsx index 31810561..5dce47e5 100644 --- a/apps/web/app/(app)/cold-email-blocker/page.tsx +++ b/apps/web/app/(app)/cold-email-blocker/page.tsx @@ -19,7 +19,7 @@ export default function ColdEmailBlockerPage() {
Cold Emails - Not Cold + Marked Not Cold Settings
From ad96aefeb5b0ddba1563bce20ac9ef955d6cf00c Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:04:02 +0200 Subject: [PATCH 06/10] extract view email button --- .../(app)/automation/ExecutedRulesTable.tsx | 12 +++------- apps/web/components/ViewEmailButton.tsx | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 apps/web/components/ViewEmailButton.tsx diff --git a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx index 7b10d1c1..b48b3db1 100644 --- a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx +++ b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { ExternalLinkIcon, EyeIcon, MailIcon } from "lucide-react"; +import { ExternalLinkIcon, EyeIcon } from "lucide-react"; import type { PendingExecutedRules } from "@/app/api/user/planned/route"; import { decodeSnippet } from "@/utils/gmail/decode"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; @@ -15,6 +15,7 @@ import { MessageText } from "@/components/Typography"; import { ReportMistake } from "@/app/(app)/automation/ReportMistake"; import type { ParsedMessage } from "@/utils/types"; import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; +import { ViewEmailButton } from "@/components/ViewEmailButton"; export function EmailCell({ from, @@ -51,14 +52,7 @@ export function EmailCell({ {decodeSnippet(snippet)}
- +
); } diff --git a/apps/web/components/ViewEmailButton.tsx b/apps/web/components/ViewEmailButton.tsx new file mode 100644 index 00000000..fe82d03e --- /dev/null +++ b/apps/web/components/ViewEmailButton.tsx @@ -0,0 +1,24 @@ +import { MailIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; + +export function ViewEmailButton({ + threadId, + messageId, +}: { + threadId: string; + messageId: string; +}) { + const { showEmail } = useDisplayedEmail(); + + return ( + + ); +} From 311de7dab39e57fae45e624ceaf198bfc4d27b76 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:19:16 +0200 Subject: [PATCH 07/10] Add view email button to smart categories page --- apps/web/components/GroupedTable.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/components/GroupedTable.tsx b/apps/web/components/GroupedTable.tsx index 33832068..00f0a85f 100644 --- a/apps/web/components/GroupedTable.tsx +++ b/apps/web/components/GroupedTable.tsx @@ -59,6 +59,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import type { CategoryWithRules } from "@/utils/category.server"; +import { ViewEmailButton } from "@/components/ViewEmailButton"; const COLUMNS = 4; @@ -540,7 +541,9 @@ function ExpandedRows({ <> {data.threads.map((thread) => ( - + + + Date: Sat, 11 Jan 2025 22:25:38 +0200 Subject: [PATCH 08/10] Use same mail icon across app --- .../web/app/(app)/automation/ProcessRules.tsx | 4 ---- .../cold-email-blocker/TestRulesMessage.tsx | 20 ++++++---------- apps/web/components/ViewEmailButton.tsx | 24 ++++++++++++------- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/apps/web/app/(app)/automation/ProcessRules.tsx b/apps/web/app/(app)/automation/ProcessRules.tsx index bd297bc3..1806a348 100644 --- a/apps/web/app/(app)/automation/ProcessRules.tsx +++ b/apps/web/app/(app)/automation/ProcessRules.tsx @@ -12,7 +12,6 @@ import { PauseIcon, ChevronsDownIcon, RefreshCcwIcon, - MailIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { toastError } from "@/components/Toast"; @@ -39,7 +38,6 @@ import { BulkRunRules } from "@/app/(app)/automation/BulkRunRules"; import { cn } from "@/utils"; import { TestCustomEmailForm } from "@/app/(app)/automation/TestCustomEmailForm"; import { ProcessResultDisplay } from "@/app/(app)/automation/ProcessResultDisplay"; -import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; type Message = MessagesResponse["messages"][number]; @@ -247,8 +245,6 @@ function ProcessRulesRow({ onRun: (rerun?: boolean) => void; testMode: boolean; }) { - const { showEmail } = useDisplayedEmail(); - return ( @@ -36,15 +33,12 @@ export function TestRulesMessage({ > - + className="ml-1.5" + /> {subject} diff --git a/apps/web/components/ViewEmailButton.tsx b/apps/web/components/ViewEmailButton.tsx index fe82d03e..16af07c2 100644 --- a/apps/web/components/ViewEmailButton.tsx +++ b/apps/web/components/ViewEmailButton.tsx @@ -1,24 +1,32 @@ import { MailIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; +import { Tooltip } from "@/components/Tooltip"; export function ViewEmailButton({ threadId, messageId, + className, + size, }: { threadId: string; messageId: string; + className?: string; + size?: "icon" | "xs"; }) { const { showEmail } = useDisplayedEmail(); return ( - + + + ); } From 56f0914dc3c706c9bb9e636f57fc9d1134c0fb0c Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:29:53 +0200 Subject: [PATCH 09/10] Hide reply button on email side panel --- apps/web/components/EmailViewer.tsx | 8 +- apps/web/components/email-list/EmailPanel.tsx | 113 +++++++++++------- 2 files changed, 77 insertions(+), 44 deletions(-) diff --git a/apps/web/components/EmailViewer.tsx b/apps/web/components/EmailViewer.tsx index 23059a20..226bb18d 100644 --- a/apps/web/components/EmailViewer.tsx +++ b/apps/web/components/EmailViewer.tsx @@ -31,7 +31,13 @@ function EmailContent({ threadId }: { threadId: string }) { return ( - {data && } + {data && ( + + )} ); } diff --git a/apps/web/components/email-list/EmailPanel.tsx b/apps/web/components/email-list/EmailPanel.tsx index 722e636b..880c2e16 100644 --- a/apps/web/components/email-list/EmailPanel.tsx +++ b/apps/web/components/email-list/EmailPanel.tsx @@ -19,7 +19,19 @@ import { } from "@/utils/gmail/forward"; import { useIsInAiQueue } from "@/store/ai-queue"; -export function EmailPanel(props: { +export function EmailPanel({ + row, + isCategorizing, + onPlanAiAction, + onAiCategorize, + onArchive, + close, + executingPlan, + rejectingPlan, + executePlan, + rejectPlan, + refetch, +}: { row: Thread; isCategorizing: boolean; onPlanAiAction: (thread: Thread) => void; @@ -33,11 +45,11 @@ export function EmailPanel(props: { rejectPlan: (thread: Thread) => Promise; refetch: () => void; }) { - const isPlanning = useIsInAiQueue(props.row.id); + const isPlanning = useIsInAiQueue(row.id); - const lastMessage = props.row.messages?.[props.row.messages.length - 1]; + const lastMessage = row.messages?.[row.messages.length - 1]; - const plan = props.row.plan; + const plan = row.plan; return (
@@ -56,19 +68,19 @@ export function EmailPanel(props: {
props.onPlanAiAction(props.row)} - onAiCategorize={() => props.onAiCategorize(props.row)} + isCategorizing={isCategorizing} + onPlanAiAction={() => onPlanAiAction(row)} + onAiCategorize={() => onAiCategorize(row)} onArchive={() => { - props.onArchive(props.row); - props.close(); + onArchive(row); + close(); }} - refetch={props.refetch} + refetch={refetch} /> - @@ -78,31 +90,41 @@ export function EmailPanel(props: {
{plan?.rule && ( )} - +
); } -export function EmailThread(props: { +export function EmailThread({ + messages, + refetch, + showReplyButton, +}: { messages: Thread["messages"]; refetch: () => void; + showReplyButton: boolean; }) { return (
    - {props.messages?.map((message) => ( + {messages?.map((message) => ( ))}
@@ -110,12 +132,15 @@ export function EmailThread(props: { ); } -function EmailMessage(props: { +function EmailMessage({ + message, + refetch, + showReplyButton, +}: { message: Thread["messages"][0]; refetch: () => void; + showReplyButton: boolean; }) { - const { message } = props; - const [showReply, setShowReply] = useState(false); const onReply = useCallback(() => setShowReply(true), []); const [showForward, setShowForward] = useState(false); @@ -164,20 +189,22 @@ function EmailMessage(props: { {formatShortDate(new Date(message.headers.date))}

-
- - - - - - -
+ {showReplyButton && ( +
+ + + + + + +
+ )}
@@ -226,7 +253,7 @@ function EmailMessage(props: { : prepareForwardingEmail(message) } novelEditorClassName="h-40 overflow-auto" - refetch={props.refetch} + refetch={refetch} onSuccess={onCloseCompose} onDiscard={onCloseCompose} /> @@ -237,8 +264,8 @@ function EmailMessage(props: { ); } -export function HtmlEmail(props: { html: string }) { - const srcDoc = useMemo(() => getIframeHtml(props.html), [props.html]); +export function HtmlEmail({ html }: { html: string }) { + const srcDoc = useMemo(() => getIframeHtml(html), [html]); const onLoad = useCallback( (event: SyntheticEvent) => { @@ -265,8 +292,8 @@ export function HtmlEmail(props: { html: string }) { ); } -function PlainEmail(props: { text: string }) { - return
{props.text}
; +function PlainEmail({ text }: { text: string }) { + return
{text}
; } function getIframeHtml(html: string) { From 59151749a777ba2b6722f29056a935cca204fa61 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:33:44 +0200 Subject: [PATCH 10/10] Add error boundary --- apps/web/components/EmailViewer.tsx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/web/components/EmailViewer.tsx b/apps/web/components/EmailViewer.tsx index 226bb18d..324f8b66 100644 --- a/apps/web/components/EmailViewer.tsx +++ b/apps/web/components/EmailViewer.tsx @@ -6,6 +6,7 @@ import { useDisplayedEmail } from "@/hooks/useDisplayedEmail"; import { EmailThread } from "@/components/email-list/EmailPanel"; import { useThread } from "@/hooks/useThread"; import { LoadingContent } from "@/components/LoadingContent"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; export function EmailViewer() { const { threadId, showEmail } = useDisplayedEmail(); @@ -30,14 +31,16 @@ function EmailContent({ threadId }: { threadId: string }) { const { data, isLoading, error, mutate } = useThread({ id: threadId }); return ( - - {data && ( - - )} - + + + {data && ( + + )} + + ); }