diff --git a/resources/js/components/chat/message-parts.tsx b/resources/js/components/chat/message-parts.tsx index 604c0758b..ae8a8c648 100644 --- a/resources/js/components/chat/message-parts.tsx +++ b/resources/js/components/chat/message-parts.tsx @@ -1,6 +1,6 @@ import { Image, Mic, Pin, Reply, SmilePlus } from 'lucide-react'; import type { ReactNode, RefObject } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { lazy, Suspense, useEffect, useRef, useState } from 'react'; import { show as showMessageImage } from '@/actions/App/Http/Controllers/MessageImageController'; import MessageLinkPreviewController from '@/actions/App/Http/Controllers/MessageLinkPreviewController'; import { preloadEmojiGroups } from '@/components/chat/message-reactions'; @@ -9,7 +9,6 @@ import { CommitBody } from '@/components/commit-body'; import { objectUrlFor } from '@/components/commit-image-picker'; import { CommitImages } from '@/components/commit-images'; import type { ShownImage } from '@/components/commit-images'; -import { ImageViewer } from '@/components/image-viewer'; import { LinkPreview } from '@/components/link-preview'; import { bodySegments } from '@/lib/code'; import { isReplySwipe, replyPull, replyThreshold } from '@/lib/reply-swipe'; @@ -22,6 +21,16 @@ import type { MessageReply, } from '@/types'; +/** + * Only needed once an image in a message is opened, which keeps the viewer and its styles out of the chat dock's + * bundle, and so out of the first load of every page the dock is part of. + */ +const ImageViewer = lazy(() => + import('@/components/image-viewer').then((module) => ({ + default: module.ImageViewer, + })), +); + /** * Who wrote a message, as a reply's ghost names them. */ @@ -135,13 +144,15 @@ export function MessageImages({ images }: { images: MessageImage[] | File[] }) { ) : ( <> - setViewedIndex(null)} - downloadUrl={downloadUrl} - /> + + setViewedIndex(null)} + downloadUrl={downloadUrl} + /> + )} diff --git a/resources/js/components/commit-composer.tsx b/resources/js/components/commit-composer.tsx index a8898760d..0e3304746 100644 --- a/resources/js/components/commit-composer.tsx +++ b/resources/js/components/commit-composer.tsx @@ -1,7 +1,7 @@ import { Form, Link, usePage } from '@inertiajs/react'; import { ChartBar, Clock3 } from 'lucide-react'; import type { KeyboardEvent } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { lazy, Suspense, useEffect, useRef, useState } from 'react'; import CommitController from '@/actions/App/Http/Controllers/CommitController'; import ScheduledCommitController from '@/actions/App/Http/Controllers/ScheduledCommitController'; import { ActionTooltip } from '@/components/action-tooltip'; @@ -32,10 +32,6 @@ import { LinkCardPicker } from '@/components/link-card-picker'; import { MentionTextarea } from '@/components/mention-textarea'; import type { PollDraft } from '@/components/poll-editor'; import { emptyPollDraft, PollEditor } from '@/components/poll-editor'; -import { - isExistingLocalTime, - SchedulePicker, -} from '@/components/schedule-picker'; import { StashLabel } from '@/components/stash-label'; import { Button } from '@/components/ui/button'; import { useImageAttachments } from '@/hooks/use-image-attachments'; @@ -57,6 +53,7 @@ import { } from '@/lib/commit'; import type { IssueKind } from '@/lib/issue-commands'; import { hasModKey, useKeyLabels } from '@/lib/keyboard'; +import { isExistingLocalTime } from '@/lib/schedule'; import { characterCount, cn, @@ -66,6 +63,16 @@ import { import { show } from '@/routes/user'; import type { User } from '@/types'; +/** + * Scheduling is behind a toggle, and the calendar it opens is heavy, so it loads when the composer is used + * rather than with the composer itself. Validating a time only needs the arithmetic in "@/lib/schedule". + */ +const SchedulePicker = lazy(() => + import('@/components/schedule-picker').then((module) => ({ + default: module.SchedulePicker, + })), +); + // The ring moves quickly while a post starts and slows toward the limit, yet only closes at the limit. function bodyProgress(count: number): number { return Math.log1p(count) / Math.log1p(maxBodyLength); @@ -370,13 +377,15 @@ function CommitComposerBody({ > Submit at - + + +

+ import('@/components/image-viewer').then((module) => ({ + default: module.ImageViewer, + })), +); + /** * Keeps the files the server would accept, and tells why any others were left out. */ @@ -167,6 +175,10 @@ export function ImagePickerPreviews({ const id = useId(); const [describedIndex, setDescribedIndex] = useState(null); const [viewedIndex, setViewedIndex] = useState(null); + // The viewer is a chunk of its own, and rendering it at mount fetches it as soon as a composer holds an + // image, whether or not a preview is ever opened. It is mounted on the first open and left mounted after + // that, so closing it passes a null index: the lightbox animates out and keeps the state it had. + const [hasOpenedViewer, setHasOpenedViewer] = useState(false); const urls = images.map(objectUrlFor); if (images.length === 0) { @@ -184,6 +196,11 @@ export function ImagePickerPreviews({ const altButtonId = (index: number) => `${id}-alt-${index}`; const previewButtonId = (index: number) => `${id}-preview-${index}`; + const openViewer = (index: number) => { + setHasOpenedViewer(true); + setViewedIndex(index); + }; + const closeViewer = () => { const indexToFocus = activeViewedIndex; setViewedIndex(null); @@ -238,7 +255,7 @@ export function ImagePickerPreviews({