Files
message-inbox-web-frontend-sdk/apps/web/components/nav/Sidebar.tsx
T
2026-07-17 21:48:37 +05:30

274 lines
8.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import clsx from "clsx";
import {
useChannels,
useFeatureFlags,
useInbox,
useSdk,
type Channel,
} from "@lynkd/messaging-inbox-sdk";
import { useUi } from "@/lib/ui-state";
import { Avatar, CountBadge } from "@/components/ui/primitives";
import {
Hash,
Lock,
ChevronDown,
ChevronRight,
Plus,
MessageSquarePlus,
Send,
Bookmark,
MessagesSquare,
Inbox as InboxIcon,
Sliders,
Circle,
PanelLeftClose,
} from "lucide-react";
export function Sidebar() {
const { starred, channels, dms } = useChannels();
const { me, brandName, channelById } = useSdk();
const flags = useFeatureFlags();
const router = useRouter();
const pathname = usePathname();
const { openProfile, setThemingOpen, openModal, toggleSidebarCollapsed } = useUi();
const { items: openInbox } = useInbox("OPEN");
const quick = [
flags.threads && { icon: MessagesSquare, label: "Threads", href: "/threads" },
flags.inbox && { icon: InboxIcon, label: "Inbox", href: "/inbox", badge: openInbox.length },
flags.drafts && { icon: Send, label: "Drafts & sent", href: "/drafts" },
flags.savedItems && { icon: Bookmark, label: "Saved", href: "/saved" },
].filter(Boolean) as { icon: typeof Hash; label: string; href: string; badge?: number }[];
return (
<aside className="flex h-full w-[264px] shrink-0 flex-col border-r border-border bg-surface">
{/* workspace header */}
<div className="flex items-center gap-2 px-3.5 py-3">
<div className="brand-mark grid h-8 w-8 shrink-0 place-items-center rounded-[10px] text-sm font-black text-black/80">
{brandName.slice(0, 1)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-[15px] font-bold leading-tight">{brandName}</div>
<div className="flex items-center gap-1 text-[11px] text-muted">
<Circle size={7} className="fill-success text-success" /> Business+
</div>
</div>
<button
title="New message"
onClick={() => openModal("new-message")}
className="focus-ring grid h-8 w-8 shrink-0 place-items-center rounded-control border border-border bg-panel text-ink transition-colors hover:bg-hover"
>
<MessageSquarePlus size={16} />
</button>
<button
title="Collapse sidebar"
onClick={toggleSidebarCollapsed}
className="focus-ring hidden h-8 w-8 shrink-0 place-items-center rounded-control text-muted transition-colors hover:bg-hover hover:text-ink md:grid"
>
<PanelLeftClose size={16} />
</button>
</div>
{/* quick nav */}
<div className="px-2 pb-1">
{quick.map((q) => {
const active = pathname === q.href;
return (
<button
key={q.href}
onClick={() => router.push(q.href)}
className={clsx(
"focus-ring flex w-full items-center gap-2.5 rounded-control px-2.5 py-[7px] text-[14px] transition-colors",
active ? "bg-accent-soft font-semibold text-accent" : "text-muted hover:bg-hover hover:text-ink",
)}
>
<q.icon size={17} />
<span className="flex-1 text-left">{q.label}</span>
{q.badge ? <CountBadge count={q.badge} /> : null}
</button>
);
})}
</div>
<div className="mx-3 my-1.5 h-px bg-border" />
{/* channel lists */}
<div className="min-h-0 flex-1 overflow-y-auto px-2 pb-3">
{flags.messaging && starred.length > 0 && (
<ChannelGroup title="Starred" defaultOpen items={starred} activePath={pathname} onOpen={(c) => router.push(`/c/${c.id}`)} me={me?.id} channelById={channelById} />
)}
{flags.messaging && (
<ChannelGroup
title="Channels"
defaultOpen
items={channels}
activePath={pathname}
onOpen={(c) => router.push(`/c/${c.id}`)}
footer={
flags.channelBrowser ? (
<SidebarAction icon={Plus} label="Add channels" onClick={() => openModal("create-channel")} />
) : null
}
me={me?.id}
channelById={channelById}
/>
)}
{flags.directMessages && (
<ChannelGroup
title="Direct messages"
defaultOpen
items={dms}
activePath={pathname}
onOpen={(c) => router.push(`/c/${c.id}`)}
footer={<SidebarAction icon={Plus} label="Invite people" onClick={() => openModal("invite")} />}
me={me?.id}
channelById={channelById}
/>
)}
</div>
{/* user card footer */}
{me && (
<div className="flex items-center gap-2.5 border-t border-border px-3 py-2.5">
<button
className="focus-ring flex min-w-0 flex-1 items-center gap-2.5 rounded-control p-1 text-left hover:bg-hover"
onClick={() => openProfile(me.id)}
>
<Avatar user={me} size={34} />
<div className="min-w-0 flex-1">
<div className="truncate text-[13px] font-semibold">{me.name}</div>
<div className="truncate text-[11px] text-muted">
{me.statusEmoji} {me.statusText ?? "Active"}
</div>
</div>
</button>
{flags.themingPanel && (
<button
title="Theme & appearance"
onClick={() => setThemingOpen(true)}
className="focus-ring grid h-8 w-8 place-items-center rounded-control text-muted hover:bg-hover hover:text-ink"
>
<Sliders size={16} />
</button>
)}
</div>
)}
</aside>
);
}
function ChannelGroup({
title,
items,
activePath,
onOpen,
footer,
defaultOpen,
me,
channelById,
}: {
title: string;
items: Channel[];
activePath: string;
onOpen: (c: Channel) => void;
footer?: React.ReactNode;
defaultOpen?: boolean;
me?: string;
channelById: (id: string) => Channel | undefined;
}) {
const [open, setOpen] = useState(defaultOpen ?? true);
return (
<div className="mb-1.5">
<button
onClick={() => setOpen((o) => !o)}
className="focus-ring flex w-full items-center gap-1 rounded px-2 py-1 text-[12px] font-bold uppercase tracking-wide text-dim hover:text-muted"
>
{open ? <ChevronDown size={13} /> : <ChevronRight size={13} />}
{title}
</button>
{open && (
<div className="mt-0.5">
{items.map((c) => (
<ChannelRow key={c.id} channel={c} active={activePath === `/c/${c.id}`} onOpen={() => onOpen(c)} meId={me} />
))}
{footer}
</div>
)}
</div>
);
}
function ChannelRow({
channel,
active,
onOpen,
meId,
}: {
channel: Channel;
active: boolean;
onOpen: () => void;
meId?: string;
}) {
const { userById } = useSdk();
const unread = channel.unreadCount ?? 0;
const isDm = channel.kind === "dm";
const other = isDm ? channel.memberIds.find((id) => id !== meId) : undefined;
const otherUser = other ? userById(other) : undefined;
return (
<button
onClick={onOpen}
className={clsx(
"focus-ring group flex w-full items-center gap-2 rounded-control px-2.5 py-[6px] text-[14px] transition-colors",
active
? "bg-accent-soft font-semibold text-accent"
: unread
? "font-semibold text-ink hover:bg-hover"
: "text-muted hover:bg-hover hover:text-ink",
)}
>
<span className="grid w-4 shrink-0 place-items-center">
{channel.kind === "channel" && <Hash size={15} />}
{channel.kind === "private" && <Lock size={13} />}
{channel.kind === "group_dm" && <MessagesSquare size={14} />}
{isDm && otherUser && <Avatar user={otherUser} size={18} showPresence rounded="6px" />}
</span>
<span className="flex-1 truncate text-left">
{channel.name}
{channel.external && <span className="ml-1 text-[10px] text-dim"></span>}
</span>
{channel.mentionCount ? (
<CountBadge count={channel.mentionCount} tone="danger" />
) : unread ? (
<CountBadge count={unread} />
) : null}
</button>
);
}
function SidebarAction({
icon: Icon,
label,
onClick,
}: {
icon: typeof Hash;
label: string;
onClick: () => void;
}) {
return (
<button
onClick={onClick}
className="focus-ring flex w-full items-center gap-2 rounded-control px-2.5 py-[6px] text-[13px] text-muted transition-colors hover:bg-hover hover:text-ink"
>
<span className="grid h-[18px] w-[18px] place-items-center rounded bg-hover">
<Icon size={13} />
</span>
{label}
</button>
);
}