diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index fcb96b65c..93ccc2d40 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -72,7 +72,11 @@ const Thread = memo( const [threadId] = useQueryState('threadId'); const [, setBackgroundQueue] = useAtom(backgroundQueueAtom); const { refetch: refetchStats } = useStats(); - const { data: getThreadData, isGroupThread, refetch: refetchThread } = useThread(message.id); + const { + data: getThreadData, + isGroupThread, + refetch: refetchThread, + } = useThread(message.id, message.historyId); const [isStarred, setIsStarred] = useState(false); const trpc = useTRPC(); const queryClient = useQueryClient(); diff --git a/apps/mail/hooks/use-previous.ts b/apps/mail/hooks/use-previous.ts new file mode 100644 index 000000000..99f4bd9ef --- /dev/null +++ b/apps/mail/hooks/use-previous.ts @@ -0,0 +1,13 @@ +import { useState } from 'react'; + +export function usePrevious(value: T) { + const [current, setCurrent] = useState(value); + const [previous, setPrevious] = useState(null); + + if (value !== current) { + setPrevious(current); + setCurrent(value); + } + + return previous; +} diff --git a/apps/mail/hooks/use-threads.ts b/apps/mail/hooks/use-threads.ts index 736ba3a05..3c1b73fa4 100644 --- a/apps/mail/hooks/use-threads.ts +++ b/apps/mail/hooks/use-threads.ts @@ -1,12 +1,13 @@ import { backgroundQueueAtom, isThreadInBackgroundQueueAtom } from '@/store/backgroundQueue'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; import { useSearchValue } from '@/hooks/use-search-value'; import { useTRPC } from '@/providers/query-provider'; import { useSession } from '@/lib/auth-client'; import { useAtom, useAtomValue } from 'jotai'; +import { usePrevious } from './use-previous'; +import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router'; import { useQueryState } from 'nuqs'; -import { useMemo } from 'react'; export const useThreads = () => { const { folder } = useParams<{ folder: string }>(); @@ -58,12 +59,20 @@ export const useThreads = () => { return [threadsQuery, threads, isReachingEnd, loadMore] as const; }; -export const useThread = (threadId: string | null) => { +export const useThread = (threadId: string | null, historyId?: string | null) => { const { data: session } = useSession(); const [_threadId] = useQueryState('threadId'); const id = threadId ? threadId : _threadId; const trpc = useTRPC(); + const previousHistoryId = usePrevious(historyId ?? null); + const queryClient = useQueryClient(); + + useEffect(() => { + if (!historyId || !previousHistoryId || historyId === previousHistoryId) return; + queryClient.invalidateQueries({ queryKey: trpc.mail.get.queryKey({ id: id! }) }); + }, [historyId, previousHistoryId, id]); + const threadQuery = useQuery( trpc.mail.get.queryOptions( { diff --git a/apps/mail/types/index.ts b/apps/mail/types/index.ts index fc1b05a31..4c35beecd 100644 --- a/apps/mail/types/index.ts +++ b/apps/mail/types/index.ts @@ -106,7 +106,7 @@ export interface MailListProps { export type MailSelectMode = 'mass' | 'range' | 'single' | 'selectAllBelow'; export type ThreadProps = { - message: { id: string }; + message: { id: string; historyId?: string | null }; onClick?: (message: ParsedMessage) => () => void; isKeyboardFocused?: boolean; }; diff --git a/apps/server/src/lib/driver/google.ts b/apps/server/src/lib/driver/google.ts index aaeca5862..943c1c0e7 100644 --- a/apps/server/src/lib/driver/google.ts +++ b/apps/server/src/lib/driver/google.ts @@ -190,11 +190,18 @@ export class GoogleMailManager implements MailManager { pageToken: pageToken ? pageToken : undefined, quotaUser: this.config.auth?.email, }); + + const threads = res.data.threads ?? []; + return { - threads: (res.data.threads ?? []) + threads: threads .filter((thread) => typeof thread.id === 'string') // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - .map((thread) => ({ id: thread.id!, $raw: thread })), + .map((thread) => ({ + id: thread.id!, + historyId: thread.historyId ?? null, + $raw: thread, + })), nextPageToken: res.data.nextPageToken ?? null, }; }, @@ -493,6 +500,7 @@ export class GoogleMailManager implements MailManager { return { threads: sortedDrafts.map((draft) => ({ id: draft.id, + historyId: draft.threadId ?? null, $raw: draft, })), nextPageToken: res.data.nextPageToken ?? null, diff --git a/apps/server/src/lib/driver/microsoft.ts b/apps/server/src/lib/driver/microsoft.ts index 250812614..1fc595150 100644 --- a/apps/server/src/lib/driver/microsoft.ts +++ b/apps/server/src/lib/driver/microsoft.ts @@ -305,6 +305,7 @@ export class OutlookMailManager implements MailManager { return { threads: messages.map((msg, index) => ({ id: msg.id || msg.internetMessageId || '', + historyId: msg.lastModifiedDateTime ?? null, $raw: { ...msg, ...fullMessages[index], @@ -619,6 +620,7 @@ export class OutlookMailManager implements MailManager { return { threads: sortedDrafts.map((draft) => ({ id: draft.id, + historyId: null, $raw: draft, })), nextPageToken: nextPageLink || null, diff --git a/apps/server/src/lib/driver/types.ts b/apps/server/src/lib/driver/types.ts index 19f4c12be..4d678ff48 100644 --- a/apps/server/src/lib/driver/types.ts +++ b/apps/server/src/lib/driver/types.ts @@ -45,7 +45,7 @@ export interface MailManager { ): Promise<{ id?: string | null; success?: boolean; error?: string }>; getDraft(id: string): Promise; listDrafts(params: { q?: string; maxResults?: number; pageToken?: string }): Promise<{ - threads: { id: string; $raw: unknown }[]; + threads: { id: string; historyId: string | null; $raw: unknown }[]; nextPageToken: string | null; }>; delete(id: string): Promise; @@ -55,7 +55,10 @@ export interface MailManager { maxResults?: number; labelIds?: string[]; pageToken?: string | number; - }): Promise<{ threads: { id: string; $raw?: unknown }[]; nextPageToken: string | null }>; + }): Promise<{ + threads: { id: string; historyId: string | null; $raw?: unknown }[]; + nextPageToken: string | null; + }>; count(): Promise<{ count?: number; label?: string }[]>; getTokens( code: string,