mirror of
https://github.com/block/buzz.git
synced 2026-11-05 00:41:28 +00:00
fix(desktop): virtualize channel member lists (#4991)
## Summary - virtualize the unfiltered channel member roster instead of eagerly mounting every member card - retain the existing member search/add flow and archived-member behavior - cover a 500-member roster, bounded mounted rows, and scrolling to the final member in E2E ## Cause The members sidebar rendered every active member card at once. On large channels this mounted hundreds or thousands of avatars, profile/presence consumers, menus, and DOM rows, blocking the renderer even though fetching the roster itself is fast. ## Testing - `pnpm typecheck` - `pnpm exec biome check src/features/channels/ui/MembersSidebar.tsx tests/e2e/channels.spec.ts` - `pnpm build:e2e` - `pnpm exec playwright test tests/e2e/channels.spec.ts --grep 'members sidebar (virtualizes large channel rosters|can invite relay-authorized agents|can invite and remove managed agents|collapses same-persona managed agents)'` (4 passed) - pre-push: `desktop-check`, full `desktop-test` (4,371 passed), branch-skew Implemented by Carl on Wes's behalf. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
import { formatOwnerLabel } from "@/features/profile/lib/identity";
|
||||
import { rankUserCandidatesBySearch } from "@/features/profile/lib/userCandidateSearch";
|
||||
import { usePresenceQuery } from "@/features/presence/hooks";
|
||||
import { VirtualizedList } from "@/shared/ui/VirtualizedList";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import { changeChannelMemberRole } from "@/shared/api/tauri";
|
||||
import type {
|
||||
@@ -199,7 +200,6 @@ export function MembersSidebar({
|
||||
),
|
||||
[bots, currentPubkey, people],
|
||||
);
|
||||
|
||||
const allMemberPubkeys = React.useMemo(
|
||||
() => rawMembers.map((member) => member.pubkey),
|
||||
[rawMembers],
|
||||
@@ -217,9 +217,7 @@ export function MembersSidebar({
|
||||
if (!normalizedSearchQuery) {
|
||||
return activeMembers;
|
||||
}
|
||||
|
||||
const profiles = memberProfilesQuery.data?.profiles ?? {};
|
||||
|
||||
return activeMembers.filter((member) => {
|
||||
const normalizedPubkey = normalizePubkey(member.pubkey);
|
||||
const profile = profiles[normalizedPubkey] ?? null;
|
||||
@@ -816,11 +814,14 @@ export function MembersSidebar({
|
||||
) : null}
|
||||
</div>
|
||||
) : filteredActiveMembers.length > 0 ? (
|
||||
<div>
|
||||
{filteredActiveMembers.map((member) =>
|
||||
renderMemberCard(member, isBot(member)),
|
||||
)}
|
||||
</div>
|
||||
<VirtualizedList
|
||||
className="h-[calc(100%_-_2.25rem)]"
|
||||
getItemKey={(member) => member.pubkey}
|
||||
items={filteredActiveMembers}
|
||||
renderItem={(member) =>
|
||||
renderMemberCard(member, isBot(member))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="px-4 py-3 text-sm text-muted-foreground">
|
||||
{membersQuery.isLoading
|
||||
|
||||
@@ -3394,6 +3394,40 @@ test("home inbox manage affordance opens management without leaving home", async
|
||||
await expect(page).not.toHaveURL(/#\/channels\//);
|
||||
});
|
||||
|
||||
test("members sidebar virtualizes large channel rosters", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const channelId = await page
|
||||
.getByTestId("channel-random")
|
||||
.getAttribute("data-channel-id");
|
||||
if (!channelId) {
|
||||
throw new Error("Random channel id missing.");
|
||||
}
|
||||
|
||||
const pubkeys = Array.from({ length: 500 }, (_, index) =>
|
||||
(index + 1).toString(16).padStart(64, "0"),
|
||||
);
|
||||
await invokeMockCommand(page, "add_channel_members", {
|
||||
channelId,
|
||||
pubkeys,
|
||||
role: "member",
|
||||
});
|
||||
|
||||
await openMembersSidebar(page, "random");
|
||||
const memberList = page.getByTestId("members-sidebar-people");
|
||||
const memberRows = memberList.locator('[data-testid^="sidebar-member-"]');
|
||||
await expect(memberRows.first()).toBeVisible();
|
||||
expect(await memberRows.count()).toBeLessThan(50);
|
||||
|
||||
const virtualizedList = memberList.locator(".overflow-y-auto");
|
||||
await virtualizedList.evaluate((element) => {
|
||||
element.scrollTop = element.scrollHeight;
|
||||
element.dispatchEvent(new Event("scroll"));
|
||||
});
|
||||
await expect(
|
||||
memberList.getByTestId(`sidebar-member-${pubkeys.at(-1)}`),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("members sidebar can invite relay-authorized agents", async ({ page }) => {
|
||||
await installMockBridge(page, {
|
||||
relayAgents: [
|
||||
|
||||
Reference in New Issue
Block a user