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 (
-
{
- window.open(getGmailUrl(messageId, userEmail), "_blank");
- }}
>
-
+
);
}
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 (
- <>
-
-
- Edit Prompt
-
-
-
-
- >
- );
-}
-
-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={
}
>
-
+
Test
@@ -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) => (
-
+
+
+
+ showEmail({ threadId, messageId })}
+ className={className}
+ >
+
+ View email
+
+
+ );
+}
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}
/>
-
+
Close
@@ -79,31 +90,41 @@ export function EmailPanel(props: {
);
}
-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))}
-
-
-
-
- Reply
-
-
-
-
-
- Forward
-
-
-
- {/*
-
+ {showReplyButton && (
+
+
+
+
+ Reply
+
+
+
-
- More
+
+ Forward
-
-
- 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);