email: charset decoding, inline attachments, UI fixes

- respect charset from Content-Type when decoding body and headers
- detect inline attachments with filenames (not just disposition: attachment)
- fix broken decodeQuotedPrintable reference in parseAttachments
- white background for email iframe (emails designed for light bg)
- sticky header in email list, scrollable message area
- arrow key navigation between emails with scroll-into-view
- /email/:id routing for deep linking to specific emails
- empty folder shows "No emails in this folder" instead of losing header
- fix attachment viewer header overlap with dialog close button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:00:45 +00:00
co-authored by Claude Opus 4.6
parent de1baa1588
commit b963d9b7a7
5 changed files with 119 additions and 25 deletions
@@ -1,4 +1,5 @@
import { useMemo, useCallback } from 'react';
import { useMemo, useCallback, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router';
import type { LayoutNode, PanelComponents } from 'officerdev';
import { WorkspaceView } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
@@ -12,7 +13,23 @@ const PROMPT_PREFIX = `You are an email assistant. The user has a local SQLite e
export const EmailScreen = () => {
const isMobile = useIsMobile();
const { emailId } = useParams<{ emailId?: string }>();
const navigate = useNavigate();
const [selectedId, setSelectedId] = useGlobal<string | null>('EMAIL_SELECTED', null);
// Sync URL param → global state
useEffect(() => {
if (emailId && emailId !== selectedId) setSelectedId(emailId);
}, [emailId]);
// Sync global state → URL
useEffect(() => {
if (selectedId && selectedId !== emailId) {
navigate(`/email/${selectedId}`, { replace: true });
} else if (!selectedId && emailId) {
navigate('/email', { replace: true });
}
}, [selectedId]);
const workspace = useDashboardState<LayoutNode>('screens/email', defaultLayout);
const components: PanelComponents = useMemo(