Skip to content

Made the email appear on fullscreen #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
170 changes: 170 additions & 0 deletions apps/web/app/(app)/mail/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"use client";

import { useCallback, useEffect, use, useMemo } from "react";
import useSWRInfinite from "swr/infinite";
import { useSetAtom } from "jotai";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { List } from "@/components/email-list/EmailList";
import { LoadingContent } from "@/components/LoadingContent";
import type { ThreadsQuery } from "@/app/api/google/threads/validation";
import type { ThreadsResponse } from "@/app/api/google/threads/controller";
import { refetchEmailListAtom } from "@/store/email";
import { BetaBanner } from "@/app/(app)/mail/BetaBanner";
import { ClientOnly } from "@/components/ClientOnly";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";
import { EmailPanel } from "@/components/email-list/EmailPanel";

export default function Mail({ params }: { params: { slug: string[] } }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const router = useRouter();
const query: ThreadsQuery = {};

// Parse the slug to determine the view type and thread ID
const [mailType, threadId] = params.slug;
const isThreadView = !!threadId;

// Handle different query params
if (mailType === "label" && searchParams.get("labelId")) {
query.labelId = searchParams.get("labelId")!;
} else {
query.type = mailType;
}

const getKey = (
pageIndex: number,
previousPageData: ThreadsResponse | null,
) => {
if (previousPageData && !previousPageData.nextPageToken) return null;
const queryParams = new URLSearchParams();
// Set query params
Object.entries(query).forEach(([key, value]) => {
if (value) queryParams.set(key, String(value));
});
// Append nextPageToken for subsequent pages
if (pageIndex > 0 && previousPageData?.nextPageToken) {
queryParams.set("nextPageToken", previousPageData.nextPageToken);
}
return `/api/google/threads?${queryParams.toString()}`;
};

const { data, size, setSize, isLoading, error, mutate } =
useSWRInfinite<ThreadsResponse>(getKey, {
keepPreviousData: true,
dedupingInterval: 1_000,
revalidateOnFocus: false,
});

const allThreads = data ? data.flatMap((page) => page.threads) : [];
const isLoadingMore =
isLoading || (size > 0 && data && typeof data[size - 1] === "undefined");
const showLoadMore = data ? !!data[data.length - 1]?.nextPageToken : false;

// Find the currently selected thread
const selectedThread = useMemo(() => {
if (!isThreadView || !allThreads || allThreads.length === 0) return null;
return allThreads.find((thread) => thread.id === threadId);
}, [allThreads, threadId, isThreadView]);

// store `refetch` in the atom so we can refresh the list upon archive via command k
const refetch = useCallback(
(options?: { removedThreadIds?: string[] }) => {
mutate(
(currentData) => {
if (!currentData) return currentData;
if (!options?.removedThreadIds) return currentData;

return currentData.map((page) => ({
...page,
threads: page.threads.filter(
(t) => !options?.removedThreadIds?.includes(t.id),
),
}));
},
{
rollbackOnError: true,
populateCache: true,
revalidate: false,
},
);
},
[mutate],
);

// Set up the refetch function in the atom store
const setRefetchEmailList = useSetAtom(refetchEmailListAtom);
useEffect(() => {
setRefetchEmailList({ refetch });
}, [refetch, setRefetchEmailList]);

const handleLoadMore = useCallback(() => {
setSize((size) => size + 1);
}, [setSize]);

const closePanel = useCallback(() => {
router.push(`/mail/${mailType}`);
}, [router, mailType]);

// Functions for the EmailPanel component when in thread view
const onPlanAiAction = useCallback(() => {
// Implementation will be added
}, []);

const onArchive = useCallback(() => {
// Implementation will be added
}, []);

const advanceToAdjacentThread = useCallback(() => {
// Implementation will be added
}, []);

// Render the full EmailPanel if we're in thread view
if (isThreadView && selectedThread) {
return (
<div className="h-full overflow-hidden">
<PermissionsCheck />
<ClientOnly>
<BetaBanner />
</ClientOnly>
<LoadingContent loading={isLoading && !data} error={error}>
{selectedThread && (
<EmailPanel
row={selectedThread}
onPlanAiAction={onPlanAiAction}
onArchive={onArchive}
advanceToAdjacentThread={advanceToAdjacentThread}
close={closePanel}
executingPlan={false}
rejectingPlan={false}
executePlan={async () => {}}
rejectPlan={async () => {}}
refetch={refetch}
/>
)}
</LoadingContent>
</div>
);
}

// Otherwise render the email list
return (
<>
<PermissionsCheck />
<ClientOnly>
<BetaBanner />
</ClientOnly>
<LoadingContent loading={isLoading && !data} error={error}>
{allThreads && (
<List
emails={allThreads}
refetch={refetch}
type={mailType}
showLoadMore={showLoadMore}
handleLoadMore={handleLoadMore}
isLoadingMore={isLoadingMore}
/>
)}
</LoadingContent>
</>
);
}
117 changes: 18 additions & 99 deletions apps/web/app/(app)/mail/page.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,28 @@
"use client";

import { useCallback, useEffect, use } from "react";
import useSWRInfinite from "swr/infinite";
import { useSetAtom } from "jotai";
import { List } from "@/components/email-list/EmailList";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { LoadingContent } from "@/components/LoadingContent";
import type { ThreadsQuery } from "@/app/api/google/threads/validation";
import type { ThreadsResponse } from "@/app/api/google/threads/controller";
import { refetchEmailListAtom } from "@/store/email";
import { BetaBanner } from "@/app/(app)/mail/BetaBanner";
import { ClientOnly } from "@/components/ClientOnly";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";

export default function Mail(props: {
searchParams: Promise<{ type?: string; labelId?: string }>;
}) {
const searchParams = use(props.searchParams);
const query: ThreadsQuery = {};
export default function MailRedirect() {
const router = useRouter();
const searchParams = useSearchParams();
const type = searchParams.get("type") || "inbox";
const threadId = searchParams.get("thread-id");

// Handle different query params
if (searchParams.type === "label" && searchParams.labelId) {
query.labelId = searchParams.labelId;
} else if (searchParams.type) {
query.type = searchParams.type;
}

const getKey = (
pageIndex: number,
previousPageData: ThreadsResponse | null,
) => {
if (previousPageData && !previousPageData.nextPageToken) return null;
const queryParams = new URLSearchParams(query as Record<string, string>);
// Append nextPageToken for subsequent pages
if (pageIndex > 0 && previousPageData?.nextPageToken) {
queryParams.set("nextPageToken", previousPageData.nextPageToken);
}
return `/api/google/threads?${queryParams.toString()}`;
};

const { data, size, setSize, isLoading, error, mutate } =
useSWRInfinite<ThreadsResponse>(getKey, {
keepPreviousData: true,
dedupingInterval: 1_000,
revalidateOnFocus: false,
});

const allThreads = data ? data.flatMap((page) => page.threads) : [];
const isLoadingMore =
isLoading || (size > 0 && data && typeof data[size - 1] === "undefined");
const showLoadMore = data ? !!data[data.length - 1]?.nextPageToken : false;

// store `refetch` in the atom so we can refresh the list upon archive via command k
// TODO is this the best way to do this?
const refetch = useCallback(
(options?: { removedThreadIds?: string[] }) => {
mutate(
(currentData) => {
if (!currentData) return currentData;
if (!options?.removedThreadIds) return currentData;

return currentData.map((page) => ({
...page,
threads: page.threads.filter(
(t) => !options?.removedThreadIds?.includes(t.id),
),
}));
},
{
rollbackOnError: true,
populateCache: true,
revalidate: false,
},
);
},
[mutate],
);

// Set up the refetch function in the atom store
const setRefetchEmailList = useSetAtom(refetchEmailListAtom);
useEffect(() => {
setRefetchEmailList({ refetch });
}, [refetch, setRefetchEmailList]);

const handleLoadMore = useCallback(() => {
setSize((size) => size + 1);
}, [setSize]);
if (threadId) {
// Redirect to the new URL structure for thread view
router.replace(`/mail/${type}/${threadId}`);
} else {
// Redirect to the new URL structure for mail list
router.replace(`/mail/${type}`);
}
}, [router, type, threadId]);

return (
<>
<PermissionsCheck />
<ClientOnly>
<BetaBanner />
</ClientOnly>
<LoadingContent loading={isLoading && !data} error={error}>
{allThreads && (
<List
emails={allThreads}
refetch={refetch}
type={searchParams.type}
showLoadMore={showLoadMore}
handleLoadMore={handleLoadMore}
isLoadingMore={isLoadingMore}
/>
)}
</LoadingContent>
</>
<div className="flex h-full items-center justify-center">
<LoadingContent loading={true}>{null}</LoadingContent>
</div>
);
}
12 changes: 7 additions & 5 deletions apps/web/components/email-list/EmailList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
deleteEmails,
markReadThreads,
} from "@/store/archive-queue";
import { useRouter, useSearchParams } from "next/navigation";

export function List({
emails,
Expand Down Expand Up @@ -178,6 +179,10 @@ export function EmailList({
const session = useSession();
// if right panel is open
const [openThreadId, setOpenThreadId] = useQueryState("thread-id");
const searchParams = useSearchParams();
const router = useRouter();
const type = searchParams.get("type") || "inbox";

const closePanel = useCallback(
() => setOpenThreadId(null),
[setOpenThreadId],
Expand Down Expand Up @@ -445,11 +450,8 @@ export function EmailList({
>
{threads.map((thread) => {
const onOpen = () => {
const alreadyOpen = !!openThreadId;
setOpenThreadId(thread.id);

if (!alreadyOpen) scrollToId(thread.id);

// Navigate to the new URL structure
router.push(`/mail/${type}/${thread.id}`);
markReadThreads([thread.id], () => refetch());
};

Expand Down
Loading