mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
Collapse channel header actions (#901)
This commit is contained in:
@@ -177,6 +177,7 @@ export function AppShell() {
|
||||
const [isChannelManagementOpen, setIsChannelManagementOpen] =
|
||||
React.useState(false);
|
||||
const [searchFocusRequest, setSearchFocusRequest] = React.useState(0);
|
||||
const [topbarSearchHidden, setTopbarSearchHidden] = React.useState(false);
|
||||
const [browseDialogType, setBrowseDialogType] =
|
||||
React.useState<BrowseDialogType>(null);
|
||||
const [isNewDmOpen, setIsNewDmOpen] = React.useState(false);
|
||||
@@ -675,6 +676,7 @@ export function AppShell() {
|
||||
unfollowThread: handleUnfollowThread,
|
||||
isFollowingThread,
|
||||
isNotifiedForThread,
|
||||
setTopbarSearchHidden,
|
||||
threadActivityItems,
|
||||
}}
|
||||
>
|
||||
@@ -693,6 +695,7 @@ export function AppShell() {
|
||||
void goChannel(channelId);
|
||||
}}
|
||||
onOpenResult={handleOpenSearchResult}
|
||||
searchHidden={topbarSearchHidden}
|
||||
searchFocusRequest={searchFocusRequest}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -20,6 +20,7 @@ type AppShellContextValue = {
|
||||
unfollowThread: (rootId: string) => void;
|
||||
isFollowingThread: (rootId: string) => boolean;
|
||||
isNotifiedForThread: (rootId: string) => boolean;
|
||||
setTopbarSearchHidden: (hidden: boolean) => void;
|
||||
threadActivityItems: ThreadActivityItem[];
|
||||
};
|
||||
|
||||
@@ -34,6 +35,7 @@ const AppShellContext = React.createContext<AppShellContextValue>({
|
||||
unfollowThread: () => {},
|
||||
isFollowingThread: () => false,
|
||||
isNotifiedForThread: () => false,
|
||||
setTopbarSearchHidden: () => {},
|
||||
threadActivityItems: [],
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type AppTopChromeProps = {
|
||||
onGoForward: () => void;
|
||||
onOpenChannel: (channelId: string) => void;
|
||||
onOpenResult: (hit: SearchHit) => void;
|
||||
searchHidden?: boolean;
|
||||
searchFocusRequest: number;
|
||||
};
|
||||
|
||||
@@ -41,6 +42,7 @@ export function AppTopChrome({
|
||||
onGoForward,
|
||||
onOpenChannel,
|
||||
onOpenResult,
|
||||
searchHidden = false,
|
||||
searchFocusRequest,
|
||||
}: AppTopChromeProps) {
|
||||
return (
|
||||
@@ -76,14 +78,16 @@ export function AppTopChrome({
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<TopbarSearch
|
||||
channels={channels}
|
||||
className="fixed left-1/2 top-[7px] z-[45] block w-[220px] max-w-[calc(100vw-11rem)] -translate-x-1/2 md:w-[300px] md:max-w-[34vw] lg:w-[360px] lg:max-w-[38vw] xl:w-[420px] xl:max-w-[42vw] 2xl:w-[480px] 2xl:max-w-[44vw]"
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onOpenResult={onOpenResult}
|
||||
/>
|
||||
{searchHidden ? null : (
|
||||
<TopbarSearch
|
||||
channels={channels}
|
||||
className="fixed left-1/2 top-[7px] z-[45] block w-[220px] max-w-[calc(100vw-11rem)] -translate-x-1/2 md:w-[300px] md:max-w-[34vw] lg:w-[360px] lg:max-w-[38vw] xl:w-[420px] xl:max-w-[42vw] 2xl:w-[480px] 2xl:max-w-[44vw]"
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onOpenResult={onOpenResult}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Plus, Settings2, Users } from "lucide-react";
|
||||
import { EllipsisVertical, Plus, Settings2, Users } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useHuddle } from "@/features/huddle";
|
||||
@@ -13,6 +13,12 @@ import { useChannelMembersQuery } from "@/features/channels/hooks";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import { AddChannelBotDialog } from "./AddChannelBotDialog";
|
||||
|
||||
type ChannelMembersBarProps = {
|
||||
@@ -20,6 +26,7 @@ type ChannelMembersBarProps = {
|
||||
currentPubkey?: string;
|
||||
onManageChannel: () => void;
|
||||
onToggleMembers: () => void;
|
||||
variant?: "inline" | "compact";
|
||||
};
|
||||
|
||||
export function ChannelMembersBar({
|
||||
@@ -27,6 +34,7 @@ export function ChannelMembersBar({
|
||||
currentPubkey,
|
||||
onManageChannel,
|
||||
onToggleMembers,
|
||||
variant = "inline",
|
||||
}: ChannelMembersBarProps) {
|
||||
const [isAddBotOpen, setIsAddBotOpen] = React.useState(false);
|
||||
const { startHuddle, isStarting: isStartingHuddle } = useHuddle();
|
||||
@@ -82,8 +90,71 @@ export function ChannelMembersBar({
|
||||
? relayAgentsQuery.error.message
|
||||
: null;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
const huddleIndicator = (
|
||||
<HuddleIndicator
|
||||
channelId={channel.id}
|
||||
onStart={async () => {
|
||||
try {
|
||||
await startHuddle(channel.id, []);
|
||||
// Refetch channels so the new ephemeral channel appears in the sidebar immediately
|
||||
// (default poll interval is 60s — too slow for huddle UX).
|
||||
void queryClient.invalidateQueries({ queryKey: ["channels"] });
|
||||
} catch (e) {
|
||||
console.error("Failed to start huddle:", e);
|
||||
}
|
||||
}}
|
||||
renderMode={variant === "compact" ? "menu-item" : "button"}
|
||||
startDisabled={!canAddAgents || isStartingHuddle}
|
||||
/>
|
||||
);
|
||||
|
||||
const controls =
|
||||
variant === "compact" ? (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
aria-label="Channel actions"
|
||||
className="h-8 w-8 rounded-lg border border-border/40 text-muted-foreground hover:bg-muted/70 hover:text-foreground [&_svg]:size-5"
|
||||
data-testid="channel-actions-menu-trigger"
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<EllipsisVertical className="size-5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48" forceMount>
|
||||
<DropdownMenuItem
|
||||
data-testid="channel-add-bot-trigger"
|
||||
disabled={!canAddAgents}
|
||||
onSelect={() => {
|
||||
setIsAddBotOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
<span>Add agent</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="channel-members-trigger"
|
||||
onSelect={onToggleMembers}
|
||||
>
|
||||
<Users />
|
||||
<span>Members</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{memberCount}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{huddleIndicator}
|
||||
<DropdownMenuItem
|
||||
data-testid="channel-management-trigger"
|
||||
onSelect={onManageChannel}
|
||||
>
|
||||
<Settings2 />
|
||||
<span>Manage channel</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<Button
|
||||
aria-label="Add agent"
|
||||
@@ -114,21 +185,7 @@ export function ChannelMembersBar({
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<HuddleIndicator
|
||||
className="h-8 w-8"
|
||||
channelId={channel.id}
|
||||
onStart={async () => {
|
||||
try {
|
||||
await startHuddle(channel.id, []);
|
||||
// Refetch channels so the new ephemeral channel appears in the sidebar immediately
|
||||
// (default poll interval is 60s — too slow for huddle UX).
|
||||
void queryClient.invalidateQueries({ queryKey: ["channels"] });
|
||||
} catch (e) {
|
||||
console.error("Failed to start huddle:", e);
|
||||
}
|
||||
}}
|
||||
startDisabled={!canAddAgents || isStartingHuddle}
|
||||
/>
|
||||
{huddleIndicator}
|
||||
|
||||
<Button
|
||||
aria-label="Manage channel"
|
||||
@@ -142,6 +199,11 @@ export function ChannelMembersBar({
|
||||
<Settings2 className="size-5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{controls}
|
||||
|
||||
<AddChannelBotDialog
|
||||
backendProviders={backendProvidersQuery.data ?? []}
|
||||
|
||||
@@ -45,7 +45,7 @@ import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback";
|
||||
import { AgentSessionProvider } from "@/shared/context/AgentSessionContext";
|
||||
import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext";
|
||||
import {
|
||||
useElementWidthBreakpoint,
|
||||
useElementWidth,
|
||||
useIsThreadPanelOverlay,
|
||||
} from "@/shared/hooks/use-mobile";
|
||||
import {
|
||||
@@ -61,6 +61,10 @@ import { useChannelAgentSessions } from "./useChannelAgentSessions";
|
||||
import { useChannelProfilePanel } from "./useChannelProfilePanel";
|
||||
import { useChannelRouteTarget } from "./useChannelRouteTarget";
|
||||
import type { ChannelScreenProps } from "./ChannelScreen.types";
|
||||
|
||||
const HEADER_ACTIONS_COMPACT_BREAKPOINT_PX = 760;
|
||||
const HEADER_ACTIONS_SPLIT_GUTTER_PX = 12;
|
||||
|
||||
export function ChannelScreen({
|
||||
activeChannel,
|
||||
currentIdentity,
|
||||
@@ -80,6 +84,7 @@ export function ChannelScreen({
|
||||
unfollowThread,
|
||||
isFollowingThread,
|
||||
isNotifiedForThread,
|
||||
setTopbarSearchHidden,
|
||||
} = useAppShell();
|
||||
const [profilePanelPubkey, setProfilePanelPubkey] = React.useState<
|
||||
string | null
|
||||
@@ -92,10 +97,8 @@ export function ChannelScreen({
|
||||
} = useThreadPanelWidth();
|
||||
const [isMembersSidebarOpen, setIsMembersSidebarOpen] = React.useState(false);
|
||||
const isThreadPanelOverlay = useIsThreadPanelOverlay();
|
||||
const [channelContentRef, isNarrowPanelViewport] =
|
||||
useElementWidthBreakpoint<HTMLDivElement>(
|
||||
THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX,
|
||||
);
|
||||
const [channelContentRef, channelContentWidthPx] =
|
||||
useElementWidth<HTMLDivElement>();
|
||||
const [openThreadHeadId, setOpenThreadHeadId] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
@@ -434,21 +437,32 @@ export function ChannelScreen({
|
||||
]);
|
||||
|
||||
useLoadMissingAncestors(activeChannel, resolvedMessages);
|
||||
const hasAuxiliaryPanel = Boolean(
|
||||
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
|
||||
);
|
||||
const isNarrowPanelViewport =
|
||||
channelContentWidthPx > 0 &&
|
||||
channelContentWidthPx < THREAD_PANEL_SINGLE_COLUMN_BREAKPOINT_PX;
|
||||
const isSinglePanelView =
|
||||
isNarrowPanelViewport &&
|
||||
activeChannel?.channelType !== "forum" &&
|
||||
Boolean(
|
||||
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
|
||||
);
|
||||
hasAuxiliaryPanel;
|
||||
const hasSplitRightPanel =
|
||||
!isSinglePanelView &&
|
||||
!isThreadPanelOverlay &&
|
||||
Boolean(
|
||||
openThreadHeadMessage || openAgentSessionPubkey || profilePanelPubkey,
|
||||
);
|
||||
const headerActionsRightInset = hasSplitRightPanel
|
||||
!isSinglePanelView && !isThreadPanelOverlay && hasAuxiliaryPanel;
|
||||
const shouldCompactHeaderActions =
|
||||
hasAuxiliaryPanel &&
|
||||
channelContentWidthPx > 0 &&
|
||||
channelContentWidthPx < HEADER_ACTIONS_COMPACT_BREAKPOINT_PX;
|
||||
const splitRightPanelInset = hasSplitRightPanel
|
||||
? `min(${threadPanelWidthPx}px, calc(100% - ${THREAD_PANEL_MIN_WIDTH_PX}px))`
|
||||
: undefined;
|
||||
const headerActionsRightInset = splitRightPanelInset
|
||||
? `calc(${splitRightPanelInset} + ${HEADER_ACTIONS_SPLIT_GUTTER_PX}px)`
|
||||
: undefined;
|
||||
React.useEffect(() => {
|
||||
setTopbarSearchHidden(isSinglePanelView);
|
||||
return () => setTopbarSearchHidden(false);
|
||||
}, [isSinglePanelView, setTopbarSearchHidden]);
|
||||
|
||||
return (
|
||||
<AgentSessionProvider onOpenAgentSession={handleOpenAgentSession}>
|
||||
@@ -458,6 +472,7 @@ export function ChannelScreen({
|
||||
activeChannelEphemeralDisplay={activeChannelEphemeralDisplay}
|
||||
activeChannelTitle={activeChannelTitle}
|
||||
actionsRightInset={headerActionsRightInset}
|
||||
actionsVariant={shouldCompactHeaderActions ? "compact" : "inline"}
|
||||
activeDmPresenceStatus={activeDmPresenceStatus}
|
||||
currentPubkey={currentPubkey}
|
||||
isJoining={joinChannelMutation.isPending}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { LogIn } from "lucide-react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import { ChatHeader } from "@/features/chat/ui/ChatHeader";
|
||||
import type { EphemeralChannelDisplay } from "@/features/channels/lib/ephemeralChannel";
|
||||
import { getChannelDescription } from "@/features/channels/lib/channelDescription";
|
||||
import { ChannelHeaderStatusBadge } from "@/features/channels/ui/ChannelHeaderStatusBadge";
|
||||
import { ChannelMembersBar } from "@/features/channels/ui/ChannelMembersBar";
|
||||
import { UpdateIndicator } from "@/features/settings/UpdateIndicator";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import type { Channel, PresenceStatus } from "@/shared/api/types";
|
||||
|
||||
@@ -15,6 +13,7 @@ type ChannelScreenHeaderProps = {
|
||||
activeChannelEphemeralDisplay: EphemeralChannelDisplay | null;
|
||||
activeChannelTitle: string;
|
||||
actionsRightInset?: string;
|
||||
actionsVariant?: "inline" | "compact";
|
||||
activeDmPresenceStatus: PresenceStatus | null;
|
||||
currentPubkey?: string;
|
||||
isJoining?: boolean;
|
||||
@@ -29,6 +28,7 @@ export function ChannelScreenHeader({
|
||||
activeChannelEphemeralDisplay,
|
||||
activeChannelTitle,
|
||||
actionsRightInset,
|
||||
actionsVariant = "inline",
|
||||
activeDmPresenceStatus,
|
||||
currentPubkey,
|
||||
isJoining = false,
|
||||
@@ -61,22 +61,13 @@ export function ChannelScreenHeader({
|
||||
currentPubkey={currentPubkey}
|
||||
onManageChannel={onManageChannel}
|
||||
onToggleMembers={onToggleMembers}
|
||||
variant={actionsVariant}
|
||||
/>
|
||||
)
|
||||
) : null;
|
||||
|
||||
if (!showHeaderContent) {
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed right-3 top-[9px] z-[45] flex shrink-0 items-center gap-1">
|
||||
<UpdateIndicator />
|
||||
{actions ? <div className="shrink-0">{actions}</div> : null}
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -136,16 +136,19 @@ export function ChatHeader({
|
||||
typeof document === "undefined" ? null : (
|
||||
createPortal(topRightActions, document.body)
|
||||
)
|
||||
) : (
|
||||
) : actionsRightInset ? (
|
||||
<div
|
||||
className="flex shrink-0 items-center gap-1"
|
||||
style={
|
||||
actionsRightInset ? { marginRight: actionsRightInset } : undefined
|
||||
}
|
||||
className="absolute top-1/2 z-10 flex shrink-0 -translate-y-1/2 items-center gap-1"
|
||||
style={{ right: actionsRightInset }}
|
||||
>
|
||||
<UpdateIndicator />
|
||||
{actions ? <div className="shrink-0">{actions}</div> : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<UpdateIndicator />
|
||||
{actions ? <div className="shrink-0">{actions}</div> : null}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { relayClient } from "@/shared/api/relayClient";
|
||||
import type { RelayEvent } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { DropdownMenuItem } from "@/shared/ui/dropdown-menu";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip";
|
||||
import { useHuddle } from "../HuddleContext";
|
||||
import { useHeadphonesGate } from "../lib/useHeadphonesGate";
|
||||
@@ -26,6 +27,7 @@ type ActiveHuddle = {
|
||||
type HuddleIndicatorProps = {
|
||||
channelId: string;
|
||||
className?: string;
|
||||
renderMode?: "button" | "menu-item";
|
||||
/** Called when the user clicks the button and no huddle is active (start). */
|
||||
onStart?: () => void;
|
||||
/** Whether the start action is disabled (e.g., permissions, already starting). */
|
||||
@@ -40,6 +42,7 @@ type HuddleIndicatorProps = {
|
||||
export function HuddleIndicator({
|
||||
channelId,
|
||||
className,
|
||||
renderMode = "button",
|
||||
onStart,
|
||||
startDisabled,
|
||||
}: HuddleIndicatorProps) {
|
||||
@@ -226,6 +229,23 @@ export function HuddleIndicator({
|
||||
// No active huddle — render the start button (if onStart provided).
|
||||
if (!activeHuddle) {
|
||||
if (!onStart) return null;
|
||||
if (renderMode === "menu-item") {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className={className}
|
||||
data-testid="channel-start-huddle-trigger"
|
||||
disabled={startDisabled || isStarting}
|
||||
onSelect={() => headphonesGate.gate(() => onStart())}
|
||||
>
|
||||
<Headphones />
|
||||
<span>Start huddle</span>
|
||||
</DropdownMenuItem>
|
||||
{gateDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@@ -267,6 +287,26 @@ export function HuddleIndicator({
|
||||
}
|
||||
}
|
||||
|
||||
if (renderMode === "menu-item") {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className={className}
|
||||
data-testid="channel-start-huddle-trigger"
|
||||
disabled={isJoining || isStarting}
|
||||
onSelect={() => headphonesGate.gate(() => void doJoin())}
|
||||
>
|
||||
<Headphones />
|
||||
<span>Join huddle</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{participantCount}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
{gateDialog}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
|
||||
@@ -11,11 +11,9 @@ const MESSAGE_BODY_OFFSET_PX = MESSAGE_TEXT_OFFSET_PX + 4;
|
||||
const NESTED_REPLY_OFFSET_PX = 28;
|
||||
|
||||
function ParticipantAvatar({
|
||||
hasNextParticipant,
|
||||
participant,
|
||||
index,
|
||||
}: {
|
||||
hasNextParticipant: boolean;
|
||||
participant: TimelineThreadSummaryParticipant;
|
||||
index: number;
|
||||
}) {
|
||||
@@ -27,7 +25,7 @@ function ParticipantAvatar({
|
||||
>
|
||||
<UserAvatar
|
||||
avatarUrl={participant.avatarUrl}
|
||||
className={`h-8 w-8 text-[10px] ${hasNextParticipant ? "ring-2 ring-background" : ""}`}
|
||||
className="h-7 w-7 text-[10px] ring-2 ring-background"
|
||||
displayName={participant.author}
|
||||
size="sm"
|
||||
/>
|
||||
@@ -100,7 +98,6 @@ export function MessageThreadSummaryRow({
|
||||
<div className="flex shrink-0 items-center">
|
||||
{summary.participants.map((participant, index) => (
|
||||
<ParticipantAvatar
|
||||
hasNextParticipant={index < summary.participants.length - 1}
|
||||
index={index}
|
||||
key={participant.id}
|
||||
participant={participant}
|
||||
|
||||
@@ -486,7 +486,7 @@ test("opens a single-level thread panel with inline expansion", async ({
|
||||
return `${Math.round(rect.width)}x${Math.round(rect.height)}`;
|
||||
}),
|
||||
)
|
||||
.toBe("32x32");
|
||||
.toBe("28x28");
|
||||
|
||||
await page.mouse.move(0, 0);
|
||||
const rootSummaryWidthBeforeHover = await rootSummaryRow.evaluate((row) =>
|
||||
@@ -718,6 +718,76 @@ test("thread panel width uses session storage and reset handle", async ({
|
||||
.toBe(defaultWidthPx);
|
||||
});
|
||||
|
||||
test("narrow thread view collapses channel header actions into a menu", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 980, height: 720 });
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-actions-menu-trigger")).toHaveCount(0);
|
||||
|
||||
const rootMessage = page.locator('[data-message-id="mock-general-alice"]');
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
|
||||
await rootMessage.hover();
|
||||
await page.getByTestId("reply-message-mock-general-alice").click();
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-back")).toHaveCount(0);
|
||||
|
||||
const menuTrigger = page.getByTestId("channel-actions-menu-trigger");
|
||||
await expect(menuTrigger).toBeVisible();
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toBeHidden();
|
||||
await expect(page.getByTestId("channel-members-trigger")).toBeHidden();
|
||||
await expect(page.getByTestId("channel-management-trigger")).toBeHidden();
|
||||
|
||||
const menuBox = await menuTrigger.boundingBox();
|
||||
const threadPanelBox = await threadPanel.boundingBox();
|
||||
if (!menuBox || !threadPanelBox) {
|
||||
throw new Error("Expected header action menu and thread panel bounds");
|
||||
}
|
||||
const menuGapPx = threadPanelBox.x - (menuBox.x + menuBox.width);
|
||||
expect(menuGapPx).toBeGreaterThanOrEqual(10);
|
||||
expect(menuGapPx).toBeLessThanOrEqual(14);
|
||||
|
||||
await menuTrigger.click();
|
||||
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-members-trigger")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-start-huddle-trigger")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-management-trigger")).toBeVisible();
|
||||
});
|
||||
|
||||
test("single-panel thread view hides topbar search and channel actions", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 860, height: 720 });
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await expect(page.getByTestId("open-search")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toBeVisible();
|
||||
|
||||
const rootMessage = page.locator('[data-message-id="mock-general-alice"]');
|
||||
const threadPanel = page.getByTestId("message-thread-panel");
|
||||
|
||||
await rootMessage.hover();
|
||||
await page.getByTestId("reply-message-mock-general-alice").click();
|
||||
await expect(threadPanel).toBeVisible();
|
||||
await expect(threadPanel.getByTestId("message-thread-back")).toBeVisible();
|
||||
await expect(page.getByTestId("open-search")).toHaveCount(0);
|
||||
await expect(page.getByTestId("channel-actions-menu-trigger")).toHaveCount(0);
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toHaveCount(0);
|
||||
|
||||
await threadPanel.getByTestId("message-thread-back").click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
||||
await expect(page.getByTestId("open-search")).toBeVisible();
|
||||
await expect(page.getByTestId("channel-add-bot-trigger")).toBeVisible();
|
||||
});
|
||||
|
||||
test("composer is focused after selecting a channel", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("channel-general").click();
|
||||
|
||||
Reference in New Issue
Block a user