mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
Fix sidebar unread indicator placement (#1319)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -1,17 +1,9 @@
|
||||
// biome-ignore format: keep compact to stay within file size limit
|
||||
import {
|
||||
Activity,
|
||||
Bot,
|
||||
FolderGit2,
|
||||
Inbox,
|
||||
MessageCirclePlus,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { MessageCirclePlus } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { AnimatePresence } from "motion/react";
|
||||
import { FeatureGate } from "@/shared/features";
|
||||
import { SidebarDndContext } from "@/features/sidebar/ui/SidebarDnd";
|
||||
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
|
||||
|
||||
import type { Workspace } from "@/features/workspaces/types";
|
||||
import { AddWorkspaceDialog } from "@/features/workspaces/ui/AddWorkspaceDialog";
|
||||
@@ -31,6 +23,7 @@ import {
|
||||
RenameSectionDialog,
|
||||
useLeaveChannelDialog,
|
||||
} from "@/features/sidebar/ui/ChannelSectionDialogs";
|
||||
import { AppSidebarPinnedHeader } from "@/features/sidebar/ui/AppSidebarPinnedHeader";
|
||||
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
|
||||
import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
|
||||
import {
|
||||
@@ -66,10 +59,7 @@ import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
useSidebar,
|
||||
@@ -534,201 +524,73 @@ export function AppSidebar({
|
||||
className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
|
||||
data-testid="app-sidebar-scroll-anchor"
|
||||
>
|
||||
{unreadAboveCount > 0 ? (
|
||||
<MoreUnreadButton
|
||||
count={unreadAboveCount}
|
||||
onClick={scrollToNextAbove}
|
||||
position="top"
|
||||
testId="sidebar-more-unread-above"
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className="shrink-0 px-2 pt-2.5"
|
||||
data-testid="sidebar-pinned-header"
|
||||
>
|
||||
<TopbarSearch
|
||||
channelLabels={dmChannelLabels}
|
||||
channels={searchChannels}
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
onOpenChannel={onSelectChannel}
|
||||
onOpenResult={onOpenSearchResult}
|
||||
onOpenUser={(user) => onOpenDm({ pubkeys: [user.pubkey] })}
|
||||
onCreateAgent={onCreateAgent}
|
||||
onCreateChannel={handleOpenCreateChannel}
|
||||
suggestionChannels={channels}
|
||||
/>
|
||||
<SidebarHeader
|
||||
className="cursor-default select-none px-0 pb-0 pt-2.5"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={selectedView === "home"}
|
||||
onClick={onSelectHome}
|
||||
tooltip="Inbox"
|
||||
type="button"
|
||||
>
|
||||
<Inbox className="h-4 w-4" />
|
||||
<span>Inbox</span>
|
||||
</SidebarMenuButton>
|
||||
{homeBadgeCount > 0 ? (
|
||||
<SidebarMenuBadge
|
||||
className="right-2 rounded-full bg-primary/15 px-1.5 text-2xs text-primary peer-data-[active=true]/menu-button:bg-sidebar-active-foreground/20 peer-data-[active=true]/menu-button:text-sidebar-active-foreground"
|
||||
data-testid="sidebar-home-count"
|
||||
>
|
||||
{Math.min(homeBadgeCount, 99)}
|
||||
</SidebarMenuBadge>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
<FeatureGate feature="pulse">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-pulse-view"
|
||||
isActive={selectedView === "pulse"}
|
||||
onClick={onSelectPulse}
|
||||
tooltip="Pulse"
|
||||
type="button"
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
<span>Pulse</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
<FeatureGate feature="projects">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-projects-view"
|
||||
isActive={selectedView === "projects"}
|
||||
onClick={onSelectProjects}
|
||||
tooltip="Projects"
|
||||
type="button"
|
||||
>
|
||||
<FolderGit2 className="h-4 w-4" />
|
||||
<span>Projects</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-agents-view"
|
||||
isActive={selectedView === "agents"}
|
||||
onClick={onSelectAgents}
|
||||
tooltip="Agents"
|
||||
type="button"
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
<span>Agents</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<FeatureGate feature="workflows">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-workflows-view"
|
||||
isActive={selectedView === "workflows"}
|
||||
onClick={onSelectWorkflows}
|
||||
tooltip="Workflows"
|
||||
type="button"
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
<span>Workflows</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
</div>
|
||||
<AppSidebarPinnedHeader
|
||||
channelLabels={dmChannelLabels}
|
||||
currentPubkey={currentPubkey}
|
||||
homeBadgeCount={homeBadgeCount}
|
||||
onCreateAgent={onCreateAgent}
|
||||
onCreateChannel={handleOpenCreateChannel}
|
||||
onOpenDm={onOpenDm}
|
||||
onOpenSearchResult={onOpenSearchResult}
|
||||
onSelectAgents={onSelectAgents}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onSelectHome={onSelectHome}
|
||||
onSelectProjects={onSelectProjects}
|
||||
onSelectPulse={onSelectPulse}
|
||||
onSelectWorkflows={onSelectWorkflows}
|
||||
searchChannels={searchChannels}
|
||||
searchFocusRequest={searchFocusRequest}
|
||||
selectedView={selectedView}
|
||||
suggestionChannels={channels}
|
||||
/>
|
||||
|
||||
<SidebarContent
|
||||
className="buzz-sidebar-scrollbar overscroll-none pt-4"
|
||||
ref={scrollRef}
|
||||
<div
|
||||
className="relative flex min-h-0 flex-1 flex-col"
|
||||
data-testid="sidebar-channel-content"
|
||||
>
|
||||
{isLoading ? (
|
||||
<SidebarLoadingContent shape={sidebarLoadingShape} />
|
||||
{unreadAboveCount > 0 ? (
|
||||
<MoreUnreadButton
|
||||
count={unreadAboveCount}
|
||||
onClick={scrollToNextAbove}
|
||||
position="top"
|
||||
testId="sidebar-more-unread-above"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{starredChannels.length > 0 ? (
|
||||
<ChannelGroupSection
|
||||
createAriaLabel="Starred channels"
|
||||
hasUnread={starredChannels.some((c) =>
|
||||
unreadChannelIds.has(c.id),
|
||||
)}
|
||||
isCollapsed={collapsedGroups.starred}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={starredChannels}
|
||||
listTestId="starred-list"
|
||||
onMarkAllRead={() => {
|
||||
for (const channel of starredChannels) {
|
||||
onMarkChannelRead(channel.id, channel.lastMessageAt);
|
||||
}
|
||||
}}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("starred")}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Starred"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
) : null}
|
||||
<SidebarDndContext
|
||||
channels={channels}
|
||||
sections={channelSections}
|
||||
sectionIds={sectionIds}
|
||||
onAssignChannel={assignChannel}
|
||||
onUnassignChannel={unassignChannel}
|
||||
onReorderSections={reorderSections}
|
||||
>
|
||||
{channelSections.map((section, idx) => (
|
||||
<CustomChannelSection
|
||||
key={section.id}
|
||||
section={section}
|
||||
channels={sectionBuckets.bySection[section.id] ?? []}
|
||||
hasUnread={
|
||||
sectionBuckets.bySection[section.id]?.some((c) =>
|
||||
unreadChannelIds.has(c.id),
|
||||
) ?? false
|
||||
}
|
||||
isCollapsed={collapsedSections[section.id] ?? false}
|
||||
<SidebarContent
|
||||
className="buzz-sidebar-scrollbar overscroll-none pt-4"
|
||||
ref={scrollRef}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SidebarLoadingContent shape={sidebarLoadingShape} />
|
||||
) : null}
|
||||
|
||||
{!isLoading ? (
|
||||
<>
|
||||
{starredChannels.length > 0 ? (
|
||||
<ChannelGroupSection
|
||||
createAriaLabel="Starred channels"
|
||||
hasUnread={starredChannels.some((c) =>
|
||||
unreadChannelIds.has(c.id),
|
||||
)}
|
||||
isCollapsed={collapsedGroups.starred}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
selectedChannelId={selectedChannelId}
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
sections={channelSections}
|
||||
assignments={channelAssignments}
|
||||
isFirst={idx === 0}
|
||||
isLast={idx === channelSections.length - 1}
|
||||
onToggleCollapsed={() => toggleCollapsedSection(section.id)}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onMarkSectionRead={() => {
|
||||
for (const channel of sectionBuckets.bySection[
|
||||
section.id
|
||||
] ?? []) {
|
||||
items={starredChannels}
|
||||
listTestId="starred-list"
|
||||
onMarkAllRead={() => {
|
||||
for (const channel of starredChannels) {
|
||||
onMarkChannelRead(channel.id, channel.lastMessageAt);
|
||||
}
|
||||
}}
|
||||
onAssignChannel={assignChannel}
|
||||
onUnassignChannel={unassignChannel}
|
||||
onCreateSectionForChannel={handleCreateSectionForChannel}
|
||||
onRenameSection={() => setRenameSectionTarget(section)}
|
||||
onDeleteSection={() => setDeleteSectionTarget(section)}
|
||||
onMoveSectionUp={() => moveSectionUp(section.id)}
|
||||
onMoveSectionDown={() => moveSectionDown(section.id)}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("starred")}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Starred"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
@@ -737,115 +599,175 @@ export function AppSidebar({
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
))}
|
||||
<ChannelGroupSection
|
||||
browseAriaLabel="Browse channels"
|
||||
createAriaLabel="Create a channel"
|
||||
draggable
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.channels}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={sectionBuckets.unassigned}
|
||||
listTestId="stream-list"
|
||||
onBrowseClick={onBrowseChannels}
|
||||
onCreateClick={() => openCreateDialog("stream")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("channels")}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Channels"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
) : null}
|
||||
<SidebarDndContext
|
||||
channels={channels}
|
||||
sections={channelSections}
|
||||
assignments={channelAssignments}
|
||||
sectionIds={sectionIds}
|
||||
onAssignChannel={assignChannel}
|
||||
onUnassignChannel={unassignChannel}
|
||||
onCreateSectionForChannel={handleCreateSectionForChannel}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
</SidebarDndContext>
|
||||
<FeatureGate feature="forum">
|
||||
<ChannelGroupSection
|
||||
createAriaLabel="Create a forum"
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.forums}
|
||||
onReorderSections={reorderSections}
|
||||
>
|
||||
{channelSections.map((section, idx) => (
|
||||
<CustomChannelSection
|
||||
key={section.id}
|
||||
section={section}
|
||||
channels={sectionBuckets.bySection[section.id] ?? []}
|
||||
hasUnread={
|
||||
sectionBuckets.bySection[section.id]?.some((c) =>
|
||||
unreadChannelIds.has(c.id),
|
||||
) ?? false
|
||||
}
|
||||
isCollapsed={collapsedSections[section.id] ?? false}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
selectedChannelId={selectedChannelId}
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
sections={channelSections}
|
||||
assignments={channelAssignments}
|
||||
isFirst={idx === 0}
|
||||
isLast={idx === channelSections.length - 1}
|
||||
onToggleCollapsed={() =>
|
||||
toggleCollapsedSection(section.id)
|
||||
}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onMarkSectionRead={() => {
|
||||
for (const channel of sectionBuckets.bySection[
|
||||
section.id
|
||||
] ?? []) {
|
||||
onMarkChannelRead(channel.id, channel.lastMessageAt);
|
||||
}
|
||||
}}
|
||||
onAssignChannel={assignChannel}
|
||||
onUnassignChannel={unassignChannel}
|
||||
onCreateSectionForChannel={handleCreateSectionForChannel}
|
||||
onRenameSection={() => setRenameSectionTarget(section)}
|
||||
onDeleteSection={() => setDeleteSectionTarget(section)}
|
||||
onMoveSectionUp={() => moveSectionUp(section.id)}
|
||||
onMoveSectionDown={() => moveSectionDown(section.id)}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
))}
|
||||
<ChannelGroupSection
|
||||
browseAriaLabel="Browse channels"
|
||||
createAriaLabel="Create a channel"
|
||||
draggable
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.channels}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={sectionBuckets.unassigned}
|
||||
listTestId="stream-list"
|
||||
onBrowseClick={onBrowseChannels}
|
||||
onCreateClick={() => openCreateDialog("stream")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("channels")}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Channels"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
sections={channelSections}
|
||||
assignments={channelAssignments}
|
||||
onAssignChannel={assignChannel}
|
||||
onUnassignChannel={unassignChannel}
|
||||
onCreateSectionForChannel={handleCreateSectionForChannel}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
starredChannelIds={starredChannelIds}
|
||||
onStarChannel={onStarChannel}
|
||||
onUnstarChannel={onUnstarChannel}
|
||||
onLeaveChannel={requestLeaveChannel}
|
||||
/>
|
||||
</SidebarDndContext>
|
||||
<FeatureGate feature="forum">
|
||||
<ChannelGroupSection
|
||||
createAriaLabel="Create a forum"
|
||||
hasUnread={unreadChannelIds.size > 0}
|
||||
isCollapsed={collapsedGroups.forums}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={forumChannels}
|
||||
listTestId="forum-list"
|
||||
onCreateClick={() => openCreateDialog("forum")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("forums")}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Forums"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
/>
|
||||
</FeatureGate>
|
||||
<SidebarSection
|
||||
action={
|
||||
<div className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5">
|
||||
<button
|
||||
aria-expanded={isNewDmOpen}
|
||||
aria-label="Compose new message"
|
||||
className={`${SECTION_ICON_BUTTON_CLASS} ${SECTION_ACTION_VISIBILITY_CLASS}`}
|
||||
data-testid="new-dm-trigger"
|
||||
onClick={() => {
|
||||
setIsNewDmOpen(true);
|
||||
}}
|
||||
title="Compose new message"
|
||||
type="button"
|
||||
>
|
||||
<MessageCirclePlus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
dmParticipantsByChannelId={dmParticipantsByChannelId}
|
||||
isCollapsed={collapsedGroups.directMessages}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={forumChannels}
|
||||
listTestId="forum-list"
|
||||
onCreateClick={() => openCreateDialog("forum")}
|
||||
onMarkAllRead={onMarkAllChannelsRead}
|
||||
items={sortedDirectMessages}
|
||||
channelLabels={dmChannelLabels}
|
||||
onHideDm={onHideDm}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("forums")}
|
||||
onToggleCollapsed={() =>
|
||||
toggleCollapsedGroup("directMessages")
|
||||
}
|
||||
presenceByChannelId={dmPresenceByChannelId}
|
||||
selectedChannelId={selectedChannelId}
|
||||
title="Forums"
|
||||
testId="dm-list"
|
||||
title="Direct messages"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
/>
|
||||
</FeatureGate>
|
||||
<SidebarSection
|
||||
action={
|
||||
<div className="absolute right-1 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5">
|
||||
<button
|
||||
aria-expanded={isNewDmOpen}
|
||||
aria-label="Compose new message"
|
||||
className={`${SECTION_ICON_BUTTON_CLASS} ${SECTION_ACTION_VISIBILITY_CLASS}`}
|
||||
data-testid="new-dm-trigger"
|
||||
onClick={() => {
|
||||
setIsNewDmOpen(true);
|
||||
}}
|
||||
title="Compose new message"
|
||||
type="button"
|
||||
>
|
||||
<MessageCirclePlus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
dmParticipantsByChannelId={dmParticipantsByChannelId}
|
||||
isCollapsed={collapsedGroups.directMessages}
|
||||
isActiveChannel={selectedView === "channel"}
|
||||
activeWorkingByChannelId={activeWorkingByChannelId}
|
||||
items={sortedDirectMessages}
|
||||
channelLabels={dmChannelLabels}
|
||||
onHideDm={onHideDm}
|
||||
onMarkChannelRead={onMarkChannelRead}
|
||||
onMarkChannelUnread={onMarkChannelUnread}
|
||||
onSelectChannel={onSelectChannel}
|
||||
onToggleCollapsed={() => toggleCollapsedGroup("directMessages")}
|
||||
presenceByChannelId={dmPresenceByChannelId}
|
||||
selectedChannelId={selectedChannelId}
|
||||
testId="dm-list"
|
||||
title="Direct messages"
|
||||
unreadChannelCounts={unreadChannelCounts}
|
||||
unreadChannelIds={unreadChannelIds}
|
||||
mutedChannelIds={mutedChannelIds}
|
||||
onMuteChannel={onMuteChannel}
|
||||
onUnmuteChannel={onUnmuteChannel}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{errorMessage &&
|
||||
!sidebarRelayConnectionCard.hasRelayUnreachableError ? (
|
||||
<div className="px-3 py-2 text-sm text-destructive">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</SidebarContent>
|
||||
{errorMessage &&
|
||||
!sidebarRelayConnectionCard.hasRelayUnreachableError ? (
|
||||
<div className="px-3 py-2 text-sm text-destructive">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
</SidebarContent>
|
||||
</div>
|
||||
|
||||
<div className="relative z-30 shrink-0">
|
||||
{unreadBelowCount > 0 ? (
|
||||
|
||||
157
desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
Normal file
157
desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { Activity, Bot, FolderGit2, Inbox, Zap } from "lucide-react";
|
||||
|
||||
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
|
||||
import { FeatureGate } from "@/shared/features";
|
||||
import type { Channel, SearchHit } from "@/shared/api/types";
|
||||
import {
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/shared/ui/sidebar";
|
||||
|
||||
type SidebarSelectedView =
|
||||
| "home"
|
||||
| "channel"
|
||||
| "agents"
|
||||
| "workflows"
|
||||
| "pulse"
|
||||
| "projects";
|
||||
|
||||
type AppSidebarPinnedHeaderProps = {
|
||||
channelLabels: Record<string, string>;
|
||||
currentPubkey?: string;
|
||||
homeBadgeCount: number;
|
||||
onCreateAgent: () => void;
|
||||
onCreateChannel: () => void;
|
||||
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
|
||||
onOpenSearchResult: (hit: SearchHit) => void;
|
||||
onSelectAgents: () => void;
|
||||
onSelectChannel: (channelId: string) => void;
|
||||
onSelectHome: () => void;
|
||||
onSelectProjects: () => void;
|
||||
onSelectPulse: () => void;
|
||||
onSelectWorkflows: () => void;
|
||||
searchChannels: Channel[];
|
||||
searchFocusRequest: number;
|
||||
selectedView: SidebarSelectedView;
|
||||
suggestionChannels: Channel[];
|
||||
};
|
||||
|
||||
export function AppSidebarPinnedHeader({
|
||||
channelLabels,
|
||||
currentPubkey,
|
||||
homeBadgeCount,
|
||||
onCreateAgent,
|
||||
onCreateChannel,
|
||||
onOpenDm,
|
||||
onOpenSearchResult,
|
||||
onSelectAgents,
|
||||
onSelectChannel,
|
||||
onSelectHome,
|
||||
onSelectProjects,
|
||||
onSelectPulse,
|
||||
onSelectWorkflows,
|
||||
searchChannels,
|
||||
searchFocusRequest,
|
||||
selectedView,
|
||||
suggestionChannels,
|
||||
}: AppSidebarPinnedHeaderProps) {
|
||||
return (
|
||||
<div className="shrink-0 px-2 pt-2.5" data-testid="sidebar-pinned-header">
|
||||
<TopbarSearch
|
||||
channelLabels={channelLabels}
|
||||
channels={searchChannels}
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
onOpenChannel={onSelectChannel}
|
||||
onOpenResult={onOpenSearchResult}
|
||||
onOpenUser={(user) => onOpenDm({ pubkeys: [user.pubkey] })}
|
||||
onCreateAgent={onCreateAgent}
|
||||
onCreateChannel={onCreateChannel}
|
||||
suggestionChannels={suggestionChannels}
|
||||
/>
|
||||
<SidebarHeader
|
||||
className="cursor-default select-none px-0 pb-0 pt-2.5"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<SidebarMenu className="pb-2">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
isActive={selectedView === "home"}
|
||||
onClick={onSelectHome}
|
||||
tooltip="Inbox"
|
||||
type="button"
|
||||
>
|
||||
<Inbox className="h-4 w-4" />
|
||||
<span>Inbox</span>
|
||||
</SidebarMenuButton>
|
||||
{homeBadgeCount > 0 ? (
|
||||
<SidebarMenuBadge
|
||||
className="right-2 rounded-full bg-primary/15 px-1.5 text-2xs text-primary peer-data-[active=true]/menu-button:bg-sidebar-active-foreground/20 peer-data-[active=true]/menu-button:text-sidebar-active-foreground"
|
||||
data-testid="sidebar-home-count"
|
||||
>
|
||||
{Math.min(homeBadgeCount, 99)}
|
||||
</SidebarMenuBadge>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
<FeatureGate feature="pulse">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-pulse-view"
|
||||
isActive={selectedView === "pulse"}
|
||||
onClick={onSelectPulse}
|
||||
tooltip="Pulse"
|
||||
type="button"
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
<span>Pulse</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
<FeatureGate feature="projects">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-projects-view"
|
||||
isActive={selectedView === "projects"}
|
||||
onClick={onSelectProjects}
|
||||
tooltip="Projects"
|
||||
type="button"
|
||||
>
|
||||
<FolderGit2 className="h-4 w-4" />
|
||||
<span>Projects</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-agents-view"
|
||||
isActive={selectedView === "agents"}
|
||||
onClick={onSelectAgents}
|
||||
tooltip="Agents"
|
||||
type="button"
|
||||
>
|
||||
<Bot className="h-4 w-4" />
|
||||
<span>Agents</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<FeatureGate feature="workflows">
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
data-testid="open-workflows-view"
|
||||
isActive={selectedView === "workflows"}
|
||||
onClick={onSelectWorkflows}
|
||||
tooltip="Workflows"
|
||||
type="button"
|
||||
>
|
||||
<Zap className="h-4 w-4" />
|
||||
<span>Workflows</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</FeatureGate>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,8 +10,9 @@
|
||||
* sidebar row, so `top-0` inside the sidebar starts below the traffic-light
|
||||
* strip.
|
||||
*
|
||||
* This spec injects a synthetic pill into the live sidebar's relative
|
||||
* container and asserts the pill clears the chrome strip.
|
||||
* This spec injects a synthetic pill into the live sidebar channel-content
|
||||
* container (the same relative wrapper that owns the real top unread pill) and
|
||||
* asserts the pill clears the pinned header.
|
||||
*/
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
@@ -39,9 +40,9 @@ async function injectSyntheticPill(
|
||||
await page.evaluate(
|
||||
({ topClass, base, html, testId }) => {
|
||||
const container = document.querySelector(
|
||||
'[data-testid="app-sidebar-scroll-anchor"]',
|
||||
'[data-testid="sidebar-channel-content"]',
|
||||
) as HTMLElement | null;
|
||||
if (!container) throw new Error("sidebar scroll anchor not found");
|
||||
if (!container) throw new Error("sidebar channel content not found");
|
||||
|
||||
// Remove any prior injection so retries start from a clean sidebar.
|
||||
container
|
||||
@@ -68,16 +69,22 @@ test.describe("sidebar MoreUnreadButton top chrome overlap", () => {
|
||||
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
||||
});
|
||||
|
||||
test("top pill clears the in-flow traffic-light strip", async ({ page }) => {
|
||||
test("top pill clears the pinned header", async ({ page }) => {
|
||||
await injectSyntheticPill(page, TOP_CLASS, "synthetic-top");
|
||||
const pill = page.getByTestId("synthetic-top");
|
||||
const pinnedHeader = page.getByTestId("sidebar-pinned-header");
|
||||
await expect(pill).toBeVisible();
|
||||
await expect(pinnedHeader).toBeVisible();
|
||||
|
||||
const box = await pill.boundingBox();
|
||||
expect(box).not.toBeNull();
|
||||
// The pill is anchored at the top of the sidebar row, below the 40px
|
||||
// in-flow chrome strip.
|
||||
expect(box?.y ?? Number.NaN).toBeGreaterThanOrEqual(40);
|
||||
const pillBox = await pill.boundingBox();
|
||||
const pinnedHeaderBox = await pinnedHeader.boundingBox();
|
||||
expect(pillBox).not.toBeNull();
|
||||
expect(pinnedHeaderBox).not.toBeNull();
|
||||
expect(pillBox?.y ?? Number.NaN).toBeGreaterThanOrEqual(
|
||||
pinnedHeaderBox?.y == null || pinnedHeaderBox.height == null
|
||||
? Number.NaN
|
||||
: pinnedHeaderBox.y + pinnedHeaderBox.height,
|
||||
);
|
||||
|
||||
await waitForAnimations(page);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user