Skip to content

Commit e572dbb

Browse files
authored
improved error handling during attachment downloads. (#1751)
# Improved Attachment Handling and UI Refinements ## Description This PR improves email attachment handling by implementing on-demand attachment fetching and enhancing error handling. It also includes several UI refinements to the email composer and navigation components. Key changes: - Added a dedicated hook and API endpoint for fetching message attachments - Improved error handling with user-facing toast notifications for attachment operations - Updated the email composer to hide subject input when replying to messages - Enhanced styling for compose button, categories dropdown, and search bar - Fixed attachment loading issues by fetching data on demand instead of storing large attachment data ## Type of Change - [x] 🐛 Bug fix (non-breaking change which fixes an issue) - [x] ✨ New feature (non-breaking change which adds functionality) - [x] 🎨 UI/UX improvement - [x] ⚡ Performance improvement ## Areas Affected - [x] Email Integration (Gmail, IMAP, etc.) - [x] User Interface/Experience - [x] Data Storage/Management - [x] API Endpoints ## Testing Done - [x] Manual testing performed ## Security Considerations - [x] No sensitive data is exposed - [x] Input validation is implemented ## Checklist - [x] I have performed a self-review of my code - [x] My changes generate no new warnings ## Additional Notes The attachment handling improvements should resolve issues where large attachments were causing performance problems or failing to load properly. The UI refinements provide a more consistent experience across the application.
1 parent d9db7a4 commit e572dbb

File tree

19 files changed

+1107
-927
lines changed

19 files changed

+1107
-927
lines changed

apps/mail/components/create/email-composer.tsx

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export function EmailComposer({
149149
const [imageQuality, setImageQuality] = useState<ImageQuality>(
150150
settings?.settings?.imageCompression || 'medium',
151151
);
152+
const [activeReplyId] = useQueryState('activeReplyId');
152153
const [toggleToolbar, setToggleToolbar] = useState(false);
153154
const processAndSetAttachments = async (
154155
filesToProcess: File[],
@@ -1224,33 +1225,35 @@ export function EmailComposer({
12241225
</div>
12251226

12261227
{/* Subject */}
1227-
<div className="flex items-center gap-2 border-b p-3">
1228-
<p className="text-sm font-medium text-[#8C8C8C]">Subject:</p>
1229-
<input
1230-
className="h-4 w-full bg-transparent text-sm font-normal leading-normal text-black placeholder:text-[#797979] focus:outline-none dark:text-white/90"
1231-
placeholder="Re: Design review feedback"
1232-
value={subjectInput}
1233-
onChange={(e) => {
1234-
const value = replaceEmojiShortcodes(e.target.value);
1235-
setValue('subject', value);
1236-
setHasUnsavedChanges(true);
1237-
}}
1238-
/>
1239-
<button
1240-
onClick={handleGenerateSubject}
1241-
disabled={isLoading || isGeneratingSubject || messageLength < 1}
1242-
>
1243-
<div className="flex items-center justify-center gap-2.5 pl-0.5">
1244-
<div className="flex h-5 items-center justify-center gap-1 rounded-sm">
1245-
{isGeneratingSubject ? (
1246-
<Loader className="h-3.5 w-3.5 animate-spin fill-black dark:fill-white" />
1247-
) : (
1248-
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
1249-
)}
1228+
{!activeReplyId ? (
1229+
<div className="flex items-center gap-2 border-b p-3">
1230+
<p className="text-sm font-medium text-[#8C8C8C]">Subject:</p>
1231+
<input
1232+
className="h-4 w-full bg-transparent text-sm font-normal leading-normal text-black placeholder:text-[#797979] focus:outline-none dark:text-white/90"
1233+
placeholder="Re: Design review feedback"
1234+
value={subjectInput}
1235+
onChange={(e) => {
1236+
const value = replaceEmojiShortcodes(e.target.value);
1237+
setValue('subject', value);
1238+
setHasUnsavedChanges(true);
1239+
}}
1240+
/>
1241+
<button
1242+
onClick={handleGenerateSubject}
1243+
disabled={isLoading || isGeneratingSubject || messageLength < 1}
1244+
>
1245+
<div className="flex items-center justify-center gap-2.5 pl-0.5">
1246+
<div className="flex h-5 items-center justify-center gap-1 rounded-sm">
1247+
{isGeneratingSubject ? (
1248+
<Loader className="h-3.5 w-3.5 animate-spin fill-black dark:fill-white" />
1249+
) : (
1250+
<Sparkles className="h-3.5 w-3.5 fill-black dark:fill-white" />
1251+
)}
1252+
</div>
12501253
</div>
1251-
</div>
1252-
</button>
1253-
</div>
1254+
</button>
1255+
</div>
1256+
) : null}
12541257

12551258
{/* From */}
12561259
{aliases && aliases.length > 1 ? (

apps/mail/components/mail/mail-display.tsx

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
3737
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
3838
import type { Sender, ParsedMessage, Attachment } from '@/types';
3939
import { useActiveConnection } from '@/hooks/use-connections';
40+
import { useAttachments } from '@/hooks/use-attachments';
4041
import { useBrainState } from '../../hooks/use-summary';
4142
import { useTRPC } from '@/providers/query-provider';
4243
import { useThreadLabels } from '@/hooks/use-labels';
@@ -378,9 +379,20 @@ const ActionButton = ({ onClick, icon, text, shortcut }: ActionButtonProps) => {
378379
);
379380
};
380381

381-
const downloadAttachment = (attachment: { body: string; mimeType: string; filename: string }) => {
382+
const downloadAttachment = async (attachment: {
383+
body: string;
384+
mimeType: string;
385+
filename: string;
386+
attachmentId: string;
387+
}) => {
382388
try {
383-
const byteCharacters = atob(attachment.body);
389+
let attachmentData = attachment.body;
390+
391+
if (!attachmentData) {
392+
throw new Error('Attachment data not found');
393+
}
394+
395+
const byteCharacters = atob(attachmentData);
384396
const byteNumbers: number[] = Array(byteCharacters.length);
385397
for (let i = 0; i < byteCharacters.length; i++) {
386398
byteNumbers[i] = byteCharacters.charCodeAt(i);
@@ -398,6 +410,7 @@ const downloadAttachment = (attachment: { body: string; mimeType: string; filena
398410
window.URL.revokeObjectURL(url);
399411
} catch (error) {
400412
console.error('Error downloading attachment:', error);
413+
toast.error('Failed to download attachment');
401414
}
402415
};
403416

@@ -455,9 +468,20 @@ const handleDownloadAllAttachments =
455468
console.log('downloaded', subject, attachments);
456469
};
457470

458-
const openAttachment = (attachment: { body: string; mimeType: string; filename: string }) => {
471+
const openAttachment = async (attachment: {
472+
body: string;
473+
mimeType: string;
474+
filename: string;
475+
attachmentId: string;
476+
}) => {
459477
try {
460-
const byteCharacters = atob(attachment.body);
478+
let attachmentData = attachment.body;
479+
480+
if (!attachmentData) {
481+
throw new Error('Attachment data not found');
482+
}
483+
484+
const byteCharacters = atob(attachmentData);
461485
const byteNumbers: number[] = Array(byteCharacters.length);
462486
for (let i = 0; i < byteCharacters.length; i++) {
463487
byteNumbers[i] = byteCharacters.charCodeAt(i);
@@ -484,6 +508,7 @@ const openAttachment = (attachment: { body: string; mimeType: string; filename:
484508
}
485509
} catch (error) {
486510
console.error('Error opening attachment:', error);
511+
toast.error('Failed to open attachment');
487512
}
488513
};
489514

@@ -638,6 +663,7 @@ const MoreAboutQuery = ({
638663
const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }: Props) => {
639664
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
640665
const { data: threadData } = useThread(emailData.threadId ?? null);
666+
const { data: messageAttachments } = useAttachments(emailData.id);
641667
// const [unsubscribed, setUnsubscribed] = useState(false);
642668
// const [isUnsubscribing, setIsUnsubscribing] = useState(false);
643669
const [preventCollapse, setPreventCollapse] = useState(false);
@@ -660,6 +686,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
660686
const { data: activeConnection } = useActiveConnection();
661687
const [researchSender, setResearchSender] = useState<Sender | null>(null);
662688
const [searchQuery, setSearchQuery] = useState<string | null>(null);
689+
// const trpc = useTRPC();
663690

664691
const isLastEmail = useMemo(
665692
() => emailData.id === threadData?.latest?.id,
@@ -1077,11 +1104,11 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
10771104
10781105
<!-- Attachments -->
10791106
${
1080-
emailData.attachments && emailData.attachments.length > 0
1107+
messageAttachments && messageAttachments.length > 0
10811108
? `
10821109
<div class="attachments-section">
1083-
<h2 class="attachments-title">Attachments (${emailData.attachments.length})</h2>
1084-
${emailData.attachments
1110+
<h2 class="attachments-title">Attachments (${messageAttachments.length})</h2>
1111+
${messageAttachments
10851112
.map(
10861113
(attachment) => `
10871114
<div class="attachment-item">
@@ -1491,11 +1518,11 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
14911518
<Printer className="fill-iconLight dark:fill-iconDark mr-2 h-4 w-4" />
14921519
{m['common.mailDisplay.print']()}
14931520
</DropdownMenuItem>
1494-
{(emailData.attachments?.length ?? 0) > 0 && (
1521+
{(messageAttachments?.length ?? 0) > 0 && (
14951522
<DropdownMenuItem
1496-
disabled={!emailData.attachments?.length}
1523+
disabled={!messageAttachments?.length}
14971524
className={
1498-
!emailData.attachments?.length
1525+
!messageAttachments?.length
14991526
? 'data-[disabled]:pointer-events-auto'
15001527
: ''
15011528
}
@@ -1504,7 +1531,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
15041531
e.preventDefault();
15051532
handleDownloadAllAttachments(
15061533
emailData.subject || 'email',
1507-
emailData.attachments || [],
1534+
messageAttachments || [],
15081535
)();
15091536
}}
15101537
>
@@ -1642,9 +1669,9 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
16421669
/>
16431670
) : null}
16441671
{/* mail attachments */}
1645-
{emailData?.attachments && emailData?.attachments.length > 0 ? (
1672+
{messageAttachments && messageAttachments.length > 0 ? (
16461673
<div className="mb-4 flex flex-wrap items-center gap-2 px-4 pt-4">
1647-
{emailData?.attachments.map((attachment) => (
1674+
{messageAttachments.map((attachment) => (
16481675
<div
16491676
key={`${attachment.filename}-${attachment.attachmentId}`}
16501677
className="flex"
@@ -1667,7 +1694,7 @@ const MailDisplay = ({ emailData, index, totalEmails, demo, threadAttachments }:
16671694
>
16681695
<HardDriveDownload className="text-muted-foreground dark:text-muted-foreground h-4 w-4 fill-[#FAFAFA] dark:fill-[#262626]" />
16691696
</button>
1670-
{index < (emailData?.attachments?.length || 0) - 1 && (
1697+
{index < (messageAttachments?.length || 0) - 1 && (
16711698
<div className="m-auto h-2 w-[1px] bg-[#E0E0E0] dark:bg-[#424242]" />
16721699
)}
16731700
</div>

apps/mail/components/mail/mail.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ export function MailLayout() {
531531
<Button
532532
variant="outline"
533533
className={cn(
534-
'text-muted-foreground relative flex h-[1.875rem] w-full select-none items-center justify-start overflow-hidden rounded-lg border bg-white pl-2 text-left text-sm font-normal shadow-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-none dark:bg-[#141414]',
534+
'text-muted-foreground relative flex h-7 w-full select-none items-center justify-start overflow-hidden rounded-lg border bg-white pl-2 text-left text-sm font-normal shadow-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0 dark:border-none dark:bg-[#141414]',
535535
)}
536536
onClick={() => setIsCommandPaletteOpen('true')}
537537
>
@@ -768,18 +768,15 @@ function CategoryDropdown({ isMultiSelectMode }: CategoryDropdownProps) {
768768
<Button
769769
variant="outline"
770770
className={cn(
771-
'flex h-7 min-w-fit items-center gap-1 rounded-md border-none bg-[#006FFE] px-2 text-white',
771+
'black:text-white text-muted-foreground flex h-7 min-w-fit items-center gap-1 rounded-md border-none px-2',
772772
)}
773773
aria-label="Filter by labels"
774774
aria-expanded={isOpen}
775775
aria-haspopup="menu"
776776
>
777-
<div className="relative overflow-visible">
778-
<Mail className="h-4 w-4 fill-white dark:fill-white" />
779-
</div>
780777
<span className="text-xs font-medium">Categories</span>
781778
<ChevronDown
782-
className={`h-2 w-2 text-white transition-transform duration-200 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
779+
className={`black:text-white text-muted-foreground h-2 w-2 transition-transform duration-200 ${isOpen ? 'rotate-180' : 'rotate-0'}`}
783780
/>
784781
</Button>
785782
</DropdownMenuTrigger>

apps/mail/components/ui/app-sidebar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,13 @@ function ComposeButton() {
176176
<DialogDescription></DialogDescription>
177177

178178
<DialogTrigger asChild>
179-
<button className="relative mb-1.5 inline-flex h-8 w-full items-center justify-center gap-1 self-stretch overflow-hidden rounded-lg border border-gray-200 bg-white text-black dark:border-none dark:bg-[#2C2C2C] dark:text-white">
179+
<button className="relative mb-1.5 inline-flex h-8 w-full items-center justify-center gap-1 self-stretch overflow-hidden rounded-lg border border-gray-200 bg-[#006FFE] text-black dark:border-none dark:text-white">
180180
{state === 'collapsed' && !isMobile ? (
181-
<PencilCompose className="fill-iconLight dark:fill-iconDark mt-0.5 text-black" />
181+
<PencilCompose className="mt-0.5 fill-white text-black" />
182182
) : (
183183
<div className="flex items-center justify-center gap-2.5 pl-0.5 pr-1">
184-
<PencilCompose className="fill-iconLight dark:fill-iconDark" />
185-
<div className="justify-start text-sm leading-none">
184+
<PencilCompose className="fill-white" />
185+
<div className="justify-start text-sm leading-none text-white">
186186
{m['common.commandPalette.commands.newEmail']()}
187187
</div>
188188
</div>

apps/mail/components/ui/nav-main.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from './sidebar';
22
import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible';
3-
import { useActiveConnection, } from '@/hooks/use-connections';
3+
import { useCommandPalette } from '../context/command-palette-context.jsx';
44
import { LabelDialog } from '@/components/labels/label-dialog';
5+
import { useActiveConnection } from '@/hooks/use-connections';
56
import { useMutation, useQuery } from '@tanstack/react-query';
6-
import { Link, useLocation, } from 'react-router';
7+
import { useSearchValue } from '@/hooks/use-search-value.js';
78
import Intercom, { show } from '@intercom/messenger-js-sdk';
89
import { MessageSquare, OldPhone } from '../icons/icons';
910
import { useSidebar } from '../context/sidebar-context';
1011
import { useTRPC } from '@/providers/query-provider';
1112
import { type NavItem } from '@/config/navigation';
1213
import type { Label as LabelType } from '@/types';
14+
import { Link, useLocation } from 'react-router';
1315
import { m } from '../../paraglide/messages.js';
1416
import { Button } from '@/components/ui/button';
1517
import { useLabels } from '@/hooks/use-labels';
@@ -55,9 +57,7 @@ export function NavMain({ items }: NavMainProps) {
5557
const pathname = location.pathname;
5658
const searchParams = new URLSearchParams();
5759
const [category] = useQueryState('category');
58-
59-
60-
60+
6161
const trpc = useTRPC();
6262
const { data: intercomToken } = useQuery(trpc.user.getIntercomToken.queryOptions());
6363

@@ -271,6 +271,7 @@ export function NavMain({ items }: NavMainProps) {
271271
function NavItem(item: NavItemProps & { href: string }) {
272272
const iconRef = useRef<IconRefType>(null);
273273
const { data: stats } = useStats();
274+
const { clearAllFilters } = useCommandPalette();
274275

275276
const { state, setOpenMobile } = useSidebar();
276277

@@ -290,6 +291,7 @@ function NavItem(item: NavItemProps & { href: string }) {
290291
if (item.onClick) {
291292
item.onClick(e as React.MouseEvent<HTMLAnchorElement>);
292293
}
294+
clearAllFilters();
293295
setOpenMobile(false);
294296
};
295297

apps/mail/hooks/use-attachments.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useTRPC } from '@/providers/query-provider';
2+
import { useQuery } from '@tanstack/react-query';
3+
import { useSession } from '@/lib/auth-client';
4+
5+
export const useAttachments = (messageId: string) => {
6+
const { data: session } = useSession();
7+
const trpc = useTRPC();
8+
const AttachmentsQuery = useQuery(
9+
trpc.mail.getMessageAttachments.queryOptions(
10+
{ messageId },
11+
{ enabled: !!session?.user.id && !!messageId, staleTime: 1000 * 60 * 60 },
12+
),
13+
);
14+
15+
return AttachmentsQuery;
16+
};

apps/server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@
3434
"@hono/trpc-server": "^0.3.4",
3535
"@microsoft/microsoft-graph-client": "^3.0.7",
3636
"@microsoft/microsoft-graph-types": "^2.40.0",
37-
"@modelcontextprotocol/sdk": "1.12.0",
37+
"@modelcontextprotocol/sdk": "1.15.1",
3838
"@react-email/components": "^0.0.41",
3939
"@react-email/render": "1.1.0",
4040
"@trpc/client": "catalog:",
4141
"@trpc/server": "catalog:",
4242
"@tsndr/cloudflare-worker-jwt": "3.2.0",
4343
"@upstash/ratelimit": "^2.0.5",
4444
"@upstash/redis": "^1.34.9",
45-
"agents": "0.0.93",
45+
"agents": "0.0.106",
4646
"ai": "^4.3.13",
4747
"autumn-js": "catalog:",
4848
"base64-js": "1.5.1",

0 commit comments

Comments
 (0)