From c00b8e95cb9dc087816cd6a38085377039a5ed51 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Sun, 18 May 2025 11:15:05 +0530 Subject: [PATCH 1/4] feat: add important toogle api and UI --- .../components/context/thread-context.tsx | 21 +++++++ apps/mail/components/mail/mail-list.tsx | 34 ++++++++++- apps/mail/components/mail/thread-display.tsx | 25 ++++++++ apps/mail/locales/en.json | 2 + apps/server/src/trpc/routes/mail.ts | 61 +++++++++++++++++++ 5 files changed, 142 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/context/thread-context.tsx b/apps/mail/components/context/thread-context.tsx index 592d0b2cc..1094380c9 100644 --- a/apps/mail/components/context/thread-context.tsx +++ b/apps/mail/components/context/thread-context.tsx @@ -32,6 +32,7 @@ import { useThread, useThreads } from '@/hooks/use-threads'; import { useSearchValue } from '@/hooks/use-search-value'; import { useParams, useRouter } from 'next/navigation'; import { useTRPC } from '@/providers/query-provider'; +import { ExclamationCircle } from '../icons/icons'; import { useLabels } from '@/hooks/use-labels'; import { LABELS, FOLDERS } from '@/lib/utils'; import { useStats } from '@/hooks/use-stats'; @@ -142,6 +143,7 @@ export function ThreadContextMenu({ trpc.mail.markAsUnread.mutationOptions({ onSuccess: () => invalidateCount() }), ); const { mutateAsync: toggleStar } = useMutation(trpc.mail.toggleStar.mutationOptions()); + const { mutateAsync: toggleImportant } = useMutation(trpc.mail.toggleImportant.mutationOptions()); const { mutateAsync: deleteThread } = useMutation(trpc.mail.delete.mutationOptions()); const selectedThreads = useMemo(() => { @@ -162,6 +164,12 @@ export function ThreadContextMenu({ ); }, [threadData]); + const isImportant = useMemo(() => { + return threadData?.messages.some((message) => + message.tags?.some((tag) => tag.name.toLowerCase() === 'important'), + ); + }, [threadData]); + const noopAction = () => async () => { toast.info(t('common.actions.featureNotImplemented')); }; @@ -212,6 +220,13 @@ export function ThreadContextMenu({ return await Promise.allSettled([refetchThread(), refetch()]); }; + const handleToggleImportant = async () => { + const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; + await toggleImportant({ ids: targets }); + setMail((prev) => ({ ...prev, bulkSelected: [] })); + return await Promise.allSettled([refetchThread(), refetch()]); + }; + const handleReadUnread = () => { const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId]; const action = isUnread ? markAsRead : markAsUnread; @@ -399,6 +414,12 @@ export function ThreadContextMenu({ action: handleReadUnread, disabled: false, }, + { + id: 'toggle-important', + label: isImportant ? t('common.mail.removeFromImportant') : t('common.mail.markAsImportant'), + icon: , + action: handleToggleImportant, + }, { id: 'favorite', label: isStarred ? t('common.mail.removeFavorite') : t('common.mail.addFavorite'), diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 103e49c52..c12a684ed 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -4,6 +4,7 @@ import { Archive2, Bell, ChevronDown, + ExclamationCircle, GroupPeople, Lightning, People, @@ -183,16 +184,22 @@ const Thread = memo( refetch: refetchThread, } = useThread(demo ? null : message.id); const [isStarred, setIsStarred] = useState(false); + const [isImportant, setIsImportant] = useState(false); const trpc = useTRPC(); const queryClient = useQueryClient(); const { mutateAsync: toggleStar } = useMutation(trpc.mail.toggleStar.mutationOptions()); + const { mutateAsync: toggleImportant } = useMutation( + trpc.mail.toggleImportant.mutationOptions(), + ); const [id, setThreadId] = useQueryState('threadId'); const [activeReplyId, setActiveReplyId] = useQueryState('activeReplyId'); const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom); useEffect(() => { if (getThreadData?.latest?.tags) { + console.log(getThreadData.latest.tags); setIsStarred(getThreadData.latest.tags.some((tag) => tag.name === 'STARRED')); + setIsImportant(getThreadData.latest.tags.some((tag) => tag.name === 'IMPORTANT')); } }, [getThreadData?.latest?.tags]); @@ -214,6 +221,16 @@ const Thread = memo( [getThreadData, message.id, isStarred, refetchThreads, t], ); + const handleToggleImportant = useCallback( + async (e: React.MouseEvent) => { + e.stopPropagation(); + if (!getThreadData || !message.id) return; + await toggleImportant({ ids: [message.id] }); + await refetchThread(); + }, + [getThreadData, message.id, refetchThreads], + ); + const handleNext = useCallback( (id: string) => { if (!id || !threads.length || focusedIndex === null) return setThreadId(null); @@ -475,7 +492,22 @@ const Thread = memo( variant="ghost" size="icon" className="h-6 w-6 [&_svg]:size-3.5" - onClick={(e: React.MouseEvent) => { + onClick={handleToggleImportant} + > + + + + + {t('common.mail.toggleImportant')} + + + + + + + + {t('common.mail.toggleImportant')} + + + diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index 8ee552228..7534da60b 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -284,6 +284,8 @@ "noSearchResults": "No search results found", "clearSearch": "Clear search", "markAsImportant": "Mark as Important", + "removeFromImportant": "Remove from Important", + "toggleImportant": "Toggle important", "unSubscribeFromAll": "Unsubscribe from all", "starAll": "Star all", "mute": "Mute", diff --git a/apps/server/src/trpc/routes/mail.ts b/apps/server/src/trpc/routes/mail.ts index 524539a3e..cbe00a58c 100644 --- a/apps/server/src/trpc/routes/mail.ts +++ b/apps/server/src/trpc/routes/mail.ts @@ -157,6 +157,47 @@ export const mailRouter = router({ removeLabels: shouldStar ? [] : ['STARRED'], }); + return { success: true }; + }), + toggleImportant: activeDriverProcedure + .input( + z.object({ + ids: z.string().array(), + }), + ) + .mutation(async ({ input, ctx }) => { + const { driver } = ctx; + const { threadIds } = driver.normalizeIds(input.ids); + + if (!threadIds.length) { + return { success: false, error: 'No thread IDs provided' }; + } + + const threadResults = await Promise.allSettled(threadIds.map((id) => driver.get(id))); + + let anyImportant = false; + let processedThreads = 0; + + for (const result of threadResults) { + if (result.status === 'fulfilled' && result.value && result.value.messages.length > 0) { + processedThreads++; + const isThreadImportant = result.value.messages.some((message) => + message.tags?.some((tag) => tag.name.toLowerCase().startsWith('important')), + ); + if (isThreadImportant) { + anyImportant = true; + break; + } + } + } + + const shouldMarkImportant = processedThreads > 0 && !anyImportant; + + await driver.modifyLabels(threadIds, { + addLabels: shouldMarkImportant ? ['IMPORTANT'] : [], + removeLabels: shouldMarkImportant ? [] : ['IMPORTANT'], + }); + return { success: true }; }), bulkStar: activeDriverProcedure @@ -169,6 +210,16 @@ export const mailRouter = router({ const { driver } = ctx; return driver.modifyLabels(input.ids, { addLabels: ['STARRED'], removeLabels: [] }); }), + bulkMarkImportant: activeDriverProcedure + .input( + z.object({ + ids: z.string().array(), + }), + ) + .mutation(async ({ input, ctx }) => { + const { driver } = ctx; + return driver.modifyLabels(input.ids, { addLabels: ['IMPORTANT'], removeLabels: [] }); + }), bulkUnstar: activeDriverProcedure .input( z.object({ @@ -179,6 +230,16 @@ export const mailRouter = router({ const { driver } = ctx; return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['STARRED'] }); }), + bulkUnmarkImportant: activeDriverProcedure + .input( + z.object({ + ids: z.string().array(), + }), + ) + .mutation(async ({ input, ctx }) => { + const { driver } = ctx; + return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['IMPORTANT'] }); + }), send: activeDriverProcedure .input( z.object({ From 0c3d7e995893cab9356d5de847da9b38f59e1774 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Sat, 17 May 2025 23:18:26 -0700 Subject: [PATCH 2/4] ui fixes --- apps/mail/components/icons/icons.tsx | 17 +++++++++++++ apps/mail/components/mail/mail-list.tsx | 11 +++++++-- apps/mail/components/mail/thread-display.tsx | 26 +++++++------------- apps/mail/locales/en.json | 2 ++ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index 74a5b13ec..b7cda0786 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -1490,3 +1490,20 @@ export const Search = ({ className }: { className?: string }) => ( /> ); + +export const Folders = ({ className }: { className?: string }) => ( + + + + + +); diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index c12a684ed..4f9115f56 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -225,7 +225,14 @@ const Thread = memo( async (e: React.MouseEvent) => { e.stopPropagation(); if (!getThreadData || !message.id) return; + const newImportantState = !isImportant; + setIsImportant(newImportantState); await toggleImportant({ ids: [message.id] }); + if (newImportantState) { + toast.success(t('common.actions.addedToImportant')); + } else { + toast.success(t('common.actions.removedFromImportant')); + } await refetchThread(); }, [getThreadData, message.id, refetchThreads], @@ -486,7 +493,7 @@ const Thread = memo( {isStarred ? t('common.threadDisplay.unstar') : t('common.threadDisplay.star')} - + {/* - - - {t('common.mail.toggleImportant')} - - - + @@ -603,12 +591,16 @@ export function ThreadDisplay() { {emailData.latest?.listUnsubscribe || emailData.latest?.listUnsubscribePost ? ( - + Unsubscribe ) : null} )} + + + {t('common.mail.toggleImportant')} + diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index 7534da60b..368f1730b 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -29,7 +29,9 @@ "addingToFavorites": "Adding to favorites...", "removingFromFavorites": "Removing from favorites...", "addedToFavorites": "Added to favorites", + "addedToImportant": "Added to important", "removedFromFavorites": "Removed from favorites", + "removedFromImportant": "Removed from important", "failedToAddToFavorites": "Failed to add to favorites", "failedToRemoveFromFavorites": "Failed to remove from favorites", "failedToModifyFavorites": "Failed to modify favorites", From 6100f4f37853f52c1bbcae4e8e64afbf49ad0d38 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Sat, 17 May 2025 23:21:07 -0700 Subject: [PATCH 3/4] fix --- apps/mail/components/mail/thread-display.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index dafb910c6..c2fcf032b 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -597,10 +597,12 @@ export function ThreadDisplay() { ) : null} )} - - - {t('common.mail.toggleImportant')} - + {!isImportant && ( + + + {t('common.mail.toggleImportant')} + + )} From e3dd6d5bed5379024a73ca767d5d8879cf330830 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Sat, 17 May 2025 23:28:23 -0700 Subject: [PATCH 4/4] toggle --- apps/mail/components/mail/thread-display.tsx | 7 ++++++- apps/mail/locales/en.json | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index c2fcf032b..6c7698242 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -310,6 +310,11 @@ export function ThreadDisplay() { if (!emailData || !id) return; await toggleImportant({ ids: [id] }); await refetchThread(); + if (isImportant) { + toast.success(t('common.mail.markedAsImportant')); + } else { + toast.error("Failed to mark as important"); + } }, [emailData, id]); // Set initial star state based on email data @@ -600,7 +605,7 @@ export function ThreadDisplay() { {!isImportant && ( - {t('common.mail.toggleImportant')} + {t('common.mail.markAsImportant')} )} diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index 368f1730b..83614786b 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -287,6 +287,8 @@ "clearSearch": "Clear search", "markAsImportant": "Mark as Important", "removeFromImportant": "Remove from Important", + "markedAsImportant": "Marked as Important", + "markedAsUnimportant": "Unmarked as Unimportant", "toggleImportant": "Toggle important", "unSubscribeFromAll": "Unsubscribe from all", "starAll": "Star all",