Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/mail/components/home/HomeContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,14 +379,15 @@ export default function HomeContent() {
target="_blank"
>
<img
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=970417&theme=light&t=1748371877825"
src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=970417&theme=light&period=daily&t=1748467771181"
alt="Zero - AI&#0032;Native&#0032;Email&#0032;Client | Product Hunt"
style={{ width: '250px', height: '54px' }}
width="250"
height="54"
className="mt-2"
/>
</a>

</section>

<section className="relative mt-10 hidden flex-col justify-center md:flex">
Expand Down Expand Up @@ -1545,7 +1546,7 @@ export default function HomeContent() {
</div>
</div>

<motion.div
{/* <motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
Expand Down Expand Up @@ -1587,7 +1588,7 @@ export default function HomeContent() {
height={50}
/>
</div>
</motion.div>
</motion.div> */}

<div className="relative mt-52 flex items-center justify-center">
<Footer />
Expand Down
2 changes: 0 additions & 2 deletions apps/mail/components/home/pixelated-bg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ export function PixelatedLeft(props: SVGProps<SVGSVGElement>) {
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<title>Pixelated Left Background</title>
<g opacity="0.3">
<mask
id="mask0_1_696"
Expand Down Expand Up @@ -121,7 +120,6 @@ export function PixelatedRight(props: SVGProps<SVGSVGElement>) {
xmlnsXlink="http://www.w3.org/1999/xlink"
{...props}
>
<title>Pixelated Right Background</title>
<g opacity="0.3">
<mask
id="mask0_1_699"
Expand Down
66 changes: 63 additions & 3 deletions apps/mail/components/mail/mail-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
Download,
MoreVertical,
HardDriveDownload,
Paperclip,
} from 'lucide-react';
import {
DropdownMenu,
Expand All @@ -43,7 +44,7 @@ import AttachmentsAccordion from './attachments-accordion';
import { cn, getEmailLogo, formatDate } from '@/lib/utils';
import { useBrainState } from '../../hooks/use-summary';
import { useThreadLabels } from '@/hooks/use-labels';
import type { Sender, ParsedMessage } from '@/types';
import type { Sender, ParsedMessage, Attachment } from '@/types';
import { Markdown } from '@react-email/components';
import AttachmentDialog from './attachment-dialog';
import { useSummary } from '@/hooks/use-summary';
Expand Down Expand Up @@ -161,6 +162,7 @@ type Props = {
onReply?: () => void;
onReplyAll?: () => void;
onForward?: () => void;
threadAttachments?: Attachment[];
};

const MailDisplayLabels = ({ labels }: { labels: string[] }) => {
Expand Down Expand Up @@ -224,6 +226,7 @@ const MailDisplayLabels = ({ labels }: { labels: string[] }) => {
>
{icon}
</Badge>
<AiSummary />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs text-white">{label}</p>
Expand Down Expand Up @@ -255,6 +258,58 @@ const cleanNameDisplay = (name?: string) => {
return name.trim();
};

const ThreadAttachments = ({ attachments }: { attachments: Attachment[] }) => {
if (!attachments || attachments.length === 0) return null;

const handleDownload = async (attachment: Attachment) => {
try {
// Convert base64 to blob
const byteCharacters = atob(attachment.body);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], { type: attachment.mimeType });

// Create download link
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = attachment.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading attachment:', error);
}
};

return (
<div className="mt-2 w-full">
<div className="flex items-center gap-2">
<span className="text-sm font-medium ">Thread Attachments <span className='text-[#8D8D8D]'>[{attachments.length}]</span></span>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{attachments.map((attachment) => (
<button
key={attachment.attachmentId}
onClick={() => handleDownload(attachment)}
className="flex items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-[#F0F0F0] dark:bg-[#262626] dark:hover:bg-[#303030] cursor-pointer"
>
<span className="text-muted-foreground">{getFileIcon(attachment.filename)}</span>
<span className="max-w-[200px] truncate" title={attachment.filename}>
{attachment.filename}
</span>
<span className="text-muted-foreground">{formatFileSize(attachment.size)}</span>
</button>
))}
</div>
</div>
);
};

const AiSummary = () => {
const [threadId] = useQueryState('threadId');
const { data: summary, isLoading } = useSummary(threadId ?? null);
Expand Down Expand Up @@ -427,7 +482,7 @@ const openAttachment = (attachment: { body: string; mimeType: string; filename:
}
};

const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => {
const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: Props) => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
// const [unsubscribed, setUnsubscribed] = useState(false);
// const [isUnsubscribing, setIsUnsubscribing] = useState(false);
Expand Down Expand Up @@ -982,6 +1037,8 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => {
</span>
</span>
</span>
<AiSummary />

<div className="mt-2 flex items-center gap-2">
{emailData?.tags?.length ? (
<MailDisplayLabels labels={emailData?.tags.map((t) => t.name) || []} />
Expand Down Expand Up @@ -1029,6 +1086,9 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => {
</div>
</div>
{brainState?.enabled && <AiSummary />}
{threadAttachments && threadAttachments.length > 0 && (
<ThreadAttachments attachments={threadAttachments} />
)}
</>
)}
</div>
Expand Down Expand Up @@ -1343,7 +1403,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo }: Props) => {
{emailData?.attachments.map((attachment, index) => (
<div key={index} className="flex">
<button
className="flex cursor-pointer items-center gap-1 rounded-[5px] border bg-[#FAFAFA] px-1.5 py-1 text-sm font-medium hover:bg-[#F0F0F0] dark:bg-[#262626] dark:hover:bg-[#303030]"
className="flex cursor-pointer items-center gap-1 rounded-[5px] bg-[#FAFAFA] px-1.5 py-1 text-sm font-medium hover:bg-[#F0F0F0] dark:bg-[#262626] dark:hover:bg-[#303030]"
onClick={() => openAttachment(attachment)}
>
{getFileIcon(attachment.filename)}
Expand Down
17 changes: 15 additions & 2 deletions apps/mail/components/mail/thread-display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-st
import { useNavigate, useParams, useSearchParams } from 'react-router';
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { focusedIndexAtom } from '@/hooks/use-mail-navigation';
import { backgroundQueueAtom } from '@/store/backgroundQueue';
import { type ThreadDestination } from '@/lib/thread-actions';
Expand All @@ -39,7 +39,8 @@ import { useTRPC } from '@/providers/query-provider';
import { useIsMobile } from '@/hooks/use-mobile';
import { Button } from '@/components/ui/button';
import { useStats } from '@/hooks/use-stats';
import type { ParsedMessage } from '@/types';
import ThreadSubject from './thread-subject';
import type { ParsedMessage, Attachment } from '@/types';
import ReplyCompose from './reply-composer';
import { useTranslations } from 'use-intl';
import { NotesPanel } from './note-panel';
Expand Down Expand Up @@ -169,6 +170,17 @@ export function ThreadDisplay() {
const [isFullscreen, setIsFullscreen] = useState(false);
const [isStarred, setIsStarred] = useState(false);
const [isImportant, setIsImportant] = useState(false);

// Collect all attachments from all messages in the thread
const allThreadAttachments = useMemo(() => {
if (!emailData?.messages) return [];
return emailData.messages.reduce<Attachment[]>((acc, message) => {
if (message.attachments && message.attachments.length > 0) {
return [...acc, ...message.attachments];
}
return acc;
}, []);
}, [emailData?.messages]);
const t = useTranslations();
const { refetch: refetchStats } = useStats();
const [mode, setMode] = useQueryState('mode');
Expand Down Expand Up @@ -1018,6 +1030,7 @@ export function ThreadDisplay() {
isLoading={false}
index={index}
totalEmails={emailData?.totalReplies}
threadAttachments={index === 0 ? allThreadAttachments : undefined}
/>
{mode && activeReplyId === message.id && (
<div className="px-4 py-2" id={`reply-composer-${message.id}`}>
Expand Down