From 85e76fc2e55b856ba00864591c929c32cfeb0b39 Mon Sep 17 00:00:00 2001 From: Nizzy Date: Tue, 22 Apr 2025 10:11:15 -0700 Subject: [PATCH] ai --- apps/mail/components/icons/icons.tsx | 193 ++++++++++++- apps/mail/components/mail/mail-display.tsx | 276 +++++++++++++++---- apps/mail/components/mail/mail-list.tsx | 15 +- apps/mail/components/mail/mail.tsx | 26 +- apps/mail/components/mail/thread-display.tsx | 196 ++++++++----- apps/mail/components/ui/ai-sidebar.tsx | 2 +- apps/mail/components/ui/badge.tsx | 2 +- 7 files changed, 554 insertions(+), 156 deletions(-) diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx index 0ecc0772c..3dc47fa66 100644 --- a/apps/mail/components/icons/icons.tsx +++ b/apps/mail/components/icons/icons.tsx @@ -1,3 +1,5 @@ +import { cn } from '@/lib/utils'; + export const Vercel = ({ className }: { className?: string }) => ( Vercel @@ -454,7 +456,13 @@ export const Bell = ({ className }: { className?: string }) => ( ); export const Tag = ({ className }: { className?: string }) => ( - + ( ); export const Mail = ({ className }: { className?: string }) => ( - + ); + +export const X = ({ className }: { className?: string }) => ( + + + +); + +export const ChevronLeft = ({ className }: { className?: string }) => ( + + + +); + +export const ChevronRight = ({ className }: { className?: string }) => ( + + + +); + +export const Reply = ({ className }: { className?: string }) => ( + + + +); + +export const Forward = ({ className }: { className?: string }) => ( + + + +); + +export const Star = ({ className }: { className?: string }) => ( + + + +); + +export const ThreeDots = ({ className }: { className?: string }) => ( + + + + + +); + +export const Trash = ({ className }: { className?: string }) => ( + + + +); + +export const Expand = ({ className }: { className?: string }) => ( + + + + + + +); + +export const ArchiveX = ({ className }: { className?: string }) => ( + + + + +); + +export const Calendar = ({ className }: { className?: string }) => ( + + + +); diff --git a/apps/mail/components/mail/mail-display.tsx b/apps/mail/components/mail/mail-display.tsx index b86b9c4a0..a6c62e389 100644 --- a/apps/mail/components/mail/mail-display.tsx +++ b/apps/mail/components/mail/mail-display.tsx @@ -10,20 +10,25 @@ import { } from '../ui/dialog'; import { BellOff, Check, ChevronDown, LoaderCircleIcon, Lock } from 'lucide-react'; import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; +import { Bell, Calendar, Lightning, Tag, User } from '../icons/icons'; import { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'; +import { memo, useEffect, useMemo, useState, useRef } from 'react'; +import { Briefcase, Star, StickyNote, Users } from 'lucide-react'; import { handleUnsubscribe } from '@/lib/email-utils.client'; import { getListUnsubscribeAction } from '@/lib/email-utils'; import AttachmentsAccordion from './attachments-accordion'; -import { memo, useEffect, useMemo, useState, useRef } from 'react'; +import { cn, getEmailLogo, formatDate } from '@/lib/utils'; import AttachmentDialog from './attachment-dialog'; import { useSummary } from '@/hooks/use-summary'; import { TextShimmer } from '../ui/text-shimmer'; -import { cn, getEmailLogo } from '@/lib/utils'; import { type ParsedMessage } from '@/types'; import { Separator } from '../ui/separator'; +import { useParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { MailIframe } from './mail-iframe'; +import { MailLabels } from './mail-list'; import { Button } from '../ui/button'; +import { Badge } from '../ui/badge'; import { format } from 'date-fns'; import Image from 'next/image'; @@ -89,12 +94,100 @@ type Props = { index: number; totalEmails?: number; demo?: boolean; + subject?: string; +}; + +const MailDisplayLabels = ({ labels }: { labels: string[] }) => { + const visibleLabels = labels.filter( + (label) => !['unread', 'inbox'].includes(label.toLowerCase()), + ); + + if (!visibleLabels.length) return null; + + return ( +
+ {visibleLabels.map((label, index) => { + const normalizedLabel = label.toLowerCase().replace(/^category_/i, ''); + + let icon = null; + let bgColor = ''; + + switch (normalizedLabel) { + case 'important': + icon = ; + bgColor = 'bg-[#F59E0D]'; + break; + case 'promotions': + icon = ; + bgColor = 'bg-[#F43F5E]'; + break; + case 'personal': + icon = ; + bgColor = 'bg-[#39AE4A]'; + break; + case 'updates': + icon = ; + bgColor = 'bg-[#8B5CF6]'; + break; + case 'work': + icon = ; + bgColor = 'bg-neutral-600'; + break; + case 'forums': + icon = ; + bgColor = 'bg-blue-600'; + break; + case 'notes': + icon = ; + bgColor = 'bg-amber-500'; + break; + case 'starred': + icon = ; + bgColor = 'bg-yellow-500'; + break; + default: + return null; + } + + return ( + + {icon} + + ); + })} +
+ ); +}; + +// Helper function to get first letter character +const getFirstLetterCharacter = (name?: string) => { + if (!name) return ''; + const match = name.match(/[a-zA-Z]/); + return match ? match[0].toUpperCase() : ''; +}; + +// Helper function to clean email display +const cleanEmailDisplay = (email?: string) => { + if (!email) return ''; + const match = email.match(/^[^a-zA-Z]*(.*?)[^a-zA-Z]*$/); + return match ? match[1] : email; +}; + +// Helper function to clean name display +const cleanNameDisplay = (name?: string) => { + if (!name) return ''; + const match = name.match(/^[^a-zA-Z]*(.*?)[^a-zA-Z]*$/); + return match ? match[1] : name; }; const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => { const [isCollapsed, setIsCollapsed] = useState(false); const [unsubscribed, setUnsubscribed] = useState(false); const [isUnsubscribing, setIsUnsubscribing] = useState(false); + const { folder } = useParams<{ folder: string }>(); const [selectedAttachment, setSelectedAttachment] = useState const { data } = demo ? { - data: { - content: - 'This email talks about how Zero Email is the future of email. It is a new way to send and receive emails that is more secure and private.', - }, - } + data: { + content: + 'This email talks about how Zero Email is the future of email. It is a new way to send and receive emails that is more secure and private.', + }, + } : useSummary(emailData.id); useEffect(() => { @@ -134,9 +227,9 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => () => emailData.listUnsubscribe ? getListUnsubscribeAction({ - listUnsubscribe: emailData.listUnsubscribe, - listUnsubscribePost: emailData.listUnsubscribePost, - }) + listUnsubscribe: emailData.listUnsubscribe, + listUnsubscribePost: emailData.listUnsubscribePost, + }) : undefined, [emailData.listUnsubscribe, emailData.listUnsubscribePost], ); @@ -159,28 +252,120 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
setIsCollapsed(!isCollapsed)} > -
-
- + {index === 0 && ( +
+

+ {emailData.subject}{' '} + + {totalEmails && `[${totalEmails}]`} + +

+
+ + + {formatDate(emailData?.receivedOn)} + +
+
+ +
+
+ {(() => { + interface Person { + name: string; + email: string; + } + + const allPeople = [ + ...(folder === 'sent' ? [] : [emailData.sender]), + ...(emailData.to || []), + ...(emailData.cc || []), + ...(emailData.bcc || []), + ]; + + const people = allPeople.filter( + (p): p is Person => + Boolean(p?.email) && + p.name !== 'No Sender Name' && + p === allPeople.find((other) => other?.email === p?.email), + ); + + const renderPerson = (person: Person) => ( +
+ + + + {getFirstLetterCharacter(person.name)} + + +
+ {person.name || person.email} +
+
+ ); + + if (people.length <= 2) { + return people.map(renderPerson); + } + + // Only show first two people plus count if we have at least two people + const firstPerson = people[0]; + const secondPerson = people[1]; + + if (firstPerson && secondPerson) { + return ( + <> + {renderPerson(firstPerson)} + {renderPerson(secondPerson)} + +{people.length - 2} others + + ); + } + + return null; + })()} +
+
+
+ )} + +
+
+ - - {emailData?.sender?.name[0]?.toUpperCase()} + + {getFirstLetterCharacter(emailData?.sender?.name)} -
-
- {emailData?.sender?.name} +
+
+
+
+ + {cleanNameDisplay(emailData?.sender?.name)} + + +
+ {/*

To: get

*/} +
- {emailData?.sender?.email} + {/* + {emailData?.sender?.email} + */} - {listUnsubscribeAction && ( + {/* {listUnsubscribeAction && ( )} - {isMuted && } + {isMuted && } */}
-
+ {/*
@@ -257,11 +442,11 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
- {emailData?.sender?.name} + {cleanNameDisplay(emailData?.sender?.name)} {emailData?.sender?.name !== emailData?.sender?.email && ( - {emailData?.sender?.email} + {cleanEmailDisplay(emailData?.sender?.email)} )}
@@ -271,7 +456,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => {t('common.mailDisplay.to')}: - {emailData?.to?.map((t) => t.email).join(', ')} + {emailData?.to?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
{emailData?.cc && emailData.cc.length > 0 && ( @@ -280,7 +465,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => {t('common.mailDisplay.cc')}: - {emailData?.cc?.map((t) => t.email).join(', ')} + {emailData?.cc?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
)} @@ -290,7 +475,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => {t('common.mailDisplay.bcc')}: - {emailData?.bcc?.map((t) => t.email).join(', ')} + {emailData?.bcc?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
)} @@ -307,7 +492,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => {t('common.mailDisplay.mailedBy')}: - {emailData?.sender?.email} + {cleanEmailDisplay(emailData?.sender?.email)}
@@ -315,7 +500,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) => {t('common.mailDisplay.signedBy')}: - {emailData?.sender?.email} + {cleanEmailDisplay(emailData?.sender?.email)}
{emailData.tls && ( @@ -332,34 +517,9 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
-
+
*/}
- {data ? ( -
- - - - - - - - -
- ) : null}
diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx index c3af4a0fe..5d2a8776d 100644 --- a/apps/mail/components/mail/mail-list.tsx +++ b/apps/mail/components/mail/mail-list.tsx @@ -252,7 +252,7 @@ const Thread = memo( )} > - {highlightText(latestMessage.sender.name, searchValue.highlight)} + {highlightText(cleanNameDisplay(latestMessage.sender.name) || '', searchValue.highlight)} {' '} {latestMessage.unread && !isMailSelected ? ( @@ -370,7 +370,7 @@ const Thread = memo( )} > - {highlightText(latestMessage.sender.name, searchValue.highlight)} + {highlightText(cleanNameDisplay(latestMessage.sender.name) || '', searchValue.highlight)} {' '}

{getThreadData.totalReplies > 1 ? ( @@ -688,7 +688,9 @@ export const MailList = memo(({ isCompact }: MailListProps) => { ); }); -const MailLabels = memo( +MailList.displayName = 'MailList'; + +export const MailLabels = memo( ({ labels }: { labels: string[] }) => { const t = useTranslations(); @@ -822,3 +824,10 @@ function getDefaultBadgeStyle(label: string): ComponentProps['vari return 'secondary'; } } + +// Helper function to clean name display +const cleanNameDisplay = (name?: string) => { + if (!name) return ''; + const match = name.match(/^[^a-zA-Z]*(.*?)[^a-zA-Z]*$/); + return match ? match[1] : name; +}; diff --git a/apps/mail/components/mail/mail.tsx b/apps/mail/components/mail/mail.tsx index 686bb96de..87868ce65 100644 --- a/apps/mail/components/mail/mail.tsx +++ b/apps/mail/components/mail/mail.tsx @@ -25,12 +25,12 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/componen import { moveThreadsTo, ThreadDestination, getAvailableActions } from '@/lib/thread-actions'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { useState, useCallback, useMemo, useEffect, useRef, memo } from 'react'; -import { Filter, Lightning, Mail } from '../icons/icons'; import { ThreadDisplay, ThreadDemo } from '@/components/mail/thread-display'; import { MailList, MailListDemo } from '@/components/mail/mail-list'; import { handleUnsubscribe } from '@/lib/email-utils.client'; import { useMediaQuery } from '../../hooks/use-media-query'; import { useSearchValue } from '@/hooks/use-search-value'; +import { Filter, Lightning, Mail, X } from '../icons/icons'; import { useHotkeysContext } from 'react-hotkeys-hook'; import { useMail } from '@/components/mail/use-mail'; import { SidebarToggle } from '../ui/sidebar-toggle'; @@ -123,6 +123,7 @@ export function DemoMailLayout() { )} > +
- +
- +
@@ -373,11 +370,11 @@ export function MailLayout() { isValidating ? 'opacity-100' : 'opacity-0', )} /> -
+
- +
@@ -615,7 +612,8 @@ export const Categories = () => { )} /> ), - colors: 'border-0 bg-[#006FFE] text-white dark:bg-[#006FFE] dark:text-white dark:hover:bg-[#006FFE]/90', + colors: + 'border-0 bg-[#006FFE] text-white dark:bg-[#006FFE] dark:text-white dark:hover:bg-[#006FFE]/90', }, { id: 'Personal', @@ -883,7 +881,7 @@ function MailCategoryTabs({ className={cn( 'flex h-7 items-center gap-1.5 rounded-full px-2 text-xs font-medium transition-all duration-200', activeCategory === category.id - ? 'text-white bg-primary' + ? 'bg-primary text-white' : 'text-muted-foreground hover:text-foreground hover:bg-muted/50', )} > @@ -914,9 +912,7 @@ function MailCategoryTabs({ onClick={() => { setActiveCategory(category.id); }} - className={cn( - 'flex items-center gap-1.5 rounded-full px-2 text-xs font-medium', - )} + className={cn('flex items-center gap-1.5 rounded-full px-2 text-xs font-medium')} tabIndex={-1} >

{category.icon}

diff --git a/apps/mail/components/mail/thread-display.tsx b/apps/mail/components/mail/thread-display.tsx index 6c458c2c8..f90a40d7a 100644 --- a/apps/mail/components/mail/thread-display.tsx +++ b/apps/mail/components/mail/thread-display.tsx @@ -1,16 +1,3 @@ -import { - Archive, - ArchiveX, - Expand, - Forward, - MailOpen, - Reply, - ReplyAll, - X, - Trash, - MoreVertical, - StickyNote, -} from 'lucide-react'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useParams } from 'next/navigation'; @@ -22,6 +9,17 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +import { + ChevronLeft, + ChevronRight, + X, + Reply, + Archive, + ThreeDots, + Trash, + Expand, + ArchiveX +} from '../icons/icons'; import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { moveThreadsTo, ThreadDestination } from '@/lib/thread-actions'; import { useThread, useThreads } from '@/hooks/use-threads'; @@ -32,6 +30,7 @@ import { modifyLabels } from '@/actions/mail'; import { useStats } from '@/hooks/use-stats'; import ThreadSubject from './thread-subject'; import ReplyCompose from './reply-composer'; +import { Separator } from '../ui/separator'; import { useTranslations } from 'next-intl'; import { useMail } from '../mail/use-mail'; import { NotesPanel } from './note-panel'; @@ -131,11 +130,11 @@ function ThreadActionButton({ onMouseEnter={() => iconRef.current?.startAnimation?.()} onMouseLeave={() => iconRef.current?.stopAnimation?.()} > - + {label} - {label} + {/* {label} */} ); @@ -229,48 +228,48 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) { [threadId, folder, mutateStats, mutateThreads, handleClose, t], ); - const handleMarkAsUnread = useCallback(async () => { - if (!emailData || !threadId) return; + // const handleMarkAsUnread = useCallback(async () => { + // if (!emailData || !threadId) return; - const promise = async () => { - const result = await markAsUnread({ ids: [threadId] }); - if (!result.success) throw new Error('Failed to mark as unread'); + // const promise = async () => { + // const result = await markAsUnread({ ids: [threadId] }); + // if (!result.success) throw new Error('Failed to mark as unread'); - setMail((prev) => ({ ...prev, bulkSelected: [] })); - await Promise.allSettled([mutateStats(), mutateThread()]); - handleClose(); - }; + // setMail((prev) => ({ ...prev, bulkSelected: [] })); + // await Promise.allSettled([mutateStats(), mutateThread()]); + // handleClose(); + // }; - toast.promise(promise(), { - loading: t('common.actions.markingAsUnread'), - success: t('common.mail.markedAsUnread'), - error: t('common.mail.failedToMarkAsUnread'), - }); - }, [emailData, threadId, t]); + // toast.promise(promise(), { + // loading: t('common.actions.markingAsUnread'), + // success: t('common.mail.markedAsUnread'), + // error: t('common.mail.failedToMarkAsUnread'), + // }); + // }, [emailData, threadId, t]); - const handleFavourites = async () => { - if (!emailData || !threadId) return; - const done = Promise.all([mutateThreads()]); - if (emailData.latest?.tags?.includes('STARRED')) { - toast.promise( - modifyLabels({ threadId: [threadId], removeLabels: ['STARRED'] }).then(() => done), - { - success: t('common.actions.removedFromFavorites'), - loading: t('common.actions.removingFromFavorites'), - error: t('common.actions.failedToRemoveFromFavorites'), - }, - ); - } else { - toast.promise( - modifyLabels({ threadId: [threadId], addLabels: ['STARRED'] }).then(() => done), - { - success: t('common.actions.addedToFavorites'), - loading: t('common.actions.addingToFavorites'), - error: t('common.actions.failedToAddToFavorites'), - }, - ); - } - }; + // const handleFavourites = async () => { + // if (!emailData || !threadId) return; + // const done = Promise.all([mutateThreads()]); + // if (emailData.latest?.tags?.includes('STARRED')) { + // toast.promise( + // modifyLabels({ threadId: [threadId], removeLabels: ['STARRED'] }).then(() => done), + // { + // success: t('common.actions.removedFromFavorites'), + // loading: t('common.actions.removingFromFavorites'), + // error: t('common.actions.failedToRemoveFromFavorites'), + // }, + // ); + // } else { + // toast.promise( + // modifyLabels({ threadId: [threadId], addLabels: ['STARRED'] }).then(() => done), + // { + // success: t('common.actions.addedToFavorites'), + // loading: t('common.actions.addingToFavorites'), + // error: t('common.actions.failedToAddToFavorites'), + // }, + // ); + // } + // }; useEffect(() => { const handleEsc = (event: KeyboardEvent) => { @@ -297,6 +296,9 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) { isFullscreen ? 'fixed inset-0 z-50' : '', )} > +
+ +
{!id ? (
@@ -317,17 +319,63 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
) : ( <> -
+
- + {/* */} +
{' '} +
+ + +
- { + setMode('replyAll'); + }} + className="inline-flex h-7 items-center justify-center gap-1 overflow-hidden rounded-md bg-white px-1.5 dark:bg-[#313131]" + > + +
+
+ Reply all +
+
+ + + {/* */} + + + + {/* { setMode('reply'); }} - /> - {hasMultipleParticipants && ( + /> */} + {/* {hasMultipleParticipants && ( - )} - { setMode('forward'); }} - /> + /> */} - + - + {/* {threadId && ( e.preventDefault()}> @@ -374,7 +421,7 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) { )} */} setIsFullscreen(!isFullscreen)}> - + {isFullscreen ? t('common.threadDisplay.exitFullscreen') @@ -388,22 +435,19 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) { ) : ( <> - moveThreadTo('archive')}> - - {t('common.threadDisplay.archive')} - + moveThreadTo('spam')}> - + {t('common.threadDisplay.moveToSpam')} - moveThreadTo('bin')}> - - {t('common.mail.moveToBin')} - + )} +
diff --git a/apps/mail/components/ui/ai-sidebar.tsx b/apps/mail/components/ui/ai-sidebar.tsx index 09480557e..5fd40b55d 100644 --- a/apps/mail/components/ui/ai-sidebar.tsx +++ b/apps/mail/components/ui/ai-sidebar.tsx @@ -86,7 +86,7 @@ export function AISidebar({ children, className }: AISidebarProps & { children: {open && ( <> - +