This commit is contained in:
Nizzy
2025-04-22 10:11:15 -07:00
parent bcb7ec4277
commit 85e76fc2e5
7 changed files with 554 additions and 156 deletions

View File

@@ -1,3 +1,5 @@
import { cn } from '@/lib/utils';
export const Vercel = ({ className }: { className?: string }) => (
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className}>
<title>Vercel</title>
@@ -454,7 +456,13 @@ export const Bell = ({ className }: { className?: string }) => (
);
export const Tag = ({ className }: { className?: string }) => (
<svg width="15.5" height="15.5" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg
width="15.5"
height="15.5"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
className={className}
fill-rule="evenodd"
@@ -599,8 +607,189 @@ export const Lightning = ({ className }: { className?: string }) => (
);
export const Mail = ({ className }: { className?: string }) => (
<svg width="14" height="16" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<svg
width="14"
height="16"
viewBox="0 0 22 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M0.5 5.6691V14.25C0.5 15.9069 1.84315 17.25 3.5 17.25H18.5C20.1569 17.25 21.5 15.9069 21.5 14.25V5.6691L12.5723 11.1631C11.6081 11.7564 10.3919 11.7564 9.42771 11.1631L0.5 5.6691Z" />
<path d="M21.5 3.90783V3.75C21.5 2.09315 20.1569 0.75 18.5 0.75H3.5C1.84315 0.75 0.5 2.09315 0.5 3.75V3.90783L10.2139 9.88558C10.696 10.1823 11.304 10.1823 11.7861 9.88558L21.5 3.90783Z" />
</svg>
);
export const X = ({ className }: { className?: string }) => (
<svg
width="2"
height="2"
viewBox="1.5 2 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M6.28033 5.21967C5.98744 4.92678 5.51256 4.92678 5.21967 5.21967C4.92678 5.51256 4.92678 5.98744 5.21967 6.28033L8.93934 10L5.21967 13.7197C4.92678 14.0126 4.92678 14.4874 5.21967 14.7803C5.51256 15.0732 5.98744 15.0732 6.28033 14.7803L10 11.0607L13.7197 14.7803C14.0126 15.0732 14.4874 15.0732 14.7803 14.7803C15.0732 14.4874 15.0732 14.0126 14.7803 13.7197L11.0607 10L14.7803 6.28033C15.0732 5.98744 15.0732 5.51256 14.7803 5.21967C14.4874 4.92678 14.0126 4.92678 13.7197 5.21967L10 8.93934L6.28033 5.21967Z" />
</svg>
);
export const ChevronLeft = ({ className }: { className?: string }) => (
<svg
width="5"
height="8"
viewBox="-2.5 -2 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.78033 0.21967C5.07322 0.512563 5.07322 0.987437 4.78033 1.28033L2.06066 4L4.78033 6.71967C5.07322 7.01256 5.07322 7.48744 4.78033 7.78033C4.48744 8.07322 4.01256 8.07322 3.71967 7.78033L0.46967 4.53033C0.176777 4.23744 0.176777 3.76256 0.46967 3.46967L3.71967 0.21967C4.01256 -0.0732233 4.48744 -0.0732233 4.78033 0.21967Z"
/>
</svg>
);
export const ChevronRight = ({ className }: { className?: string }) => (
<svg
width="5"
height="8"
viewBox="-4 -2 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0.21967 0.21967C0.512563 -0.0732233 0.987437 -0.0732233 1.28033 0.21967L4.53033 3.46967C4.82322 3.76256 4.82322 4.23744 4.53033 4.53033L1.28033 7.78033C0.987437 8.07322 0.512563 8.07322 0.21967 7.78033C-0.0732233 7.48744 -0.0732233 7.01256 0.21967 6.71967L2.93934 4L0.21967 1.28033C-0.0732232 0.987437 -0.0732232 0.512563 0.21967 0.21967Z"
/>
</svg>
);
export const Reply = ({ className }: { className?: string }) => (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5 7.75C10.5 6.23122 9.26878 5 7.75 5H2.56066L4.78033 7.21967C5.07322 7.51256 5.07322 7.98744 4.78033 8.28033C4.48744 8.57322 4.01256 8.57322 3.71967 8.28033L0.21967 4.78033C-0.0732234 4.48744 -0.0732233 4.01256 0.21967 3.71967L3.71967 0.21967C4.01256 -0.0732233 4.48744 -0.0732233 4.78033 0.21967C5.07322 0.512563 5.07322 0.987437 4.78033 1.28033L2.56066 3.5L7.75 3.5C10.0972 3.5 12 5.40279 12 7.75C12 10.0972 10.0972 12 7.75 12H6.75C6.33579 12 6 11.6642 6 11.25C6 10.8358 6.33579 10.5 6.75 10.5H7.75C9.26878 10.5 10.5 9.26878 10.5 7.75Z"
/>
</svg>
);
export const Forward = ({ className }: { className?: string }) => (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 6C-1.81059e-08 5.58579 0.335786 5.25 0.75 5.25L9.43934 5.25L6.21967 2.03033C5.92678 1.73744 5.92678 1.26256 6.21967 0.96967C6.51256 0.676777 6.98744 0.676777 7.28033 0.96967L11.7803 5.46967C12.0732 5.76256 12.0732 6.23744 11.7803 6.53033L7.28033 11.0303C6.98744 11.3232 6.51256 11.3232 6.21967 11.0303C5.92678 10.7374 5.92678 10.2626 6.21967 9.96967L9.43934 6.75L0.75 6.75C0.335786 6.75 1.81059e-08 6.41421 0 6Z"
/>
</svg>
);
export const Star = ({ className }: { className?: string }) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.78814 1.21068C9.23648 0.132735 10.7635 0.132735 11.2119 1.21068L13.2938 6.2164L18.6979 6.64964C19.8617 6.74293 20.3336 8.19522 19.4469 8.95473L15.3296 12.4817L16.5875 17.7551C16.8584 18.8908 15.623 19.7883 14.6267 19.1798L10 16.3538L5.37335 19.1798C4.37703 19.7883 3.14163 18.8908 3.41252 17.7551L4.67043 12.4817L0.553089 8.95473C-0.333552 8.19523 0.138322 6.74293 1.30206 6.64964L6.70615 6.2164L8.78814 1.21068Z"
/>
</svg>
);
export const ThreeDots = ({ className }: { className?: string }) => (
<svg
className={className}
width="12"
height="4"
viewBox="0 0 12 4"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 2C0 1.17157 0.671573 0.5 1.5 0.5C2.32843 0.5 3 1.17157 3 2C3 2.82843 2.32843 3.5 1.5 3.5C0.671573 3.5 0 2.82843 0 2Z" />
<path d="M4.5 2C4.5 1.17157 5.17157 0.5 6 0.5C6.82843 0.5 7.5 1.17157 7.5 2C7.5 2.82843 6.82843 3.5 6 3.5C5.17157 3.5 4.5 2.82843 4.5 2Z" />
<path d="M10.5 0.5C9.67157 0.5 9 1.17157 9 2C9 2.82843 9.67157 3.5 10.5 3.5C11.3284 3.5 12 2.82843 12 2C12 1.17157 11.3284 0.5 10.5 0.5Z" />
</svg>
);
export const Trash = ({ className }: { className?: string }) => (
<svg
className={className}
width="12"
height="14"
viewBox="0 0 12 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3 2.25V3H0.75C0.335786 3 0 3.33579 0 3.75C0 4.16421 0.335786 4.5 0.75 4.5H1.05L1.86493 12.6493C1.94161 13.4161 2.58685 14 3.35748 14H8.64252C9.41315 14 10.0584 13.4161 10.1351 12.6493L10.95 4.5H11.25C11.6642 4.5 12 4.16421 12 3.75C12 3.33579 11.6642 3 11.25 3H9V2.25C9 1.00736 7.99264 0 6.75 0H5.25C4.00736 0 3 1.00736 3 2.25ZM5.25 1.5C4.83579 1.5 4.5 1.83579 4.5 2.25V3H7.5V2.25C7.5 1.83579 7.16421 1.5 6.75 1.5H5.25ZM4.05044 6.00094C4.46413 5.98025 4.81627 6.29885 4.83696 6.71255L5.11195 12.2125C5.13264 12.6262 4.81404 12.9784 4.40034 12.9991C3.98665 13.0197 3.63451 12.7011 3.61383 12.2875L3.33883 6.78745C3.31814 6.37376 3.63674 6.02162 4.05044 6.00094ZM7.95034 6.00094C8.36404 6.02162 8.68264 6.37376 8.66195 6.78745L8.38696 12.2875C8.36627 12.7011 8.01413 13.0197 7.60044 12.9991C7.18674 12.9784 6.86814 12.6262 6.88883 12.2125L7.16383 6.71255C7.18451 6.29885 7.53665 5.98025 7.95034 6.00094Z"
/>
</svg>
);
export const Expand = ({ className }: { className?: string }) => (
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M12.85 1.75022C12.85 1.25316 13.2529 0.85022 13.75 0.85022L18.25 0.85022C18.747 0.85022 19.15 1.25316 19.15 1.75022V6.25022C19.15 6.74728 18.747 7.15022 18.25 7.15022C17.7529 7.15022 17.35 6.74728 17.35 6.25022V3.92301L13.6364 7.63662C13.2849 7.98809 12.715 7.98809 12.3636 7.63662C12.0121 7.28514 12.0121 6.7153 12.3636 6.36382L16.0772 2.65022L13.75 2.65022C13.2529 2.65022 12.85 2.24728 12.85 1.75022Z" />
<path d="M0.849976 1.75022C0.849976 1.25316 1.25292 0.85022 1.74998 0.85022H6.24998C6.74703 0.85022 7.14998 1.25316 7.14998 1.75022C7.14998 2.24728 6.74703 2.65022 6.24998 2.65022H3.92277L7.63637 6.36382C7.98784 6.7153 7.98784 7.28514 7.63637 7.63662C7.2849 7.98809 6.71505 7.98809 6.36358 7.63662L2.64998 3.92301V6.25022C2.64998 6.74728 2.24703 7.15022 1.74998 7.15022C1.25292 7.15022 0.849976 6.74728 0.849976 6.25022V1.75022Z" />
<path d="M7.63637 12.3638C7.98784 12.7153 7.98784 13.2851 7.63637 13.6366L3.92277 17.3502H6.24998C6.74703 17.3502 7.14998 17.7532 7.14998 18.2502C7.14998 18.7473 6.74703 19.1502 6.24998 19.1502H1.74998C1.25292 19.1502 0.849976 18.7473 0.849976 18.2502V13.7502C0.849976 13.2532 1.25292 12.8502 1.74998 12.8502C2.24703 12.8502 2.64998 13.2532 2.64998 13.7502V16.0774L6.36358 12.3638C6.71505 12.0124 7.2849 12.0124 7.63637 12.3638Z" />
<path d="M12.3636 12.3638C12.715 12.0124 13.2849 12.0124 13.6364 12.3638L17.35 16.0774V13.7502C17.35 13.2532 17.7529 12.8502 18.25 12.8502C18.747 12.8502 19.15 13.2532 19.15 13.7502V18.2502C19.15 18.7473 18.747 19.1502 18.25 19.1502H13.75C13.2529 19.1502 12.85 18.7473 12.85 18.2502C12.85 17.7532 13.2529 17.3502 13.75 17.3502H16.0772L12.3636 13.6366C12.0121 13.2851 12.0121 12.7153 12.3636 12.3638Z" />
</svg>
);
export const ArchiveX = ({ className }: { className?: string }) => (
<svg
width="22"
height="18"
viewBox="0 0 22 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path d="M2.375 0C1.33947 0 0.5 0.839466 0.5 1.875V2.625C0.5 3.66053 1.33947 4.5 2.375 4.5H19.625C20.6605 4.5 21.5 3.66053 21.5 2.625V1.875C21.5 0.839467 20.6605 0 19.625 0H2.375Z" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.08679 6L2.62657 15.1762C2.71984 16.7619 4.03296 18 5.62139 18H16.3783C17.9667 18 19.2799 16.7619 19.3731 15.1762L19.9129 6H2.08679ZM8.21967 8.84467C8.51256 8.55178 8.98744 8.55178 9.28033 8.84467L11 10.5643L12.7197 8.84467C13.0126 8.55178 13.4874 8.55178 13.7803 8.84467C14.0732 9.13756 14.0732 9.61244 13.7803 9.90533L12.0607 11.625L13.7803 13.3447C14.0732 13.6376 14.0732 14.1124 13.7803 14.4053C13.4874 14.6982 13.0126 14.6982 12.7197 14.4053L11 12.6857L9.28033 14.4053C8.98744 14.6982 8.51256 14.6982 8.21967 14.4053C7.92678 14.1124 7.92678 13.6376 8.21967 13.3447L9.93934 11.625L8.21967 9.90533C7.92678 9.61244 7.92678 9.13756 8.21967 8.84467Z"
/>
</svg>
);
export const Calendar = ({ className }: { className?: string }) => (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.75 2.25C7.16421 2.25 7.5 2.58579 7.5 3V4.5H16.5V3C16.5 2.58579 16.8358 2.25 17.25 2.25C17.6642 2.25 18 2.58579 18 3V4.5H18.75C20.4069 4.5 21.75 5.84315 21.75 7.5V18.75C21.75 20.4069 20.4069 21.75 18.75 21.75H5.25C3.59315 21.75 2.25 20.4069 2.25 18.75V7.5C2.25 5.84315 3.59315 4.5 5.25 4.5H6V3C6 2.58579 6.33579 2.25 6.75 2.25ZM20.25 11.25C20.25 10.4216 19.5784 9.75 18.75 9.75H5.25C4.42157 9.75 3.75 10.4216 3.75 11.25V18.75C3.75 19.5784 4.42157 20.25 5.25 20.25H18.75C19.5784 20.25 20.25 19.5784 20.25 18.75V11.25Z"
/>
</svg>
);

View File

@@ -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 (
<div className="flex">
{visibleLabels.map((label, index) => {
const normalizedLabel = label.toLowerCase().replace(/^category_/i, '');
let icon = null;
let bgColor = '';
switch (normalizedLabel) {
case 'important':
icon = <Lightning className="h-3.5 w-3.5 fill-white" />;
bgColor = 'bg-[#F59E0D]';
break;
case 'promotions':
icon = <Tag className="h-3.5 w-3.5 fill-white" />;
bgColor = 'bg-[#F43F5E]';
break;
case 'personal':
icon = <User className="h-3.5 w-3.5 fill-white" />;
bgColor = 'bg-[#39AE4A]';
break;
case 'updates':
icon = <Bell className="h-3.5 w-3.5 fill-white" />;
bgColor = 'bg-[#8B5CF6]';
break;
case 'work':
icon = <Briefcase className="h-3.5 w-3.5 text-white" />;
bgColor = 'bg-neutral-600';
break;
case 'forums':
icon = <Users className="h-3.5 w-3.5 text-white" />;
bgColor = 'bg-blue-600';
break;
case 'notes':
icon = <StickyNote className="h-3.5 w-3.5 text-white" />;
bgColor = 'bg-amber-500';
break;
case 'starred':
icon = <Star className="h-3.5 w-3.5 text-white" />;
bgColor = 'bg-yellow-500';
break;
default:
return null;
}
return (
<Badge
key={`${label}-${index}`}
className={`rounded-md p-1 ${bgColor} -ml-1.5 border-2 border-white transition-transform first:ml-0 dark:border-[#1A1A1A]`}
>
{icon}
</Badge>
);
})}
</div>
);
};
// 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<boolean>(false);
const [unsubscribed, setUnsubscribed] = useState(false);
const [isUnsubscribing, setIsUnsubscribing] = useState(false);
const { folder } = useParams<{ folder: string }>();
const [selectedAttachment, setSelectedAttachment] = useState<null | {
id: string;
name: string;
@@ -107,11 +200,11 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
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) =>
<div className={cn('relative flex-1 overflow-hidden')} id={`mail-${emailData.id}`}>
<div className="relative h-full overflow-y-auto">
<div
className="flex flex-col gap-4 p-4 pb-2 transition-all duration-200 cursor-pointer"
className="flex cursor-pointer flex-col p-4 pb-2 transition-all duration-200"
onClick={() => setIsCollapsed(!isCollapsed)}
>
<div className="flex items-start justify-between gap-4">
<div className="flex items-start justify-center gap-4">
<Avatar className="h-8 w-8">
{index === 0 && (
<div className="mb-2">
<p className="font-medium text-black dark:text-white">
{emailData.subject}{' '}
<span className="text-[#6D6D6D] dark:text-[#8C8C8C]">
{totalEmails && `[${totalEmails}]`}
</span>
</p>
<div className="mb-4 mt-1 flex items-center gap-1">
<Calendar className="size-3.5 fill-[#6D6D6D] dark:fill-[#8C8C8C]" />
<span className="text-sm font-medium text-[#6D6D6D] dark:text-[#8C8C8C]">
{formatDate(emailData?.receivedOn)}
</span>
</div>
<div className="flex items-center gap-4">
<MailDisplayLabels labels={emailData?.tags || []} />
<div className="bg-iconLight dark:bg-iconDark/20 relative h-3 w-0.5 rounded-full" />
<div className="flex items-center gap-2 text-sm text-[#6D6D6D] dark:text-[#8C8C8C]">
{(() => {
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) => (
<div
key={person.email}
className="inline-flex items-center justify-start gap-1.5 overflow-hidden rounded-full border border-[#DBDBDB] bg-[#F5F5F5] py-1 pl-1 pr-2.5 dark:border-[#2B2B2B] dark:bg-[#1A1A1A]"
>
<Avatar className="h-5 w-5">
<AvatarImage src={getEmailLogo(person.email)} className="rounded-full" />
<AvatarFallback className="rounded-full bg-[#FFFFFF] text-xs font-bold dark:bg-[#373737]">
{getFirstLetterCharacter(person.name)}
</AvatarFallback>
</Avatar>
<div className="justify-start text-sm font-medium leading-none text-[#1A1A1A] dark:text-white">
{person.name || person.email}
</div>
</div>
);
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)}
<span className="text-sm">+{people.length - 2} others</span>
</>
);
}
return null;
})()}
</div>
</div>
</div>
)}
<div className="flex items-start justify-between gap-4 w-full mt-3">
<div className="flex justify-center gap-4 w-full ">
<Avatar className="h-8 w-8 rounded-full">
<AvatarImage
className="bg-muted-foreground/50 dark:bg-muted/50 p-2"
className="bg-muted-foreground/50 dark:bg-muted/50 rounded-full p-2"
src={getEmailLogo(emailData?.sender?.email)}
/>
<AvatarFallback className="bg-muted-foreground/50 dark:bg-muted/50">
{emailData?.sender?.name[0]?.toUpperCase()}
<AvatarFallback className="rounded-full bg-[#FFFFFF] font-bold dark:bg-[#373737]">
{getFirstLetterCharacter(emailData?.sender?.name)}
</AvatarFallback>
</Avatar>
<div className="relative bottom-1 flex-1">
<div className="flex items-center justify-start gap-2">
<span className="font-semibold">{emailData?.sender?.name}</span>
<div className="flex w-full items-center justify-between">
<div className="flex items-center justify-start gap-2 w-full">
<div className="flex flex-col w-full">
<div className="flex items-center justify-between w-full">
<span className="font-semibold">
{cleanNameDisplay(emailData?.sender?.name)}
</span>
<time className="text-sm dark:text-[#8C8C8C] text-[#6D6D6D] font-medium">
{formatDate(emailData?.receivedOn)}
</time>
</div>
{/* <p className="text-sm dark:text-[#8C8C8C] text-[#6D6D6D] font-medium">To: get</p> */}
</div>
<span className="text-muted-foreground flex grow-0 items-center gap-2 text-sm">
<span className="overflow-hidden text-ellipsis whitespace-nowrap min-w-0">{emailData?.sender?.email}</span>
{/* <span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{emailData?.sender?.email}
</span> */}
{listUnsubscribeAction && (
{/* {listUnsubscribeAction && (
<Dialog>
<DialogTrigger asChild>
<Button
@@ -220,12 +405,12 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
</DialogContent>
</Dialog>
)}
{isMuted && <BellOff className="h-4 w-4" />}
{isMuted && <BellOff className="h-4 w-4" />} */}
</span>
</div>
<div className="flex items-center gap-2">
{/* <div className="flex items-center gap-2">
<time className="text-muted-foreground text-xs">
{format(new Date(emailData?.receivedOn), 'PPp')}
{formatDate(emailData?.receivedOn)}
</time>
<Popover open={openDetailsPopover} onOpenChange={setOpenDetailsPopover}>
<PopoverTrigger asChild>
@@ -257,11 +442,11 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
</span>
<div className="ml-3">
<span className="text-muted-foreground pr-1 font-bold">
{emailData?.sender?.name}
{cleanNameDisplay(emailData?.sender?.name)}
</span>
{emailData?.sender?.name !== emailData?.sender?.email && (
<span className="text-muted-foreground">
{emailData?.sender?.email}
{cleanEmailDisplay(emailData?.sender?.email)}
</span>
)}
</div>
@@ -271,7 +456,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
{t('common.mailDisplay.to')}:
</span>
<span className="text-muted-foreground ml-3">
{emailData?.to?.map((t) => t.email).join(', ')}
{emailData?.to?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
</span>
</div>
{emailData?.cc && emailData.cc.length > 0 && (
@@ -280,7 +465,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
{t('common.mailDisplay.cc')}:
</span>
<span className="text-muted-foreground ml-3">
{emailData?.cc?.map((t) => t.email).join(', ')}
{emailData?.cc?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
</span>
</div>
)}
@@ -290,7 +475,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
{t('common.mailDisplay.bcc')}:
</span>
<span className="text-muted-foreground ml-3">
{emailData?.bcc?.map((t) => t.email).join(', ')}
{emailData?.bcc?.map((t) => cleanEmailDisplay(t.email)).join(', ')}
</span>
</div>
)}
@@ -307,7 +492,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
{t('common.mailDisplay.mailedBy')}:
</span>
<span className="text-muted-foreground ml-3">
{emailData?.sender?.email}
{cleanEmailDisplay(emailData?.sender?.email)}
</span>
</div>
<div className="flex">
@@ -315,7 +500,7 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
{t('common.mailDisplay.signedBy')}:
</span>
<span className="text-muted-foreground ml-3">
{emailData?.sender?.email}
{cleanEmailDisplay(emailData?.sender?.email)}
</span>
</div>
{emailData.tls && (
@@ -332,34 +517,9 @@ const MailDisplay = ({ emailData, isMuted, index, totalEmails, demo }: Props) =>
</div>
</PopoverContent>
</Popover>
</div>
</div> */}
</div>
</div>
{data ? (
<div className='relative -top-1'>
<Popover>
<PopoverTrigger asChild>
<Button
size={'icon'}
variant="ghost"
className="rounded-md"
onClick={(e) => e.stopPropagation()}
>
<Image
src="/ai.svg"
alt="logo"
className="h-6 w-6"
width={100}
height={100}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="relative -left-24 rounded-lg border p-3 shadow-lg">
<StreamingText text={data.content} />
</PopoverContent>
</Popover>
</div>
) : null}
</div>
</div>

View File

@@ -252,7 +252,7 @@ const Thread = memo(
)}
>
<span className={cn(threadId ? 'max-w-[3ch] truncate' : '')}>
{highlightText(latestMessage.sender.name, searchValue.highlight)}
{highlightText(cleanNameDisplay(latestMessage.sender.name) || '', searchValue.highlight)}
</span>{' '}
{latestMessage.unread && !isMailSelected ? (
<span className="size-2 rounded bg-[#006FFE]" />
@@ -370,7 +370,7 @@ const Thread = memo(
)}
>
<span className={cn('truncate text-sm', threadId ? 'max-w-[20ch]' : 'max-w-[20ch]')}>
{highlightText(latestMessage.sender.name, searchValue.highlight)}
{highlightText(cleanNameDisplay(latestMessage.sender.name) || '', searchValue.highlight)}
</span>{' '}
</p>
{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<typeof Badge>['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;
};

View File

@@ -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() {
)}
>
<SidebarToggle className="h-fit px-2" />
<div>
<MailCategoryTabs
iconsOnly={true}
@@ -163,7 +164,7 @@ export function DemoMailLayout() {
{isDesktop && mail.selected && (
<>
<ResizableHandle className="opacity-0" />
<div className="opacity-0" />
<ResizablePanel
className="bg-offsetLight dark:bg-offsetDark shadow-sm md:flex md:rounded-2xl md:border md:shadow-sm"
defaultSize={75}
@@ -293,15 +294,11 @@ export function MailLayout() {
autoSaveId="mail-panel-layout"
className="rounded-inherit gap-0.5 overflow-hidden"
>
<ResizablePanel
className={cn('border-none !bg-transparent', threadId ? 'md:hidden lg:block' : '')}
defaultSize={isMobile ? 100 : 33}
minSize={isMobile ? 100 : 33}
>
<div className={cn('border-none !bg-transparent', threadId ? 'md:hidden lg:block' : '')}>
<div className="bg-panelLight dark:bg-panelDark flex-1 flex-col overflow-y-auto border-[#E7E7E7] shadow-inner md:flex md:rounded-2xl md:border md:shadow-sm dark:border-[#252525]">
<div
className={cn(
'sticky top-0 z-10 flex items-center justify-between gap-1.5 border-b border-[#E7E7E7] p-2 px-[20px] transition-colors md:min-h-14 dark:border-[#252525]',
'sticky top-0 z-10 flex items-center justify-between gap-1.5 border-b border-[#E7E7E7] dark:border-[#252525] p-2 px-[20px] transition-colors md:min-h-14 ',
)}
>
<div className="flex w-full items-center justify-between gap-2">
@@ -373,11 +370,11 @@ export function MailLayout() {
isValidating ? 'opacity-100' : 'opacity-0',
)}
/>
<div className="h-[calc(100dvh-56px)] overflow-hidden pt-0 md:h-[calc(100dvh-170px)]">
<div className="h-[calc(100dvh-56px)] overflow-hidden pt-0 md:h-[calc(100dvh-165px)]">
<MailList isCompact={true} />
</div>
</div>
</ResizablePanel>
</div>
<ResizableHandle className="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}
>
<p>{category.icon}</p>

View File

@@ -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?.()}
>
<Icon ref={iconRef} className="h-4 w-4" />
<Icon ref={iconRef} className="dark:fill-iconDark fill-iconLight" />
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>{label}</TooltipContent>
{/* <TooltipContent>{label}</TooltipContent> */}
</Tooltip>
</TooltipProvider>
);
@@ -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' : '',
)}
>
<div>
</div>
{!id ? (
<div className="flex h-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-2 text-center">
@@ -317,17 +319,63 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
</div>
) : (
<>
<div className="flex flex-shrink-0 items-center border-b px-1 pb-1 md:px-3 md:pb-2 md:pt-[14px]">
<div className="flex flex-shrink-0 items-center border-b border-[#E7E7E7] px-1 pb-1 md:px-3 md:pb-[11px] md:pt-[12px] dark:border-[#252525]">
<div className="flex flex-1 items-center gap-2">
<ThreadActionButton
icon={X}
label={t('common.actions.close')}
onClick={handleClose}
/>
<ThreadSubject subject={emailData.latest?.subject} />
{/* <ThreadSubject subject={emailData.latest?.subject} /> */}
<div className="bg-iconLight dark:bg-iconDark/20 relative h-3 w-0.5 rounded-full" />{' '}
<div>
<ThreadActionButton
icon={ChevronLeft}
label="Previous email"
onClick={handleClose}
/>
<ThreadActionButton
icon={ChevronRight}
label="Next email"
onClick={handleClose}
/>
</div>
</div>
<div className="flex items-center md:gap-2">
<ThreadActionButton
<button
onClick={() => {
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 className="fill-[#6D6D6D] dark:fill-[#9B9B9B]" />
<div className="flex items-center justify-center gap-2.5 pl-0.5 pr-1">
<div className="justify-start font-['Inter'] text-sm leading-none text-white">
Reply all
</div>
</div>
</button>
{/* <button
onClick={() => {
setMode('forward');
}}
className="inline-flex h-7 items-center justify-center gap-1 overflow-hidden rounded-md bg-white px-1.5 dark:bg-[#313131]"
>
<Forward className="fill-[#6D6D6D] dark:fill-[#9B9B9B]" />
<div className="flex items-center justify-center gap-2.5 pl-0.5 pr-1">
<div className="justify-start font-['Inter'] text-sm leading-none text-white">
Forward
</div>
</div>
</button> */}
<button className="inline-flex h-7 w-7 items-center justify-center gap-1 overflow-hidden rounded-md bg-white dark:bg-[#313131]">
<Archive className="fill-iconLight dark:fill-iconDark" />
</button>
{/* <ThreadActionButton
icon={Reply}
label={t('common.threadDisplay.reply')}
disabled={!emailData}
@@ -335,8 +383,8 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
onClick={() => {
setMode('reply');
}}
/>
{hasMultipleParticipants && (
/> */}
{/* {hasMultipleParticipants && (
<ThreadActionButton
icon={ReplyAll}
label={t('common.threadDisplay.replyAll')}
@@ -346,8 +394,8 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
setMode('replyAll');
}}
/>
)}
<ThreadActionButton
)} */}
{/* <ThreadActionButton
icon={Forward}
label={t('common.threadDisplay.forward')}
disabled={!emailData}
@@ -355,15 +403,14 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
onClick={() => {
setMode('forward');
}}
/>
/> */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More actions</span>
</Button>
<button className="inline-flex h-7 w-7 items-center justify-center gap-1 overflow-hidden rounded-md bg-white dark:bg-[#313131]">
<ThreeDots className="fill-iconLight dark:fill-iconDark" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end" className='bg-white dark:bg-[#313131]'>
{/* {threadId && (
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<StickyNote className="mr-2 h-4 w-4" />
@@ -374,7 +421,7 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
</DropdownMenuItem>
)} */}
<DropdownMenuItem onClick={() => setIsFullscreen(!isFullscreen)}>
<Expand className="mr-2 h-4 w-4" />
<Expand className="fill-iconLight dark:fill-iconDark mr-2" />
<span>
{isFullscreen
? t('common.threadDisplay.exitFullscreen')
@@ -388,22 +435,19 @@ export function ThreadDisplay({ isMobile, id }: ThreadDisplayProps) {
</DropdownMenuItem>
) : (
<>
<DropdownMenuItem onClick={() => moveThreadTo('archive')}>
<Archive className="mr-2 h-4 w-4" />
<span>{t('common.threadDisplay.archive')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => moveThreadTo('spam')}>
<ArchiveX className="mr-2 h-4 w-4" />
<ArchiveX className="fill-iconLight dark:fill-iconDark mr-2" />
<span>{t('common.threadDisplay.moveToSpam')}</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => moveThreadTo('bin')}>
<Trash className="mr-2 h-4 w-4" />
<span>{t('common.mail.moveToBin')}</span>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<button className="inline-flex h-7 w-7 items-center justify-center gap-1 overflow-hidden rounded-md border border-[#FCCDD5] bg-[#FDE4E9] dark:border-[#6E2532] dark:bg-[#411D23]">
<Trash className="fill-[#F43F5E]" />
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col">

View File

@@ -86,7 +86,7 @@ export function AISidebar({ children, className }: AISidebarProps & { children:
{open && (
<>
<ResizableHandle className='opacity-0 w-1.5' />
<ResizablePanel defaultSize={25} minSize={20} maxSize={25} className="bg-panelLight dark:bg-panelDark shadow-sm dark:border-[#252525] border-[#E7E7E7] md:rounded-2xl md:border md:shadow-sm h-[calc(98vh+6px)] mt-1 mr-1.5">
<ResizablePanel defaultSize={25} minSize={20} maxSize={45} className="bg-panelLight dark:bg-panelDark shadow-sm dark:border-[#252525] border-[#E7E7E7] md:rounded-2xl md:border md:shadow-sm h-[calc(98vh+9px)] mt-1 mr-1.5">
<div className={cn(
'h-[calc(98vh+15px)]',
'flex flex-col',

View File

@@ -8,7 +8,7 @@ const badgeVariants = cva(
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
default: 'border-transparent bg-primary text-primary-foreground ',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive: