Load the composer, previews, dialogs and other sidebars on demand
The main chunk carried everything the mail view might open: the file preview and its Markdown renderer, the composer and its editor, the contact editor, the filter and share dialogs, and the calendar, contacts and files sidebars. Each is now loaded when first shown. The composer is also fetched when the browser is idle after startup, so the first Compose does not wait on the network. Import the notification helpers statically where they already were: the dynamic imports beside those static ones split nothing.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState, type MouseEvent, type ReactNode } from "react";
|
||||
import { lazy, Suspense, useCallback, useState, type MouseEvent, type ReactNode } from "react";
|
||||
import { Copy, Mail, Pencil, UserPlus } from "lucide-react";
|
||||
import type { EmailAddress } from "@/jmap/types";
|
||||
import { useContacts } from "@/store/contacts";
|
||||
@@ -7,9 +7,11 @@ import { contactFromAddress } from "@/lib/contacts";
|
||||
import { formatAddress } from "@/lib/address";
|
||||
import { MenuItem, MenuSep, Popover, type Anchor } from "@/ui/popover";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { ContactEditor } from "../contacts/ContactEditor";
|
||||
import { t } from "@/lib/i18n";
|
||||
|
||||
// Loaded when first opened: it is not needed to show mail, and it is not small.
|
||||
const ContactEditor = lazy(() => import("../contacts/ContactEditor").then((m) => ({ default: m.ContactEditor })));
|
||||
|
||||
/**
|
||||
* Right-click on anyone named in a message — sender, recipients, Reply-To — to
|
||||
* add them to the address book. The contact editor opens prefilled rather than
|
||||
@@ -65,12 +67,14 @@ export function useAddressMenu() {
|
||||
</Popover>
|
||||
)}
|
||||
{editing && (
|
||||
<Suspense fallback={null}>
|
||||
<ContactEditor
|
||||
card={editing}
|
||||
defaultBookId={defaultBookId}
|
||||
onClose={() => setEditing(null)}
|
||||
onSaved={() => setEditing(null)}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState, type DragEvent, type ReactNode } from "react";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState, type DragEvent, type ReactNode } from "react";
|
||||
import { Link, useLocation } from "wouter";
|
||||
import { AlertOctagon, Archive, ChevronDown, ChevronLeft, Clock, ChevronRight, File, Folder, FolderPlus, Inbox, Mail, MoreVertical, Palette, Send, Star, Tag, Trash2, Plus, Pencil, Eye, EyeOff, CheckCheck, Eraser, Share2, X, FolderInput } from "lucide-react";
|
||||
import { useMail } from "@/store/mail";
|
||||
@@ -11,7 +11,6 @@ import { MenuItem, MenuSep, MenuTitle, Popover, useMenu } from "@/ui/popover";
|
||||
import { CALENDAR_COLORS, useIsMobile, useIsTouch } from "@/ui/misc";
|
||||
import { confirmDialog, promptDialog } from "@/ui/dialog";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { ShareDialog } from "../settings/ShareDialog";
|
||||
import { MailboxPicker } from "./MailboxPicker";
|
||||
import { loadRaw, saveJson } from "@/lib/storage";
|
||||
import { canDropFolder, canMoveFolderTo, folderColor, movable } from "@/lib/mailbox/folderMove";
|
||||
@@ -19,6 +18,9 @@ import { haptic, useTouchRow } from "@/lib/input/touch";
|
||||
import { plural, t } from "@/lib/i18n";
|
||||
import { mailboxDisplayName } from "@/lib/mailbox/mailboxName";
|
||||
|
||||
// Loaded when first opened: it is not needed to show mail, and it is not small.
|
||||
const ShareDialog = lazy(() => import("../settings/ShareDialog").then((m) => ({ default: m.ShareDialog })));
|
||||
|
||||
const ROLE_ICONS: Record<string, ReactNode> = {
|
||||
inbox: <Inbox size={20} />,
|
||||
drafts: <File size={20} />,
|
||||
@@ -273,7 +275,7 @@ export function MailboxTree() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{shareTarget && <ShareDialog kind="Mailbox" id={shareTarget.id} name={shareTarget.name} shareWith={shareTarget.shareWith ?? null} onClose={() => setShareTarget(null)} />}
|
||||
{shareTarget && <Suspense fallback={null}><ShareDialog kind="Mailbox" id={shareTarget.id} name={shareTarget.name} shareWith={shareTarget.shareWith ?? null} onClose={() => setShareTarget(null)} /></Suspense>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment, memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent, type ReactNode } from "react";
|
||||
import { Fragment, lazy, memo, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type DragEvent, type MouseEvent, type ReactNode } from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { Archive, ArrowLeft, CalendarDays, CalendarRange, CalendarPlus, CheckSquare, FolderInput, PanelRight, PanelBottom, PanelTop, Filter, Inbox, Mail, MailOpen, MailPlus, MoreVertical, Paperclip, RefreshCw, Reply, Search, Star, Tag, Trash2, AlertOctagon, Forward, Eraser, ShieldCheck, X } from "lucide-react";
|
||||
@@ -21,9 +21,11 @@ import { startAppointment } from "@/lib/calendar/appointment";
|
||||
import { toast } from "@/ui/toast";
|
||||
import { haptic, usePullToRefresh, useTouchRow, PULL_TRIGGER } from "@/lib/input/touch";
|
||||
import { describeSwipe, type SwipeAction, type SwipeDescriptor, type SwipeIcon } from "@/lib/input/swipe";
|
||||
import { FilterFromMessageDialog } from "./FilterFromMessage";
|
||||
import { plural, t } from "@/lib/i18n";
|
||||
|
||||
// Loaded when first opened: it is not needed to show mail, and it is not small.
|
||||
const FilterFromMessageDialog = lazy(() => import("./FilterFromMessage").then((m) => ({ default: m.FilterFromMessageDialog })));
|
||||
|
||||
/**
|
||||
* The glyph on the strip a swipe reveals. Sized larger than the toolbar's
|
||||
* icons: it is read at arm's length, in motion, out of the corner of an eye.
|
||||
@@ -560,7 +562,7 @@ export function MessageList({ title, list, openThreadId, openMessageId, focusId,
|
||||
<MenuItem icon={<Filter size={16} />} label={t("Filter messages like this…")} onClick={() => { const e = ctxRow ? emails[ctxRow] : undefined; if (e) setFilterFrom(e); }} />
|
||||
{hasCalendar && <MenuItem icon={<CalendarPlus size={16} />} label={t("Create event…")} onClick={() => { const e = ctxRow ? emails[ctxRow] : undefined; if (e) void startAppointment(e, navigate).catch((err: unknown) => toast.error((err as Error).message)); }} />}
|
||||
</Popover>
|
||||
{filterFrom && <FilterFromMessageDialog email={filterFrom} mailboxId={mailboxId} onClose={() => setFilterFrom(null)} />}
|
||||
{filterFrom && <Suspense fallback={null}><FilterFromMessageDialog email={filterFrom} mailboxId={mailboxId} onClose={() => setFilterFrom(null)} /></Suspense>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ChevronDown, ChevronUp, Download, ExternalLink, Forward, MailPlus, MoreVertical, Printer, Reply, ReplyAll, Star, Trash2, Code, FileText, Image as ImageIcon, File as FileIcon, Eye, Calendar, CalendarPlus, UserPlus, ShieldAlert, Mail, Ban, Clock, CheckCheck, Paperclip, FileArchive, FileSpreadsheet, Film, Music, Filter, Share2 } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { FilterFromMessageDialog } from "./FilterFromMessage";
|
||||
import type { Email, EmailAddress, EmailBodyPart, Id } from "@/jmap/types";
|
||||
import { useMail } from "@/store/mail";
|
||||
import { useSettings } from "@/store/settings";
|
||||
@@ -20,7 +19,10 @@ import { formatFullDate, formatListDate, formatSize } from "@/lib/format";
|
||||
import { displayName, domainOf, formatAddress } from "@/lib/address";
|
||||
import { EMAIL_BASE_CSS, TEXT_EMAIL_CSS, hasHtmlAlternative, htmlDeclaresColors, markKeptSurfaces, sanitizeEmailHtml } from "@/lib/text/html";
|
||||
import { openableInTab, previewKind } from "@/lib/preview";
|
||||
import { FilePreviewDialog } from "@/ui/filepreview";
|
||||
// Loaded when first opened: it is not needed to show mail, and it is not small.
|
||||
const FilterFromMessageDialog = lazy(() => import("./FilterFromMessage").then((m) => ({ default: m.FilterFromMessageDialog })));
|
||||
// The preview carries a Markdown renderer, which is most of its weight.
|
||||
const FilePreviewDialog = lazy(() => import("@/ui/filepreview").then((m) => ({ default: m.FilePreviewDialog })));
|
||||
import { findQuoteStart, htmlToText, textToHtml, withoutBidiControls } from "@/lib/text/text";
|
||||
import { canShare, canShareFiles, shareFile, shareText } from "@/lib/share";
|
||||
import { Avatar } from "@/ui/misc";
|
||||
@@ -456,7 +458,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
||||
</>
|
||||
)}
|
||||
{addrMenu.node}
|
||||
{filterOpen && <FilterFromMessageDialog email={e} mailboxId={Object.keys(e.mailboxIds)[0] ?? null} onClose={() => setFilterOpen(false)} />}
|
||||
{filterOpen && <Suspense fallback={null}><FilterFromMessageDialog email={e} mailboxId={Object.keys(e.mailboxIds)[0] ?? null} onClose={() => setFilterOpen(false)} /></Suspense>}
|
||||
<Dialog open={showSource} onClose={() => setShowSource(false)} title={translate("Original message")} size="xl">
|
||||
{source === null ? <div className="center"><span className="spinner" /></div> : <pre className="code notranslate" translate="no" style={{ minHeight: 300, maxHeight: "65vh" }}>{source}</pre>}
|
||||
</Dialog>
|
||||
@@ -915,8 +917,10 @@ function AttachmentList({ attachments, accountId, email }: { attachments: EmailB
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{preview && (
|
||||
<Suspense fallback={null}>
|
||||
<FilePreviewDialog
|
||||
file={preview && preview.blobId ? {
|
||||
file={preview.blobId ? {
|
||||
name: preview.name ?? translate("file"),
|
||||
type: preview.type,
|
||||
size: preview.size,
|
||||
@@ -926,6 +930,8 @@ function AttachmentList({ attachments, accountId, email }: { attachments: EmailB
|
||||
onClose={() => setPreview(null)}
|
||||
caption={<p className="hint" style={{ marginTop: 8 }}>{translate("From: {sender}", { sender: displayName(email.from?.[0]) })}</p>}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user