diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index 70c7bf10a8a..a2231dc9378 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -70,6 +70,7 @@ import { useMembersSidebarActions } from "./useMembersSidebarActions"; import { useMembersSidebarModeration } from "./useMembersSidebarModeration"; const MEMBER_ADD_RESULT_LIMIT = 50; const MEMBER_SEARCH_MIN_QUERY_LENGTH = 2; +const MEMBER_ROW_ESTIMATE_PX = 60; type AddMemberSearchCandidate = UserSearchResult & { isManagedAgent?: boolean; isMember?: boolean; @@ -636,57 +637,62 @@ export function MembersSidebar({ ? managedAgentPairAction(managedAgentRuntime) : undefined; return ( -
diff --git a/desktop/src/shared/styles/globals/utilities.css b/desktop/src/shared/styles/globals/utilities.css
index 0d5e0f453f4..2c858474a47 100644
--- a/desktop/src/shared/styles/globals/utilities.css
+++ b/desktop/src/shared/styles/globals/utilities.css
@@ -37,6 +37,15 @@
contain-intrinsic-size: auto 2rem;
}
+ /*
+ * Member cards rendered eagerly can skip offscreen layout and paint.
+ * Virtualized member rows must not use this utility.
+ */
+ .content-visibility-auto-member-row {
+ content-visibility: auto;
+ contain-intrinsic-size: auto 3.75rem;
+ }
+
.buzz-huddle-tooltip {
background: hsl(
var(--huddle-tooltip-surface, var(--huddle-control-surface, 0 0% 20%))
diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts
index 52d91dea3ed..6849c3e9477 100644
--- a/desktop/tests/e2e/channels.spec.ts
+++ b/desktop/tests/e2e/channels.spec.ts
@@ -4007,7 +4007,40 @@ test("members sidebar virtualizes large channel rosters", async ({ page }) => {
await expect(memberRows.first()).toBeVisible();
expect(await memberRows.count()).toBeLessThan(50);
+ const firstGeneratedRow = memberList.getByTestId(
+ `sidebar-member-${pubkeys[0]}`,
+ );
+ await expect
+ .poll(() =>
+ firstGeneratedRow.evaluate(
+ (row) =>
+ getComputedStyle(row.parentElement as HTMLElement).contentVisibility,
+ ),
+ )
+ .toBe("visible");
+
const virtualizedList = memberList.locator(".overflow-y-auto");
+ await page.evaluate(() => document.fonts.ready);
+
+ const heights: number[] = [];
+ for (const ratio of [0, 0.1, 0.25, 0.5, 0.75, 1]) {
+ heights.push(
+ await virtualizedList.evaluate(async (element, ratio) => {
+ element.scrollTop =
+ (element.scrollHeight - element.clientHeight) * ratio;
+ element.dispatchEvent(new Event("scroll"));
+
+ await new Promise