diff --git a/apps/mail/app/(routes)/mail/[folder]/page.tsx b/apps/mail/app/(routes)/mail/[folder]/page.tsx index 19c8d640b3..a7168d1158 100644 --- a/apps/mail/app/(routes)/mail/[folder]/page.tsx +++ b/apps/mail/app/(routes)/mail/[folder]/page.tsx @@ -12,7 +12,7 @@ interface MailPageProps { }>; } -const ALLOWED_FOLDERS = ['inbox', 'draft', 'sent', 'spam', 'trash', 'archive']; +const ALLOWED_FOLDERS = ['inbox', 'draft', 'sent', 'spam', 'bin', 'archive']; export default async function MailPage({ params, searchParams }: MailPageProps) { const headersList = await headers(); diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts index 76c752252f..f725cd660f 100644 --- a/apps/mail/app/api/driver/google.ts +++ b/apps/mail/app/api/driver/google.ts @@ -234,7 +234,7 @@ export const driver = async (config: IConfig): Promise => { } const normalizeSearch = (folder: string, q: string) => { // Handle special folders - if (folder === 'trash') { + if (folder === 'bin') { return { folder: undefined, q: `in:trash ${q}` }; } if (folder === 'archive') { @@ -630,8 +630,8 @@ export const driver = async (config: IConfig): Promise => { // Sort drafts by date, newest first const sortedDrafts = [...drafts].sort((a, b) => { - const dateA = new Date(a.receivedOn || new Date()).getTime(); - const dateB = new Date(b.receivedOn || new Date()).getTime(); + const dateA = new Date(a?.receivedOn || new Date()).getTime(); + const dateB = new Date(b?.receivedOn || new Date()).getTime(); return dateB - dateA; }); diff --git a/apps/mail/components/context/thread-context.tsx b/apps/mail/components/context/thread-context.tsx index a38581cdfa..768456ef3e 100644 --- a/apps/mail/components/context/thread-context.tsx +++ b/apps/mail/components/context/thread-context.tsx @@ -56,6 +56,7 @@ interface EmailContextMenuProps { isInbox?: boolean; isSpam?: boolean; isSent?: boolean; + isBin?: boolean; refreshCallback?: () => void; } @@ -66,6 +67,7 @@ export function ThreadContextMenu({ isInbox = true, isSpam = false, isSent = false, + isBin = false, refreshCallback, }: EmailContextMenuProps) { const { folder } = useParams<{ folder: string }>(); @@ -113,6 +115,7 @@ export function ThreadContextMenu({ let destination: ThreadDestination = null; if (to === LABELS.INBOX) destination = FOLDERS.INBOX; else if (to === LABELS.SPAM) destination = FOLDERS.SPAM; + else if (to === LABELS.TRASH) destination = FOLDERS.BIN; else if (from && !to) destination = FOLDERS.ARCHIVE; const promise = moveThreadsTo({ @@ -136,6 +139,9 @@ export function ThreadContextMenu({ } else if (destination === FOLDERS.ARCHIVE) { loadingMessage = t('common.actions.archiving'); successMessage = t('common.actions.archived'); + } else if (destination === FOLDERS.BIN) { + loadingMessage = t('common.actions.movingToBin'); + successMessage = t('common.actions.movedToBin'); } toast.promise(promise, { @@ -217,6 +223,25 @@ export function ThreadContextMenu({ action: handleMove(LABELS.SPAM, LABELS.INBOX), disabled: false, }, + { + id: 'move-to-bin', + label: t('common.mail.moveToBin'), + icon: , + action: handleMove(LABELS.SPAM, LABELS.TRASH), + disabled: false, + }, + ]; + } + + if (isBin) { + return [ + { + id: 'restore-from-bin', + label: t('common.mail.restoreFromBin' as any), + icon: , + action: handleMove(LABELS.TRASH, LABELS.INBOX), + disabled: false, + }, ]; } @@ -229,6 +254,13 @@ export function ThreadContextMenu({ action: handleMove('', LABELS.INBOX), disabled: false, }, + { + id: 'move-to-bin', + label: t('common.mail.moveToBin'), + icon: , + action: handleMove('', LABELS.TRASH), + disabled: false, + }, ]; } @@ -242,6 +274,13 @@ export function ThreadContextMenu({ action: handleMove(LABELS.SENT, ''), disabled: false, }, + { + id: 'move-to-bin', + label: t('common.mail.moveToBin'), + icon: , + action: handleMove(LABELS.SENT, LABELS.TRASH), + disabled: false, + }, ]; } @@ -261,19 +300,16 @@ export function ThreadContextMenu({ action: handleMove(LABELS.INBOX, LABELS.SPAM), disabled: !isInbox, }, + { + id: 'move-to-bin', + label: t('common.mail.moveToBin'), + icon: , + action: handleMove(LABELS.INBOX, LABELS.TRASH), + disabled: false, + }, ]; }; - const moveActions: EmailAction[] = [ - { - id: 'move-to-trash', - label: t('common.mail.moveToTrash'), - icon: , - action: noopAction, - disabled: true, // TODO: Move to trash functionality to be implemented - }, - ]; - const otherActions: EmailAction[] = [ { id: 'toggle-read', @@ -324,7 +360,6 @@ export function ThreadContextMenu({ {getActions().map(renderAction as any)} - {moveActions.filter((action) => action.id !== 'move-to-spam').map(renderAction)} diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index 013a4bc36d..77af379839 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -47,6 +47,7 @@ const ThreadWrapper = ({ isFolderInbox, isFolderSpam, isFolderSent, + isFolderBin, refreshCallback, }: { children: React.ReactNode; @@ -55,6 +56,7 @@ const ThreadWrapper = ({ isFolderInbox: boolean; isFolderSpam: boolean; isFolderSent: boolean; + isFolderBin: boolean; refreshCallback: () => void; }) => { return ( @@ -64,6 +66,7 @@ const ThreadWrapper = ({ isInbox={isFolderInbox} isSpam={isFolderSpam} isSent={isFolderSent} + isBin={isFolderBin} refreshCallback={refreshCallback} > {children} @@ -104,6 +107,7 @@ const Thread = memo( const isFolderInbox = folder === FOLDERS.INBOX || !folder; const isFolderSpam = folder === FOLDERS.SPAM; const isFolderSent = folder === FOLDERS.SENT; + const isFolderBin = folder === FOLDERS.BIN; const handleMouseEnter = () => { if (demo) return; @@ -332,6 +336,7 @@ const Thread = memo( isFolderInbox={isFolderInbox} isFolderSpam={isFolderSpam} isFolderSent={isFolderSent} + isFolderBin={isFolderBin} refreshCallback={() => mutate()} > {content} diff --git a/apps/mail/components/mail/mail-quick-actions.tsx b/apps/mail/components/mail/mail-quick-actions.tsx index 162946cfce..a02895ac17 100644 --- a/apps/mail/components/mail/mail-quick-actions.tsx +++ b/apps/mail/components/mail/mail-quick-actions.tsx @@ -142,7 +142,7 @@ export const MailQuickActions = memo( const handleDelete = useCallback( async (e?: React.MouseEvent) => { // TODO: Implement delete - toast.info(t('common.mail.moveToTrash')); + toast.info(t('common.mail.moveToBin')); }, [t], ); diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 50a70012d1..78d085bdfa 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -16,6 +16,7 @@ import { RotateCw, Mail, MailOpen, + Trash, } from 'lucide-react'; import { Dialog, @@ -516,6 +517,10 @@ function BulkSelectActions() { icon: , tooltip: t('common.mail.moveToInbox'), }, + bin: { + icon: , + tooltip: t('common.mail.moveToBin'), + }, }; return ( diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index f81de0ff02..b07997d822 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -6,6 +6,7 @@ import { MailOpen, Reply, X, + Trash, } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { useSearchParams, useParams } from 'next/navigation'; @@ -166,7 +167,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp const isInArchive = folder === FOLDERS.ARCHIVE; const isInSpam = folder === FOLDERS.SPAM; - + const isInBin = folder === FOLDERS.BIN; const handleClose = useCallback(() => { // Reset reply composer state when closing thread display setMail((prev) => ({ @@ -196,7 +197,9 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp ? t('common.actions.movedToInbox') : destination === 'spam' ? t('common.actions.movedToSpam') - : t('common.actions.archived'), + : destination === 'bin' + ? t('common.actions.movedToBin') + : t('common.actions.archived'), error: t('common.actions.failedToMove'), }); }, @@ -320,7 +323,7 @@ export function ThreadDisplay({ threadParam, onClose, isMobile, id }: ThreadDisp disabled={!emailData} onClick={() => setIsFullscreen(!isFullscreen)} /> - {isInSpam || isInArchive ? ( + {isInSpam || isInArchive || isInBin ? ( moveThreadTo('spam')} /> + moveThreadTo('bin')} + /> )} = { title: 'navigation.sidebar.bin', url: '/mail/bin', icon: DeleteIcon, - disabled: true, + disabled: false, }, { id: 'settings', diff --git a/apps/mail/lib/thread-actions.ts b/apps/mail/lib/thread-actions.ts index 1f2df54c6f..589f9a44e8 100644 --- a/apps/mail/lib/thread-actions.ts +++ b/apps/mail/lib/thread-actions.ts @@ -1,8 +1,8 @@ import { modifyLabels } from '@/actions/mail'; import { LABELS, FOLDERS } from '@/lib/utils'; -export type ThreadDestination = 'inbox' | 'archive' | 'spam' | null; -export type FolderLocation = 'inbox' | 'archive' | 'spam' | 'sent' | string; +export type ThreadDestination = 'inbox' | 'archive' | 'spam' | 'bin' | null; +export type FolderLocation = 'inbox' | 'archive' | 'spam' | 'sent' | 'bin' | string; interface MoveThreadOptions { threadIds: string[]; @@ -21,14 +21,20 @@ export function isActionAvailable(folder: FolderLocation, action: ThreadDestinat return true; case `${FOLDERS.INBOX}_to_archive`: return true; + case `${FOLDERS.INBOX}_to_trash`: + return true; // From archive rules case `${FOLDERS.ARCHIVE}_to_inbox`: return true; + case `${FOLDERS.ARCHIVE}_to_trash`: + return true; // From spam rules case `${FOLDERS.SPAM}_to_inbox`: return true; + case `${FOLDERS.SPAM}_to_trash`: + return true; default: return false; @@ -36,7 +42,7 @@ export function isActionAvailable(folder: FolderLocation, action: ThreadDestinat } export function getAvailableActions(folder: FolderLocation): ThreadDestination[] { - const allPossibleActions: ThreadDestination[] = ['inbox', 'archive', 'spam']; + const allPossibleActions: ThreadDestination[] = ['inbox', 'archive', 'spam', 'bin']; return allPossibleActions.filter(action => isActionAvailable(folder, action)); } @@ -49,6 +55,7 @@ export async function moveThreadsTo({ if (!threadIds.length) return; const isInInbox = currentFolder === FOLDERS.INBOX || !currentFolder; const isInSpam = currentFolder === FOLDERS.SPAM; + const isInBin = currentFolder === FOLDERS.BIN; let addLabel = ''; let removeLabel = ''; @@ -56,24 +63,34 @@ export async function moveThreadsTo({ switch(destination) { case 'inbox': addLabel = LABELS.INBOX; - removeLabel = isInSpam ? LABELS.SPAM : ''; + removeLabel = isInSpam ? LABELS.SPAM : (isInBin ? LABELS.TRASH : ''); break; case 'archive': - removeLabel = isInInbox ? LABELS.INBOX : (isInSpam ? LABELS.SPAM : ''); + addLabel = ''; + removeLabel = isInInbox ? LABELS.INBOX : (isInSpam ? LABELS.SPAM : (isInBin ? LABELS.TRASH : '')); break; case 'spam': addLabel = LABELS.SPAM; - removeLabel = isInInbox ? LABELS.INBOX : ''; + removeLabel = isInInbox ? LABELS.INBOX : (isInBin ? LABELS.TRASH : ''); break; - default: + case 'bin': + addLabel = LABELS.TRASH; + removeLabel = isInInbox ? LABELS.INBOX : (isInSpam ? LABELS.SPAM : ''); break; + default: + return; + } + + if (!addLabel && !removeLabel) { + console.warn('No labels to modify, skipping API call'); + return; } return modifyLabels({ threadId: threadIds, addLabels: addLabel ? [addLabel] : [], removeLabels: removeLabel ? [removeLabel] : [], - }) + }); } catch (error) { console.error(`Error moving thread(s):`, error); throw error; diff --git a/apps/mail/lib/utils.ts b/apps/mail/lib/utils.ts index 7a833547da..806fd44300 100644 --- a/apps/mail/lib/utils.ts +++ b/apps/mail/lib/utils.ts @@ -11,7 +11,7 @@ export const FOLDERS = { SPAM: 'spam', INBOX: 'inbox', ARCHIVE: 'archive', - TRASH: 'trash', + BIN: 'bin', DRAFT: 'draft', SENT: 'sent', } as const; @@ -22,12 +22,13 @@ export const LABELS = { UNREAD: 'UNREAD', IMPORTANT: 'IMPORTANT', SENT: 'SENT', + TRASH: 'TRASH', } as const; export const FOLDER_NAMES = [ 'inbox', 'spam', - 'trash', + 'bin', 'unread', 'starred', 'important', @@ -40,6 +41,7 @@ export const FOLDER_TAGS: Record = { [FOLDERS.INBOX]: [LABELS.INBOX], [FOLDERS.ARCHIVE]: [], [FOLDERS.SENT]: [LABELS.SENT], + [FOLDERS.BIN]: [LABELS.TRASH], }; export const getFolderTags = (folder: string): string[] => { diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json index a40ca4066a..72e1504a5a 100644 --- a/apps/mail/locales/en.json +++ b/apps/mail/locales/en.json @@ -29,6 +29,9 @@ "failedToAddToFavorites": "Failed to add to favorites", "failedToRemoveFromFavorites": "Failed to remove from favorites", "failedToModifyFavorites": "Failed to modify favorites", + "movingToBin": "Moving to bin...", + "movedToBin": "Moved to bin", + "failedToMoveToBin": "Failed to move to bin", "markingAsRead": "Marking as read...", "markingAsUnread": "Marking as unread...", "hiddenImagesWarning": "Images are hidden by default for security reasons.", @@ -244,7 +247,8 @@ "moveToInbox": "Move to Inbox", "unarchive": "Unarchive", "archive": "Archive", - "moveToTrash": "Move to Trash", + "moveToBin": "Move to Bin", + "restoreFromBin": "Restore from Bin", "markAsUnread": "Mark as Unread", "markAsRead": "Mark as Read", "addFavorite": "Favorite",