diff --git a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx index 317eb8df6..b48b3db13 100644 --- a/apps/web/app/(app)/automation/ExecutedRulesTable.tsx +++ b/apps/web/app/(app)/automation/ExecutedRulesTable.tsx @@ -14,29 +14,35 @@ 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"; +import { ViewEmailButton } from "@/components/ViewEmailButton"; 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 +52,7 @@ export function EmailCell({ {decodeSnippet(snippet)}
+
); } @@ -136,14 +143,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 21fba80ab..21563e170 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 695b25034..a3ee7f3ba 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)/automation/ProcessRules.tsx b/apps/web/app/(app)/automation/ProcessRules.tsx index b3bf618c8..1806a3481 100644 --- a/apps/web/app/(app)/automation/ProcessRules.tsx +++ b/apps/web/app/(app)/automation/ProcessRules.tsx @@ -258,6 +258,7 @@ function ProcessRulesRow({ subject={message.headers.subject} snippet={message.snippet?.trim() || ""} userEmail={userEmail} + threadId={message.threadId} messageId={message.id} />
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 e31a6c6c6..ceea49591 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 dc38943e6..6238a9b57 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 eb9e97a50..f6a3e2e16 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 3ff252281..1d2da27d8 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={} > - @@ -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 161cb7c99..d1109f26d 100644 --- a/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx +++ b/apps/web/app/(app)/cold-email-blocker/TestRulesMessage.tsx @@ -5,18 +5,21 @@ import Link from "next/link"; import { MessageText } from "@/components/Typography"; import { getGmailUrl } from "@/utils/url"; import { decodeSnippet } from "@/utils/gmail/decode"; +import { ViewEmailButton } from "@/components/ViewEmailButton"; export function TestRulesMessage({ from, userEmail, subject, snippet, + threadId, messageId, }: { from: string; userEmail: string; subject: string; snippet: string; + threadId: string; messageId: string; }) { return ( @@ -30,6 +33,12 @@ export function TestRulesMessage({ > + {subject} diff --git a/apps/web/app/(app)/cold-email-blocker/page.tsx b/apps/web/app/(app)/cold-email-blocker/page.tsx index 318105614..5dce47e54 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
diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 78e3bd9cc..9560368b2 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 ad5b80bd1..cc716bfc8 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/CommandK.tsx b/apps/web/components/CommandK.tsx index d522389c4..b7debcdd5 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 000000000..324f8b667 --- /dev/null +++ b/apps/web/components/EmailViewer.tsx @@ -0,0 +1,46 @@ +"use client"; + +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"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; + +export function EmailViewer() { + const { threadId, showEmail } = useDisplayedEmail(); + + const hideEmail = useCallback(() => showEmail(null), [showEmail]); + + return ( + + + {threadId && } + + + ); +} + +function EmailContent({ threadId }: { threadId: string }) { + const { data, isLoading, error, mutate } = useThread({ id: threadId }); + + return ( + + + {data && ( + + )} + + + ); +} diff --git a/apps/web/components/GroupedTable.tsx b/apps/web/components/GroupedTable.tsx index 338320686..00f0a85f8 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) => ( - + + + + + + ); +} diff --git a/apps/web/components/email-list/EmailList.tsx b/apps/web/components/email-list/EmailList.tsx index d11de0595..f670c823d 100644 --- a/apps/web/components/email-list/EmailList.tsx +++ b/apps/web/components/email-list/EmailList.tsx @@ -1,12 +1,11 @@ "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"; import { toast } from "sonner"; -import { useAtom } from "jotai"; import { ChevronsDownIcon } from "lucide-react"; import { ActionButtonsBulk } from "@/components/ActionButtonsBulk"; import { Celebration } from "@/components/Celebration"; @@ -27,7 +26,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"; @@ -52,8 +50,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,15 +179,15 @@ export function EmailList({ }) { const session = useSession(); // if right panel is open - const [openedRowId, setOpenedRowId] = useAtom(selectedEmailAtom); + const [openThreadId, setOpenThreadId] = useQueryState("thread-id"); const closePanel = useCallback( - () => setOpenedRowId(undefined), - [setOpenedRowId], + () => 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 @@ -485,8 +482,8 @@ export function EmailList({ > {threads.map((thread) => { const onOpen = () => { - const alreadyOpen = !!openedRowId; - setOpenedRowId(thread.id); + const alreadyOpen = !!openThreadId; + setOpenThreadId(thread.id); if (!alreadyOpen) scrollToId(thread.id); @@ -506,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} @@ -547,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 7a3815c71..880c2e16a 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"; @@ -20,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; @@ -34,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 (
@@ -57,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} /> - @@ -79,31 +90,41 @@ export function EmailPanel(props: {
{plan?.rule && ( )} - +
); } -function EmailThread(props: { +export function EmailThread({ + messages, + refetch, + showReplyButton, +}: { messages: Thread["messages"]; refetch: () => void; + showReplyButton: boolean; }) { return (
    - {props.messages?.map((message) => ( + {messages?.map((message) => ( ))}
@@ -111,12 +132,15 @@ 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); @@ -165,35 +189,22 @@ function EmailMessage(props: { {formatShortDate(new Date(message.headers.date))}

-
- - - - - - - - {/* - + {showReplyButton && ( +
+ + + + - - - Delete this message - Report spam - Mark as unread - Open in Gmail - - */} -
+ +
+ )}
@@ -242,7 +253,7 @@ function EmailMessage(props: { : prepareForwardingEmail(message) } novelEditorClassName="h-40 overflow-auto" - refetch={props.refetch} + refetch={refetch} onSuccess={onCloseCompose} onDiscard={onCloseCompose} /> @@ -253,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) => { @@ -281,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) { diff --git a/apps/web/components/ui/sheet.tsx b/apps/web/components/ui/sheet.tsx index 222c7e3f7..bb91cd231 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 new file mode 100644 index 000000000..861403dd0 --- /dev/null +++ b/apps/web/hooks/useDisplayedEmail.ts @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { useQueryState } from "nuqs"; + +export const useDisplayedEmail = () => { + const [threadId, setThreadId] = useQueryState("side-panel-thread-id"); + const [messageId, setMessageId] = useQueryState("side-panel-message-id"); + + const showEmail = useCallback( + ( + options: { + threadId: string; + messageId?: string; + } | null, + ) => { + setThreadId(options?.threadId ?? null); + setMessageId(options?.messageId ?? null); + }, + [setMessageId, setThreadId], + ); + + return { + threadId, + messageId, + showEmail, + }; +}; diff --git a/apps/web/hooks/useThread.ts b/apps/web/hooks/useThread.ts new file mode 100644 index 000000000..f3540ea2e --- /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); +} diff --git a/apps/web/store/email.ts b/apps/web/store/email.ts index a73025ba8..10614501b 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);