mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Fix desktop DM and sidebar UI polish (#908)
This commit is contained in:
@@ -2,6 +2,7 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
import { TopbarSearch } from "@/features/search/ui/TopbarSearch";
|
||||
import type { Channel, SearchHit } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { SidebarTrigger, useSidebar } from "@/shared/ui/sidebar";
|
||||
|
||||
@@ -30,6 +31,43 @@ function GlobalTopDivider() {
|
||||
);
|
||||
}
|
||||
|
||||
function CenterColumnTopbarSearch({
|
||||
channels,
|
||||
currentPubkey,
|
||||
onOpenChannel,
|
||||
onOpenResult,
|
||||
searchFocusRequest,
|
||||
}: Pick<
|
||||
AppTopChromeProps,
|
||||
| "channels"
|
||||
| "currentPubkey"
|
||||
| "onOpenChannel"
|
||||
| "onOpenResult"
|
||||
| "searchFocusRequest"
|
||||
>) {
|
||||
const { isResizing, state } = useSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none fixed right-0 top-[7px] z-[45] flex justify-center px-24",
|
||||
!isResizing && "transition-[left] duration-200 ease-linear",
|
||||
)}
|
||||
data-testid="topbar-search-column"
|
||||
style={{ left: state === "expanded" ? "var(--sidebar-width)" : 0 }}
|
||||
>
|
||||
<TopbarSearch
|
||||
channels={channels}
|
||||
className="pointer-events-auto w-[220px] max-w-full md:w-[300px] lg:w-[360px] xl:w-[420px] 2xl:w-[480px]"
|
||||
currentPubkey={currentPubkey}
|
||||
focusRequest={searchFocusRequest}
|
||||
onOpenChannel={onOpenChannel}
|
||||
onOpenResult={onOpenResult}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TOP_CHROME_ICON_BUTTON_CLASS =
|
||||
"size-6 rounded-[4px] p-1 text-muted-foreground/70 hover:bg-border/45 hover:text-foreground";
|
||||
|
||||
@@ -79,13 +117,12 @@ export function AppTopChrome({
|
||||
</Button>
|
||||
</div>
|
||||
{searchHidden ? null : (
|
||||
<TopbarSearch
|
||||
<CenterColumnTopbarSearch
|
||||
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}
|
||||
searchFocusRequest={searchFocusRequest}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -20,7 +20,10 @@ import { Button } from "@/shared/ui/button";
|
||||
import type { useChannelFind } from "@/features/search/useChannelFind";
|
||||
import type { MainTimelineEntry } from "@/features/messages/lib/threadPanel";
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import {
|
||||
resolveUserLabel,
|
||||
type UserProfileLookup,
|
||||
} from "@/features/profile/lib/identity";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
|
||||
type ChannelPaneProps = {
|
||||
@@ -278,6 +281,43 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
}, [botTypingEntries, openThreadHeadId]);
|
||||
const hasThreadComposerBotActivity =
|
||||
threadComposerBotTypingPubkeys.length > 0;
|
||||
const directMessageIntro = React.useMemo(() => {
|
||||
if (activeChannel?.channelType !== "dm") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const participants = activeChannel.participantPubkeys.map(
|
||||
(pubkey, index) => ({
|
||||
fallbackName: activeChannel.participants[index] ?? null,
|
||||
pubkey,
|
||||
}),
|
||||
);
|
||||
const otherParticipants = currentPubkey
|
||||
? participants.filter(
|
||||
(participant) =>
|
||||
participant.pubkey.toLowerCase() !== currentPubkey.toLowerCase(),
|
||||
)
|
||||
: participants;
|
||||
const [participant] =
|
||||
otherParticipants.length > 0 ? otherParticipants : participants;
|
||||
|
||||
if (!participant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = profiles?.[participant.pubkey.toLowerCase()] ?? null;
|
||||
const displayName = resolveUserLabel({
|
||||
currentPubkey,
|
||||
fallbackName: participant.fallbackName,
|
||||
profiles,
|
||||
pubkey: participant.pubkey,
|
||||
});
|
||||
|
||||
return {
|
||||
avatarUrl: profile?.avatarUrl ?? null,
|
||||
displayName,
|
||||
};
|
||||
}, [activeChannel, currentPubkey, profiles]);
|
||||
|
||||
const selectedAgent = React.useMemo(
|
||||
() =>
|
||||
@@ -305,6 +345,7 @@ export const ChannelPane = React.memo(function ChannelPane({
|
||||
) : null}
|
||||
<MessageTimeline
|
||||
channelId={activeChannel?.id}
|
||||
directMessageIntro={directMessageIntro}
|
||||
scrollContainerRef={timelineScrollRef}
|
||||
currentPubkey={currentPubkey}
|
||||
fetchOlder={fetchOlder}
|
||||
|
||||
@@ -133,6 +133,7 @@ export function ChannelScreen({
|
||||
}, [activeChannel?.isMember, activeChannelId, activeReadAt, markChannelRead]);
|
||||
const {
|
||||
activeChannelTitle,
|
||||
activeDmAvatarUrl,
|
||||
activeDmPresenceStatus,
|
||||
activeChannelEphemeralDisplay,
|
||||
} = useActiveChannelHeader(activeChannel, currentPubkey);
|
||||
@@ -164,14 +165,22 @@ export function ChannelScreen({
|
||||
currentPubkey,
|
||||
latestMessageEvent,
|
||||
);
|
||||
const activeDmParticipantPubkeys = React.useMemo(
|
||||
() =>
|
||||
activeChannel?.channelType === "dm"
|
||||
? activeChannel.participantPubkeys
|
||||
: [],
|
||||
[activeChannel],
|
||||
);
|
||||
const messageProfilePubkeys = React.useMemo(
|
||||
() => [
|
||||
...new Set([
|
||||
...messageAuthorPubkeys,
|
||||
...activeDmParticipantPubkeys,
|
||||
...typingEntries.map((entry) => entry.pubkey),
|
||||
]),
|
||||
],
|
||||
[messageAuthorPubkeys, typingEntries],
|
||||
[activeDmParticipantPubkeys, messageAuthorPubkeys, typingEntries],
|
||||
);
|
||||
const messageProfilesQuery = useUsersBatchQuery(messageProfilePubkeys, {
|
||||
enabled: messageProfilePubkeys.length > 0,
|
||||
@@ -473,6 +482,7 @@ export function ChannelScreen({
|
||||
activeChannelTitle={activeChannelTitle}
|
||||
actionsRightInset={headerActionsRightInset}
|
||||
actionsVariant={shouldCompactHeaderActions ? "compact" : "inline"}
|
||||
activeDmAvatarUrl={activeDmAvatarUrl}
|
||||
activeDmPresenceStatus={activeDmPresenceStatus}
|
||||
currentPubkey={currentPubkey}
|
||||
isJoining={joinChannelMutation.isPending}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { EphemeralChannelDisplay } from "@/features/channels/lib/ephemeralC
|
||||
import { getChannelDescription } from "@/features/channels/lib/channelDescription";
|
||||
import { ChannelHeaderStatusBadge } from "@/features/channels/ui/ChannelHeaderStatusBadge";
|
||||
import { ChannelMembersBar } from "@/features/channels/ui/ChannelMembersBar";
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import type { Channel, PresenceStatus } from "@/shared/api/types";
|
||||
|
||||
@@ -14,6 +15,7 @@ type ChannelScreenHeaderProps = {
|
||||
activeChannelTitle: string;
|
||||
actionsRightInset?: string;
|
||||
actionsVariant?: "inline" | "compact";
|
||||
activeDmAvatarUrl: string | null;
|
||||
activeDmPresenceStatus: PresenceStatus | null;
|
||||
currentPubkey?: string;
|
||||
isJoining?: boolean;
|
||||
@@ -29,6 +31,7 @@ export function ChannelScreenHeader({
|
||||
activeChannelTitle,
|
||||
actionsRightInset,
|
||||
actionsVariant = "inline",
|
||||
activeDmAvatarUrl,
|
||||
activeDmPresenceStatus,
|
||||
currentPubkey,
|
||||
isJoining = false,
|
||||
@@ -78,6 +81,17 @@ export function ChannelScreenHeader({
|
||||
actionsRightInset={actionsRightInset}
|
||||
channelType={activeChannel?.channelType}
|
||||
description={getChannelDescription(activeChannel)}
|
||||
leadingContent={
|
||||
activeChannel?.channelType === "dm" ? (
|
||||
<ProfileAvatar
|
||||
avatarUrl={activeDmAvatarUrl}
|
||||
className="h-6 w-6 rounded-md text-[10px]"
|
||||
iconClassName="h-3.5 w-3.5"
|
||||
label={activeChannelTitle}
|
||||
testId="chat-header-dm-avatar"
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
statusBadge={
|
||||
<ChannelHeaderStatusBadge
|
||||
channelType={activeChannel?.channelType}
|
||||
|
||||
@@ -33,6 +33,12 @@ export function useActiveChannelHeader(
|
||||
activeDmParticipantPubkeys[0]?.toLowerCase()
|
||||
] ?? null)
|
||||
: null;
|
||||
const activeDmAvatarUrl =
|
||||
activeDmParticipantPubkeys.length > 0
|
||||
? (activeDmProfilesQuery.data?.profiles?.[
|
||||
activeDmParticipantPubkeys[0]?.toLowerCase()
|
||||
]?.avatarUrl ?? null)
|
||||
: null;
|
||||
|
||||
return {
|
||||
activeChannelTitle: activeChannel
|
||||
@@ -42,6 +48,7 @@ export function useActiveChannelHeader(
|
||||
activeDmProfilesQuery.data?.profiles,
|
||||
)
|
||||
: "Channels",
|
||||
activeDmAvatarUrl,
|
||||
activeDmPresenceStatus,
|
||||
activeChannelEphemeralDisplay,
|
||||
};
|
||||
|
||||
@@ -26,6 +26,7 @@ type ChatHeaderProps = {
|
||||
description?: string;
|
||||
channelType?: ChannelType;
|
||||
visibility?: ChannelVisibility;
|
||||
leadingContent?: React.ReactNode;
|
||||
mode?: "home" | "channel" | "agents" | "workflows" | "pulse" | "projects";
|
||||
overlaysContent?: boolean;
|
||||
statusBadge?: React.ReactNode;
|
||||
@@ -88,6 +89,7 @@ export function ChatHeader({
|
||||
description,
|
||||
channelType,
|
||||
visibility,
|
||||
leadingContent,
|
||||
mode = "channel",
|
||||
overlaysContent = false,
|
||||
statusBadge,
|
||||
@@ -103,8 +105,10 @@ export function ChatHeader({
|
||||
const header = (
|
||||
<header
|
||||
className={cn(
|
||||
"pointer-events-auto relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pl-[24px] sm:pr-[12px]",
|
||||
density === "compact" ? "h-[32px] py-0" : "min-h-[44px] py-[6px]",
|
||||
"pointer-events-auto relative z-30 flex min-w-0 shrink-0 cursor-default select-none items-center gap-[10px] bg-transparent pl-[16px] pr-[8px] transition-[margin,padding] duration-200 ease-linear sm:pr-[12px]",
|
||||
density === "compact"
|
||||
? "h-[32px] py-0"
|
||||
: "min-h-[44px] py-[6px] sm:pl-[24px]",
|
||||
overlaysContent && !belowSystemChrome && "-mb-[44px]",
|
||||
)}
|
||||
data-testid="chat-header"
|
||||
@@ -112,11 +116,13 @@ export function ChatHeader({
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-[4px]">
|
||||
<ChannelIcon
|
||||
channelType={channelType}
|
||||
mode={mode}
|
||||
visibility={visibility}
|
||||
/>
|
||||
{leadingContent ?? (
|
||||
<ChannelIcon
|
||||
channelType={channelType}
|
||||
mode={mode}
|
||||
visibility={visibility}
|
||||
/>
|
||||
)}
|
||||
<h1
|
||||
className="min-w-0 translate-y-px truncate text-base font-semibold leading-6 tracking-tight"
|
||||
data-testid="chat-title"
|
||||
|
||||
@@ -141,15 +141,15 @@ export function CustomEmojiSettingsCard() {
|
||||
}}
|
||||
>
|
||||
<SettingsOptionGroup>
|
||||
<div className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3 text-sm">
|
||||
<div className="min-w-0 flex-[1_1_22rem]">
|
||||
<h4 className="text-sm font-medium">Upload an image</h4>
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
Square images work best. GIF, PNG, JPEG, and WebP files are
|
||||
supported.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex min-w-0 flex-[1_1_16rem] items-center gap-3">
|
||||
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-md border bg-background">
|
||||
{pendingUpload ? (
|
||||
<img
|
||||
@@ -163,9 +163,11 @@ export function CustomEmojiSettingsCard() {
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 space-y-2">
|
||||
<p className="max-w-48 truncate text-sm font-normal text-muted-foreground">
|
||||
{pendingUpload?.filename ?? "No image selected"}
|
||||
</p>
|
||||
{pendingUpload?.filename ? (
|
||||
<p className="max-w-full truncate text-sm font-normal text-muted-foreground">
|
||||
{pendingUpload.filename}
|
||||
</p>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
data-testid="custom-emoji-upload"
|
||||
@@ -183,15 +185,15 @@ export function CustomEmojiSettingsCard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 px-4 py-3 text-sm sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 px-4 py-3 text-sm">
|
||||
<div className="min-w-0 flex-[1_1_22rem]">
|
||||
<h4 className="text-sm font-medium">Give it a name</h4>
|
||||
<p className="text-sm font-normal text-muted-foreground">
|
||||
This is what you’ll type to add this emoji to messages and
|
||||
reactions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full min-w-0 max-w-sm space-y-2">
|
||||
<div className="w-full min-w-0 max-w-sm flex-[1_1_20rem] space-y-2">
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
|
||||
:
|
||||
|
||||
@@ -4,7 +4,7 @@ export function HomeLoadingState() {
|
||||
return (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="grid h-full min-h-0 w-full lg:grid-cols-[320px_minmax(0,1fr)]">
|
||||
<div className="overflow-hidden border-r border-border/70 bg-background/60">
|
||||
<div className="relative overflow-hidden bg-background/60 after:absolute after:bottom-0 after:right-0 after:top-10 after:w-px after:bg-border/70 after:content-['']">
|
||||
<div className="border-b border-border/70 px-4 pb-4 pt-14">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="mt-2 h-4 w-28" />
|
||||
|
||||
@@ -52,7 +52,7 @@ export function InboxListPane({
|
||||
className={cn(
|
||||
"relative flex min-h-0 min-w-0 flex-col overflow-hidden bg-background/60",
|
||||
showRightDivider &&
|
||||
"after:pointer-events-none after:absolute after:inset-y-0 after:right-0 after:z-40 after:w-px after:bg-border/35 after:content-['']",
|
||||
"after:pointer-events-none after:absolute after:bottom-0 after:right-0 after:top-10 after:z-40 after:w-px after:bg-border/35 after:content-['']",
|
||||
)}
|
||||
>
|
||||
<TopChromeBackdrop className="h-[76px]" />
|
||||
|
||||
@@ -3,9 +3,10 @@ import { ArrowDown } from "lucide-react";
|
||||
|
||||
import type { TimelineMessage } from "@/features/messages/types";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Separator } from "@/shared/ui/separator";
|
||||
import { Spinner } from "@/shared/ui/spinner";
|
||||
import { TooltipProvider } from "@/shared/ui/tooltip";
|
||||
import { TimelineSkeleton } from "./TimelineSkeleton";
|
||||
@@ -16,6 +17,10 @@ import { useTimelineScrollManager } from "./useTimelineScrollManager";
|
||||
type MessageTimelineProps = {
|
||||
channelId?: string | null;
|
||||
messages: TimelineMessage[];
|
||||
directMessageIntro?: {
|
||||
avatarUrl: string | null;
|
||||
displayName: string;
|
||||
} | null;
|
||||
isLoading?: boolean;
|
||||
emptyTitle?: string;
|
||||
emptyDescription?: string;
|
||||
@@ -56,6 +61,7 @@ type MessageTimelineProps = {
|
||||
|
||||
export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
channelId,
|
||||
directMessageIntro = null,
|
||||
messages,
|
||||
isLoading = false,
|
||||
emptyTitle = "No messages yet",
|
||||
@@ -141,6 +147,16 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
sentinelRef: topSentinelRef,
|
||||
});
|
||||
|
||||
const hasConversationMessage = messages.some(
|
||||
(message) => message.kind !== KIND_SYSTEM_MESSAGE,
|
||||
);
|
||||
const showDirectMessageIntro =
|
||||
!isLoading && directMessageIntro !== null && !hasConversationMessage;
|
||||
const showGenericEmpty =
|
||||
!isLoading && messages.length === 0 && directMessageIntro === null;
|
||||
const showMessageList =
|
||||
!isLoading && messages.length > 0 && !showDirectMessageIntro;
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
@@ -155,7 +171,10 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
ref={scrollContainerRef}
|
||||
>
|
||||
<div
|
||||
className="flex w-full flex-col gap-2 pt-[92px]"
|
||||
className={cn(
|
||||
"flex w-full flex-col gap-2 pt-[92px]",
|
||||
(showDirectMessageIntro || showGenericEmpty) && "min-h-full",
|
||||
)}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div ref={topSentinelRef} aria-hidden className="h-px" />
|
||||
@@ -166,22 +185,34 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!hasOlderMessages && !isLoading && messages.length > 0 ? (
|
||||
{isLoading ? <TimelineSkeleton /> : null}
|
||||
|
||||
{showDirectMessageIntro ? (
|
||||
<div
|
||||
className="flex items-center gap-3 py-2"
|
||||
data-testid="message-timeline-beginning"
|
||||
className="mb-10 mt-auto flex w-full flex-col items-start text-left sm:-ml-2"
|
||||
data-testid="message-dm-intro"
|
||||
>
|
||||
<Separator className="flex-1" />
|
||||
<p className="text-[11px] font-medium uppercase tracking-[0.08em] text-muted-foreground/75">
|
||||
Beginning of conversation
|
||||
<ProfileAvatar
|
||||
avatarUrl={directMessageIntro.avatarUrl}
|
||||
className="h-[60px] w-[60px] text-base"
|
||||
iconClassName="h-6 w-6"
|
||||
label={directMessageIntro.displayName}
|
||||
testId="message-dm-intro-avatar"
|
||||
/>
|
||||
<p className="mt-4 max-w-full truncate text-xl font-semibold leading-7 tracking-tight text-foreground">
|
||||
{directMessageIntro.displayName}
|
||||
</p>
|
||||
<p className="mt-1 max-w-full truncate whitespace-nowrap text-sm leading-5 text-muted-foreground">
|
||||
This is the beginning of your direct message with{" "}
|
||||
<span className="font-medium text-foreground">
|
||||
{directMessageIntro.displayName}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<Separator className="flex-1" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? <TimelineSkeleton /> : null}
|
||||
|
||||
{!isLoading && messages.length === 0 ? (
|
||||
{showGenericEmpty ? (
|
||||
<div
|
||||
className="rounded-3xl border border-dashed border-border/80 bg-card/70 px-6 py-10 text-center shadow-xs"
|
||||
data-testid="message-empty"
|
||||
@@ -195,7 +226,7 @@ export const MessageTimeline = React.memo(function MessageTimeline({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isLoading && messages.length > 0 ? (
|
||||
{showMessageList ? (
|
||||
<TimelineMessageList
|
||||
channelId={channelId}
|
||||
currentPubkey={currentPubkey}
|
||||
|
||||
@@ -33,11 +33,11 @@ import { SidebarSection } from "@/features/sidebar/ui/SidebarSection";
|
||||
import {
|
||||
ChannelGroupSection,
|
||||
CustomChannelSection,
|
||||
SECTION_ACTION_VISIBILITY_CLASS,
|
||||
} from "@/features/sidebar/ui/CustomChannelSection";
|
||||
import { CreateChannelDialog } from "@/features/sidebar/ui/CreateChannelDialog";
|
||||
import { NewDirectMessageDialog } from "@/features/sidebar/ui/NewDirectMessageDialog";
|
||||
import { SidebarProfileCard } from "@/features/sidebar/ui/SidebarProfileCard";
|
||||
import { SECTION_ACTION_VISIBILITY_CLASS } from "@/features/sidebar/ui/sidebarSectionStyles";
|
||||
import type {
|
||||
Channel,
|
||||
ChannelVisibility,
|
||||
|
||||
@@ -41,6 +41,10 @@ import {
|
||||
DroppableUngroupedBody,
|
||||
SortableSectionShell,
|
||||
} from "@/features/sidebar/ui/SidebarDnd";
|
||||
import {
|
||||
SECTION_ACTION_VISIBILITY_CLASS,
|
||||
SECTION_ICON_BUTTON_CLASS,
|
||||
} from "@/features/sidebar/ui/sidebarSectionStyles";
|
||||
import type { ChannelSection } from "@/features/sidebar/lib/useChannelSections";
|
||||
import type { Channel } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
@@ -49,10 +53,6 @@ import { cn } from "@/shared/lib/cn";
|
||||
// Shared styles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SECTION_ICON_BUTTON_CLASS =
|
||||
"flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
|
||||
export const SECTION_ACTION_VISIBILITY_CLASS =
|
||||
"opacity-0 transition-opacity group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";
|
||||
const SECTION_LABEL_BUTTON_CLASS =
|
||||
"group/section-label flex w-fit max-w-[calc(100%-3rem)] cursor-pointer appearance-none items-center gap-1 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground";
|
||||
const SECTION_LABEL_CHEVRON_CLASS =
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/shared/ui/sidebar";
|
||||
@@ -37,6 +36,10 @@ const SECTION_LABEL_BUTTON_CLASS =
|
||||
"group/section-label flex w-fit max-w-[calc(100%-3rem)] cursor-pointer appearance-none items-center gap-1 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground";
|
||||
const SECTION_LABEL_CHEVRON_CLASS =
|
||||
"h-2.5 w-2.5 shrink-0 opacity-0 text-sidebar-foreground/45 transition-[color,opacity,transform] group-hover/section-label:opacity-100 group-hover/section-label:text-sidebar-foreground group-focus-visible/section-label:opacity-100 group-focus-visible/section-label:text-sidebar-foreground";
|
||||
const SIDEBAR_ROW_ACTION_VISIBILITY_CLASS =
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 md:opacity-0";
|
||||
const SIDEBAR_ROW_ICON_ACTION_CLASS =
|
||||
"flex size-6 items-center justify-center p-1 text-sidebar-foreground/45 transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring peer-data-[active=true]/menu-button:text-sidebar-active-foreground/75 peer-data-[active=true]/menu-button:hover:text-sidebar-active-foreground [&>svg]:size-4 [&>svg]:shrink-0";
|
||||
|
||||
export type SidebarDmParticipant = {
|
||||
avatarUrl: string | null;
|
||||
@@ -168,6 +171,9 @@ export function ChannelMenuButton({
|
||||
return (
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
isActive
|
||||
? "group-hover/menu-item:bg-sidebar-active group-hover/menu-item:text-sidebar-active-foreground"
|
||||
: "group-hover/menu-item:bg-sidebar-accent group-hover/menu-item:text-sidebar-accent-foreground",
|
||||
!isActive &&
|
||||
hasUnread &&
|
||||
"font-semibold text-sidebar-foreground hover:text-sidebar-foreground",
|
||||
@@ -326,15 +332,23 @@ export function SidebarSection({
|
||||
/>
|
||||
) : null}
|
||||
{channel.channelType === "dm" && onHideDm ? (
|
||||
<SidebarMenuAction
|
||||
<button
|
||||
aria-label="Close direct message"
|
||||
className="right-0 top-1/2 h-8 w-8 -translate-y-1/2 rounded-lg border border-border/40 [&>svg]:size-5 peer-data-[size=default]/menu-button:top-1/2 peer-data-[size=lg]/menu-button:top-1/2 peer-data-[size=sm]/menu-button:top-1/2"
|
||||
className={cn(
|
||||
"absolute right-1 top-1/2 z-10 -translate-y-1/2 after:absolute after:-inset-2 after:md:hidden group-data-[collapsible=icon]:hidden",
|
||||
SIDEBAR_ROW_ICON_ACTION_CLASS,
|
||||
SIDEBAR_ROW_ACTION_VISIBILITY_CLASS,
|
||||
)}
|
||||
data-sidebar="menu-action"
|
||||
data-testid={`hide-dm-${channel.name}`}
|
||||
onClick={() => onHideDm(channel.id)}
|
||||
showOnHover
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onHideDm(channel.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<X />
|
||||
</SidebarMenuAction>
|
||||
</button>
|
||||
) : null}
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export const SECTION_ICON_BUTTON_CLASS =
|
||||
"flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
|
||||
|
||||
export const SECTION_ACTION_VISIBILITY_CLASS =
|
||||
"opacity-0 transition-opacity group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";
|
||||
Reference in New Issue
Block a user