feat(announcements): rename "Announcements" to "What's New" across the application; add AnnouncementsDialog component for displaying updates; update empty state messaging; remove unused AnnouncementsSidebar component.

This commit is contained in:
Anish Sarkar 2026-05-03 18:42:29 +05:30
parent 3cdfe8b5b6
commit a9192beae3
16 changed files with 189 additions and 160 deletions

View file

@ -0,0 +1,50 @@
"use client";
import { useAtom } from "jotai";
import { useEffect } from "react";
import { announcementsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
import { AnnouncementCard } from "@/components/announcements/AnnouncementCard";
import { AnnouncementsEmptyState } from "@/components/announcements/AnnouncementsEmptyState";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { useAnnouncements } from "@/hooks/use-announcements";
export function AnnouncementsDialog() {
const [open, setOpen] = useAtom(announcementsDialogAtom);
const { announcements, markAllRead } = useAnnouncements();
// Auto-mark all visible announcements as read when the dialog opens
useEffect(() => {
if (open) {
markAllRead();
}
}, [open, markAllRead]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="select-none max-w-[900px] w-[95vw] md:w-[90vw] h-[90vh] md:h-[80vh] max-h-[640px] flex flex-col p-0 gap-0 overflow-hidden [--card:var(--background)] dark:[--card:oklch(0.205_0_0)] dark:[--background:oklch(0.205_0_0)]">
<DialogTitle className="sr-only">What's New</DialogTitle>
<div className="flex flex-1 flex-col overflow-hidden min-w-0">
<div className="px-6 md:px-8 pt-6 pb-2 shrink-0">
<h2 className="text-lg font-semibold">What's New</h2>
<Separator className="mt-4" />
</div>
<div className="flex-1 overflow-y-auto overflow-x-hidden">
<div className="px-4 md:px-8 pt-4 pb-6 min-w-0">
{announcements.length === 0 ? (
<AnnouncementsEmptyState />
) : (
<div className="flex flex-col gap-4">
{announcements.map((announcement) => (
<AnnouncementCard key={announcement.id} announcement={announcement} />
))}
</div>
)}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -6,9 +6,9 @@ export function AnnouncementsEmptyState() {
<div className="mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
<BellOff className="h-5 w-5 text-muted-foreground" />
</div>
<h3 className="text-sm font-semibold">No announcements</h3>
<h3 className="text-sm font-semibold">Nothing new yet</h3>
<p className="mt-1 max-w-xs text-xs text-muted-foreground">
You're all caught up! New announcements will appear here.
You're all caught up! New updates will appear here.
</p>
</div>
);

View file

@ -38,7 +38,7 @@ export function FooterNew() {
href: "/contact",
},
{
title: "Announcements",
title: "What's New",
href: "/announcements",
},
];

View file

@ -1,6 +1,6 @@
"use client";
import { Inbox, Megaphone, SquareLibrary } from "lucide-react";
import { Inbox, SquareLibrary } from "lucide-react";
import { useRouter } from "next/navigation";
import type { ReactNode } from "react";
import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
@ -55,28 +55,24 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
const navItems: NavItem[] = useMemo(
() =>
[
{
title: "Inbox",
url: "#inbox",
icon: Inbox,
isActive: false,
},
isMobile
? {
title: "Documents",
url: "#documents",
icon: SquareLibrary,
isActive: false,
}
: null,
{
title: "Announcements",
url: "#announcements",
icon: Megaphone,
isActive: false,
},
].filter((item): item is NavItem => item !== null),
(
[
{
title: "Inbox",
url: "#inbox",
icon: Inbox,
isActive: false,
},
isMobile
? {
title: "Documents",
url: "#documents",
icon: SquareLibrary,
isActive: false,
}
: null,
] as (NavItem | null)[]
).filter((item): item is NavItem => item !== null),
[isMobile]
);
@ -90,11 +86,12 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
(item: NavItem) => {
if (item.title === "Inbox") gate("use the inbox");
else if (item.title === "Documents") setIsDocsSidebarOpen((v) => !v);
else if (item.title === "Announcements") gate("view announcements");
},
[gate]
);
const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]);
const handleSearchSpaceSelect = useCallback(
(_id: number) => gate("switch search spaces"),
[gate]
@ -127,6 +124,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
onSettings={gatedAction("search space settings")}
onManageMembers={gatedAction("team management")}
onUserSettings={gatedAction("account settings")}
onAnnouncements={handleAnnouncements}
onLogout={() => router.push("/register")}
pageUsage={pageUsage}
isChatPage

View file

@ -23,6 +23,8 @@ interface IconRailProps {
onNavItemClick?: (item: NavItem) => void;
user: User;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
theme?: string;
setTheme?: (theme: "light" | "dark" | "system") => void;
@ -42,6 +44,8 @@ export function IconRail({
onNavItemClick,
user,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
theme,
setTheme,
@ -138,6 +142,8 @@ export function IconRail({
<SidebarUserProfile
user={user}
onUserSettings={onUserSettings}
onAnnouncements={onAnnouncements}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
isCollapsed
theme={theme}

View file

@ -19,7 +19,6 @@ import { RightPanel, RightPanelExpandButton } from "../right-panel/RightPanel";
import {
AllPrivateChatsSidebarContent,
AllSharedChatsSidebarContent,
AnnouncementsSidebarContent,
DocumentsSidebar,
InboxSidebarContent,
MobileSidebar,
@ -54,7 +53,7 @@ interface TabDataSource {
markAllAsRead: () => Promise<boolean>;
}
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | "announcements" | null;
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | null;
// Inbox-related props — per-tab data sources with independent loading/pagination
interface InboxProps {
@ -88,6 +87,8 @@ interface LayoutShellProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@ -189,6 +190,8 @@ export function LayoutShell({
onSettings,
onManageMembers,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@ -237,9 +240,7 @@ export function LayoutShell({
? "Shared Chats"
: activeSlideoutPanel === "private"
? "Private Chats"
: activeSlideoutPanel === "announcements"
? "Announcements"
: "Panel";
: "Panel";
// Mobile layout
if (isMobile) {
@ -277,6 +278,8 @@ export function LayoutShell({
onSettings={onSettings}
onManageMembers={onManageMembers}
onUserSettings={onUserSettings}
onAnnouncements={onAnnouncements}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
pageUsage={pageUsage}
theme={theme}
@ -313,21 +316,6 @@ export function LayoutShell({
/>
</motion.div>
)}
{activeSlideoutPanel === "announcements" && (
<motion.div
key="announcements"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AnnouncementsSidebarContent
onOpenChange={(open) => closeSlideout(open)}
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
</motion.div>
)}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
<motion.div
key="shared"
@ -397,6 +385,8 @@ export function LayoutShell({
onNavItemClick={onNavItemClick}
user={user}
onUserSettings={onUserSettings}
onAnnouncements={onAnnouncements}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
theme={theme}
setTheme={setTheme}
@ -433,6 +423,8 @@ export function LayoutShell({
onSettings={onSettings}
onManageMembers={onManageMembers}
onUserSettings={onUserSettings}
onAnnouncements={onAnnouncements}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
pageUsage={pageUsage}
theme={theme}
@ -479,18 +471,6 @@ export function LayoutShell({
/>
</motion.div>
)}
{activeSlideoutPanel === "announcements" && (
<motion.div
key="announcements"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AnnouncementsSidebarContent onOpenChange={(open) => closeSlideout(open)} />
</motion.div>
)}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && (
<motion.div
key="shared"

View file

@ -1,84 +0,0 @@
"use client";
import { ChevronLeft } from "lucide-react";
import { useEffect } from "react";
import { AnnouncementCard } from "@/components/announcements/AnnouncementCard";
import { AnnouncementsEmptyState } from "@/components/announcements/AnnouncementsEmptyState";
import { Button } from "@/components/ui/button";
import { useAnnouncements } from "@/hooks/use-announcements";
import { useMediaQuery } from "@/hooks/use-media-query";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
export interface AnnouncementsSidebarContentProps {
onOpenChange: (open: boolean) => void;
onCloseMobileSidebar?: () => void;
}
interface AnnouncementsSidebarProps extends AnnouncementsSidebarContentProps {
open: boolean;
}
export function AnnouncementsSidebarContent({
onOpenChange,
onCloseMobileSidebar,
}: AnnouncementsSidebarContentProps) {
const isMobile = !useMediaQuery("(min-width: 640px)");
const { announcements, markAllRead } = useAnnouncements();
useEffect(() => {
markAllRead();
}, [markAllRead]);
return (
<div className="h-full flex flex-col">
<div className="shrink-0 p-3 pb-1.5 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{isMobile && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full"
onClick={() => {
onOpenChange(false);
onCloseMobileSidebar?.();
}}
>
<ChevronLeft className="h-4 w-4 text-muted-foreground" />
<span className="sr-only">Close</span>
</Button>
)}
<h2 className="text-md font-semibold">Announcements</h2>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto p-3">
{announcements.length === 0 ? (
<AnnouncementsEmptyState />
) : (
<div className="flex flex-col gap-4">
{announcements.map((announcement) => (
<AnnouncementCard key={announcement.id} announcement={announcement} />
))}
</div>
)}
</div>
</div>
);
}
export function AnnouncementsSidebar({
open,
onOpenChange,
onCloseMobileSidebar,
}: AnnouncementsSidebarProps) {
return (
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel="Announcements">
<AnnouncementsSidebarContent
onOpenChange={onOpenChange}
onCloseMobileSidebar={onCloseMobileSidebar}
/>
</SidebarSlideOutPanel>
);
}

View file

@ -34,6 +34,8 @@ interface MobileSidebarProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@ -77,6 +79,8 @@ export function MobileSidebar({
onSettings,
onManageMembers,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@ -193,6 +197,15 @@ export function MobileSidebar({
}
: undefined
}
onAnnouncements={
onAnnouncements
? () => {
onOpenChange(false);
onAnnouncements();
}
: undefined
}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
pageUsage={pageUsage}
theme={theme}

View file

@ -139,10 +139,6 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
return (
<div className={cn("flex flex-col gap-0.5 py-2", isCollapsed && "items-center")}>
{items.map((item) => {
const joyrideAttr =
item.title === "Inbox" || item.title.toLowerCase().includes("inbox")
? { "data-joyride": "inbox-sidebar" as const }
: {};
const { tooltip } = getStatusInfo(item.statusIndicator);
return (
@ -159,12 +155,11 @@ export function NavSection({ items, onItemClick, isCollapsed = false }: NavSecti
<StatusIcon
status={item.statusIndicator}
FallbackIcon={item.icon}
className="h-3.5 w-3.5"
className="h-3.5 w-3.5"
/>
}
trailingContent={<StatusPill status={item.statusIndicator} />}
tooltipContent={tooltip}
buttonProps={joyrideAttr}
/>
);
})}

View file

@ -4,7 +4,7 @@ import { CreditCard, SquarePen, Zap } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
@ -53,6 +53,8 @@ interface SidebarProps {
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
@ -87,6 +89,8 @@ export function Sidebar({
onSettings,
onManageMembers,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
@ -101,6 +105,14 @@ export function Sidebar({
const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
// Inbox is rendered explicitly right below New Chat. Pull it out of the
// nav items list so it doesn't also appear in the bottom NavSection.
const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]);
const footerNavItems = useMemo(
() => navItems.filter((item) => item.url !== "#inbox"),
[navItems]
);
return (
<div
className={cn(
@ -138,7 +150,7 @@ export function Sidebar({
</div>
)}
{/* New chat button */}
{/* New chat button + Inbox */}
<div className={cn("flex flex-col gap-0.5 py-1.5", isCollapsed && "items-center")}>
<SidebarButton
icon={SquarePen}
@ -146,6 +158,21 @@ export function Sidebar({
onClick={onNewChat}
isCollapsed={isCollapsed}
/>
{inboxItem && (
<SidebarButton
icon={inboxItem.icon}
label={inboxItem.title}
onClick={() => onNavItemClick?.(inboxItem)}
isCollapsed={isCollapsed}
isActive={inboxItem.isActive}
badge={inboxItem.badge}
buttonProps={
{
"data-joyride": "inbox-sidebar",
} as React.ButtonHTMLAttributes<HTMLButtonElement>
}
/>
)}
</div>
{/* Chat sections - fills available space */}
@ -271,8 +298,12 @@ export function Sidebar({
{/* Footer */}
<div className="mt-auto border-t border-border/60">
{/* Platform navigation */}
{navItems.length > 0 && (
<NavSection items={navItems} onItemClick={onNavItemClick} isCollapsed={isCollapsed} />
{footerNavItems.length > 0 && (
<NavSection
items={footerNavItems}
onItemClick={onNavItemClick}
isCollapsed={isCollapsed}
/>
)}
<SidebarUsageFooter pageUsage={pageUsage} isCollapsed={isCollapsed} />
@ -281,6 +312,8 @@ export function Sidebar({
<SidebarUserProfile
user={user}
onUserSettings={onUserSettings}
onAnnouncements={onAnnouncements}
announcementUnreadCount={announcementUnreadCount}
onLogout={onLogout}
isCollapsed={isCollapsed}
theme={theme}

View file

@ -8,6 +8,7 @@ import {
Info,
Languages,
LogOut,
Megaphone,
Monitor,
Moon,
Sun,
@ -60,12 +61,22 @@ const LEARN_MORE_LINKS = [
interface SidebarUserProfileProps {
user: User;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
isCollapsed?: boolean;
theme?: string;
setTheme?: (theme: "light" | "dark" | "system") => void;
}
function formatAnnouncementCount(count: number): string {
if (count <= 999) {
return count.toString();
}
const thousands = Math.floor(count / 1000);
return `${thousands}k+`;
}
/**
* Generates a consistent color based on email
*/
@ -152,6 +163,8 @@ function UserAvatar({
export function SidebarUserProfile({
user,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
isCollapsed = false,
theme,
@ -228,6 +241,18 @@ export function SidebarUserProfile({
{t("user_settings")}
</DropdownMenuItem>
{onAnnouncements && (
<DropdownMenuItem onClick={onAnnouncements}>
<Megaphone className="h-4 w-4" />
<span className="flex-1">What's New</span>
{announcementUnreadCount > 0 && (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{formatAnnouncementCount(announcementUnreadCount)}
</span>
)}
</DropdownMenuItem>
)}
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
@ -382,6 +407,18 @@ export function SidebarUserProfile({
{t("user_settings")}
</DropdownMenuItem>
{onAnnouncements && (
<DropdownMenuItem onClick={onAnnouncements}>
<Megaphone className="h-4 w-4" />
<span className="flex-1">What's New</span>
{announcementUnreadCount > 0 && (
<span className="inline-flex items-center justify-center min-w-4 h-4 px-1 rounded-full bg-red-500 text-white text-[10px] font-medium">
{formatAnnouncementCount(announcementUnreadCount)}
</span>
)}
</DropdownMenuItem>
)}
{setTheme && (
<DropdownMenuSub>
<DropdownMenuSubTrigger>

View file

@ -1,6 +1,5 @@
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar";
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
export { AnnouncementsSidebar, AnnouncementsSidebarContent } from "./AnnouncementsSidebar";
export { ChatListItem } from "./ChatListItem";
export { DocumentsSidebar } from "./DocumentsSidebar";
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";