Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,7 @@ preview is removed entirely (including behind transparency) only after the lazy
original decodes; failure retains the preview. Missing/invalid hashes or canvas
failures keep the existing background. Syntax validation bounds hashes to 166
base83 characters / 9×9 components; folding does no pixel work. Preview work is
per-mounted-image and uncached, visibility-gated even in nonvirtualized threads.
per-mounted-image and uncached, visibility-gated even for rows mounted offscreen.
Without IntersectionObserver, only the ordinary placeholder/original is used.
This favors bounded visible work over instant offscreen previews on scrolling.
`tests/browser/image-scroll.spec.mjs` covers delayed/failed loads, actual remounts,
Expand Down
6 changes: 3 additions & 3 deletions patches/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Virtua 0.51.0 scroll correction boundaries

The application imports the React ESM entry (`virtua` → `lib/index.js`) from
`src/features/messages/ChannelTimeline.tsx`. Only that entry's element scroller
and store are patched; CommonJS, window scrolling, and other-framework exports
are untouched.
`src/features/messages/ChannelTimeline.tsx` and `ThreadPanel.tsx`. Only that
entry's element scroller and store are patched; CommonJS, window scrolling, and
other-framework exports are untouched.
Keep the dependency pinned to 0.51.0 and review the patch plus version-coupled
installed-bundle tests before upgrading or adding a different import.

Expand Down
1 change: 1 addition & 0 deletions src/bundled/inbox/DraftsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
// @ts-expect-error Node host codec, with disposable test identities only.
import { decodeReadState, signReadState } from "../../../dev/read-state.mjs";

vi.mock("virtua", () => import("../../features/messages/virtua.testing"));
vi.mock("./ChannelPreview", { spy: true });
vi.mock("../../features/messages/ThreadPanel", { spy: true });

Expand Down
2 changes: 2 additions & 0 deletions src/bundled/inbox/InboxPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import {
// @ts-expect-error Node host codec, with disposable test identities only.
import { decodeReadState, signReadState } from "../../../dev/read-state.mjs";

vi.mock("virtua", () => import("../../features/messages/virtua.testing"));

const owners: ReturnType<typeof createRelaySession>[] = [];
composerDOMFixture();
beforeEach(() => localStorage.clear());
Expand Down
1 change: 1 addition & 0 deletions src/features/conversation/EmbeddedThread.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { RelaySession } from "../relay/session";

// The real ThreadPanel and MessageManagement are composed; only leaf UI with
// its own mounted suites is reduced to the props this owner supplies.
vi.mock("virtua", () => import("../messages/virtua.testing"));
vi.mock("../relay/react", () => {
const profiles = new Map();
return { useRowProfiles: () => profiles };
Expand Down
2 changes: 1 addition & 1 deletion src/features/messages/AttachmentImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function ImagePixels({
const preview = canvas.current;
if (hash && preview && typeof IntersectionObserver !== "undefined") {
// Original requests keep native loading="lazy". Only preview CPU work is
// visibility-gated, including nonvirtualized thread rows. No new scheduler.
// visibility-gated, including rows mounted offscreen. No new scheduler.
observer = new IntersectionObserver((entries) => {
if (
!active ||
Expand Down
2 changes: 2 additions & 0 deletions src/features/messages/MediaReviewViewer.comments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { keypair, message, metadata, roster, signed } from "../relay/testing";
import { MediaReviewViewer } from "./MediaReviewViewer";
import { ThreadPanel } from "./ThreadPanel";

vi.mock("virtua", () => import("./virtua.testing"));

const owners: ReturnType<typeof createRelaySession>[] = [];

afterEach(() => {
Expand Down
2 changes: 2 additions & 0 deletions src/features/messages/MessageEdit.integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import type { RelayEvent } from "../relay/events";
import type { ComposerInputElement } from "./composer-dom";
import { composerDOMFixture } from "./composer-testing";

vi.mock("virtua", () => import("./virtua.testing"));

composerDOMFixture();

beforeEach(() => {
Expand Down
14 changes: 13 additions & 1 deletion src/features/messages/Messages.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,25 @@ html[data-keyboard-navigation] .attachmentImage:focus-visible {
flex: 1;
min-height: 0;
overflow: auto;
/* Virtua owns anchoring. */
overflow-anchor: none;
padding: 0 var(--space-panel-inset);
padding-bottom: var(--space-panel-inset);
/* Older-history corrections hide overflow for a task (scroll-correction.ts). */
/* Virtua and older-history corrections hide overflow for a task
(patches/README.md, scroll-correction.ts). */
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--text) 20%, transparent) transparent;
}
/* Virtua hides a row until it is measured at a new index. Hiding blurs focus
and refuses the focus a reparented exact target takes in the same commit. */
.threadHistory
> [data-thread-rows]
> ol
> li:is(:focus-within, :has([data-selected])) {
/* biome-ignore lint/complexity/noImportantStyles: Must win over Virtua's inline style. */
visibility: visible !important;
}
/* Measure loaded rows without painting a scroll position we will replace. */
.threadHistory[data-positioning] > [data-thread-rows] {
opacity: 0;
Expand Down
2 changes: 2 additions & 0 deletions src/features/messages/ThreadPanel.identity.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
} from "../relay/testing";
import { ThreadPanel } from "./ThreadPanel";

vi.mock("virtua", () => import("./virtua.testing"));

const bodyRender = vi.fn();
vi.mock("./MessageMarkdown", () => ({
MessageMarkdown: ({ row }: { row: ChannelMessage }) => {
Expand Down
9 changes: 9 additions & 0 deletions src/features/messages/ThreadPanel.nesting.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vi.mock("./use-reading", () => ({
Reading: () => null,
readingPositioned: () => {},
}));
vi.mock("virtua", () => import("./virtua.testing"));
vi.mock("../relay/react", () => {
const profiles = new Map();
return { useRowProfiles: () => profiles };
Expand Down Expand Up @@ -58,9 +59,17 @@ vi.mock("./MessageComposer", () => ({
}));
beforeEach(() => {
HTMLElement.prototype.scrollIntoView = vi.fn();
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
disconnect() {}
},
);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
delete (HTMLElement.prototype as Partial<HTMLElement>).scrollIntoView;
});
function row(id: string, replyParentId?: string): ChannelMessage {
Expand Down
8 changes: 8 additions & 0 deletions src/features/messages/ThreadPanel.profile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ vi.mock("./use-reading", () => ({
Reading: () => null,
readingPositioned: () => {},
}));
vi.mock("virtua", () => import("./virtua.testing"));
vi.stubGlobal(
"ResizeObserver",
class {
observe() {}
disconnect() {}
},
);

function mount(session: RelaySession, channelId: string, messageId: string) {
const container = document.createElement("div");
Expand Down
1 change: 1 addition & 0 deletions src/features/messages/ThreadPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { ChannelMessage } from "../relay/contracts";

// Real React owns effects, refs and subscriptions. Only independent child UI is
// reduced here; MessageRow/MessageComposer retain their own mounted suites.
vi.mock("virtua", () => import("./virtua.testing"));
vi.mock("../relay/react", () => {
const profiles = new Map();
return { useRowProfiles: () => profiles };
Expand Down
Loading
Loading