Skip to content
Closed
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
130 changes: 68 additions & 62 deletions desktop/src/features/sidebar/ui/CommunityRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,71 +368,77 @@ export function CommunityRail({
};

return (
<nav
aria-label="Communities"
className="relative z-20 mb-2 mt-[calc(var(--buzz-top-chrome-height,40px)+1px)] flex w-14 shrink-0 flex-col items-center gap-2 overflow-y-auto bg-sidebar px-2.5 pb-3 pt-1.5"
data-testid="community-rail"
<div
className="relative z-20 flex w-14 shrink-0 flex-col bg-sidebar"
data-testid="community-rail-surface"
>
<DndContext
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
sensors={sensors}
<nav
aria-label="Communities"
className="mb-2 mt-[calc(var(--buzz-top-chrome-height,40px)+1px)] flex min-h-0 w-full flex-1 flex-col items-center gap-2 overflow-y-auto px-2.5 pb-3 pt-1.5"
data-testid="community-rail"
>
<SortableContext
items={communityIds}
strategy={verticalListSortingStrategy}
<DndContext
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
sensors={sensors}
>
{communities.map((community) => (
<SortableCommunityButton
key={community.id}
activeCommunityId={activeCommunityId}
canInvite={
community.id === activeCommunityId && canInviteToActiveCommunity
}
community={community}
iconsByCommunity={iconsByCommunity}
unreadByCommunity={unreadByCommunity}
onInvite={() => onOpenSettings?.("community-members")}
onMarkAllRead={handleMarkAllRead}
onSetEditingCommunity={setEditingCommunity}
onSwitchCommunity={onSwitchCommunity}
/>
))}
</SortableContext>
<DragOverlay>
{draggingCommunity ? (
<CommunityDragOverlay
community={draggingCommunity}
iconUrl={iconsByCommunity[draggingCommunity.id] ?? null}
/>
) : null}
</DragOverlay>
</DndContext>
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label="Add community"
className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70 outline-hidden transition-all hover:rounded-xl hover:bg-primary/80 hover:text-primary-foreground focus:outline-none focus-visible:outline-none"
data-testid="community-rail-add"
onClick={onAddCommunity}
type="button"
<SortableContext
items={communityIds}
strategy={verticalListSortingStrategy}
>
<Plus className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">Add community</TooltipContent>
</Tooltip>
<EditCommunityDialog
canRemove={communities.length > 1}
onOpenChange={(open) => {
if (!open) setEditingCommunity(null);
}}
onRemove={onRemoveCommunity}
onSave={onUpdateCommunity}
open={editingCommunity !== null}
community={editingCommunity}
showIconEditor={editingCommunity?.id === activeCommunityId}
/>
</nav>
{communities.map((community) => (
<SortableCommunityButton
key={community.id}
activeCommunityId={activeCommunityId}
canInvite={
community.id === activeCommunityId &&
canInviteToActiveCommunity
}
community={community}
iconsByCommunity={iconsByCommunity}
unreadByCommunity={unreadByCommunity}
onInvite={() => onOpenSettings?.("community-members")}
onMarkAllRead={handleMarkAllRead}
onSetEditingCommunity={setEditingCommunity}
onSwitchCommunity={onSwitchCommunity}
/>
))}
</SortableContext>
<DragOverlay>
{draggingCommunity ? (
<CommunityDragOverlay
community={draggingCommunity}
iconUrl={iconsByCommunity[draggingCommunity.id] ?? null}
/>
) : null}
</DragOverlay>
</DndContext>
<Tooltip>
<TooltipTrigger asChild>
<button
aria-label="Add community"
className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70 outline-hidden transition-all hover:rounded-xl hover:bg-primary/80 hover:text-primary-foreground focus:outline-none focus-visible:outline-none"
data-testid="community-rail-add"
onClick={onAddCommunity}
type="button"
>
<Plus className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="right">Add community</TooltipContent>
</Tooltip>
<EditCommunityDialog
canRemove={communities.length > 1}
onOpenChange={(open) => {
if (!open) setEditingCommunity(null);
}}
onRemove={onRemoveCommunity}
onSave={onUpdateCommunity}
open={editingCommunity !== null}
community={editingCommunity}
showIconEditor={editingCommunity?.id === activeCommunityId}
/>
</nav>
</div>
);
}
55 changes: 55 additions & 0 deletions desktop/tests/e2e/community-rail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ test.describe("community rail", () => {
// Spoof macOS so the rail applies its traffic-light top inset.
await page.addInitScript(() => {
Object.defineProperty(navigator, "platform", { get: () => "MacIntel" });
window.localStorage.setItem("buzz-theme", "dracula");
});
await installMockBridge(page, undefined, { skipCommunitySeed: true });
await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id);
Expand All @@ -661,6 +662,12 @@ test.describe("community rail", () => {
await expect(firstButton).toBeVisible();
const buttonBox = await firstButton.boundingBox();
const railBox = await page.getByTestId("community-rail").boundingBox();
const railSurfaceBox = await page
.getByTestId("community-rail-surface")
.boundingBox();
const appSurfaceBox = await page
.locator(".buzz-huddle-app-surface")
.boundingBox();
const searchBox = await page.getByTestId("open-search").boundingBox();
const contentBox = await page
.locator("[data-buzz-content-surface]")
Expand All @@ -670,10 +677,58 @@ test.describe("community rail", () => {
expect(railBox).not.toBeNull();
expect(searchBox).not.toBeNull();
expect(contentBox).not.toBeNull();
expect(railSurfaceBox).not.toBeNull();
expect(appSurfaceBox).not.toBeNull();
expect(buttonBox?.y ?? 0).toBeGreaterThanOrEqual(32);
expect(Math.abs((railBox?.y ?? 0) - (contentBox?.y ?? 0))).toBeLessThan(
0.5,
);
expect(
Math.abs((railSurfaceBox?.y ?? 0) - (appSurfaceBox?.y ?? 0)),
).toBeLessThan(0.5);
expect(
Math.abs(
(railSurfaceBox?.y ?? 0) +
(railSurfaceBox?.height ?? 0) -
((appSurfaceBox?.y ?? 0) + (appSurfaceBox?.height ?? 0)),
),
).toBeLessThan(0.5);

const gapPaint = await page.evaluate(() => {
const surface = document.querySelector<HTMLElement>(
'[data-testid="community-rail-surface"]',
);
const rail = document.querySelector<HTMLElement>(
'[data-testid="community-rail"]',
);
if (!surface || !rail) throw new Error("community rail is missing");

const surfaceBox = surface.getBoundingClientRect();
const railBox = rail.getBoundingClientRect();
const x = surfaceBox.left + surfaceBox.width / 2;
const bottomGapElement = document.elementFromPoint(
x,
railBox.bottom + (surfaceBox.bottom - railBox.bottom) / 2,
);
const topGapElement = document.elementFromPoint(
x,
surfaceBox.top + (railBox.top - surfaceBox.top) / 2,
);

return {
backgroundColor: getComputedStyle(surface).backgroundColor,
bottomOwned:
bottomGapElement?.closest(
'[data-testid="community-rail-surface"]',
) === surface,
topOwned:
topGapElement?.closest('[data-testid="community-rail-surface"]') ===
surface,
};
});
expect(gapPaint.backgroundColor).not.toBe("rgba(0, 0, 0, 0)");
expect(gapPaint.topOwned).toBe(true);
expect(gapPaint.bottomOwned).toBe(true);

const leftInset = (buttonBox?.x ?? 0) - (railBox?.x ?? 0);
const rightInset =
Expand Down