Repository navigation
perf: keep the photo lightbox and the date picker off the first load #1205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: production
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| </label> | ||
| <SchedulePicker | ||
| id={`${id}-scheduled-at`} | ||
| value={scheduledAt} | ||
| onChange={setScheduledAt} | ||
| invalid={scheduleError ? true : undefined} | ||
| describedBy={`${id}-schedule-help ${id}-schedule-error`} | ||
| /> | ||
| <Suspense fallback={null}> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Show a placeholder while the schedule picker loads. When a user first opens scheduling, the lazy component suspends and this null fallback leaves the “Submit at” section without a control or loading indication. On a slow connection, the user cannot tell whether the picker is loading. Render a disabled, picker-sized placeholder with a loading label instead. React shows the fallback until the component is ready. (react.dev) As per retrieved learnings, fallback skeletons should match the loaded UI to avoid layout shifts.
|
||
| <SchedulePicker | ||
| id={`${id}-scheduled-at`} | ||
| value={scheduledAt} | ||
| onChange={setScheduledAt} | ||
| invalid={scheduleError ? true : undefined} | ||
| describedBy={`${id}-schedule-help ${id}-schedule-error`} | ||
| /> | ||
| </Suspense> | ||
| <p | ||
| id={`${id}-schedule-help`} | ||
| className="text-xs leading-relaxed text-muted-foreground" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * The pieces of the schedule picker that are only date arithmetic, kept apart from the picker itself so that | ||
| * anything validating a scheduled time does not have to load the calendar to do it. | ||
| */ | ||
|
|
||
| export function pad(value: number): string { | ||
| return String(value).padStart(2, '0'); | ||
| } | ||
|
|
||
| /** | ||
| * Writes a date the way a `datetime-local` input does, in the browser's timezone. | ||
| */ | ||
| export function localDateTime(date: Date): string { | ||
| return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; | ||
| } | ||
|
|
||
| /** | ||
| * A local time skipped by a daylight saving change, like 02:30 on the night clocks go forward, comes back shifted. | ||
| */ | ||
| export function isExistingLocalTime(value: string): boolean { | ||
| return localDateTime(new Date(value)) === value; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: dev-null-sh/dev-null.sh
Length of output: 8124
Mount the lazy viewer after the first open, and keep it mounted after that.
React.lazystarts its loader on the first render of the lazy component.MessageImagesrendersImageViewerfor every persisted image message, including whileviewedIndexisnull, so the viewer chunk loads before any image is opened.Do not unmount the viewer when
viewedIndexreturns tonull.ImageViewerpassesopen={isOpen}toLightbox, so closing is driven by theopenprop on a mounted component. Unmounting on close also resetscaptionedImageIdand removes the closing transition, and the repository does not show how the lightbox library handles that transition. Gating on the first open avoids both changes. After the first open, the viewer stays mounted, as it does today.