From 93a09e1978def3c428389411a74c2f7783a24839 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:47:48 +0530 Subject: [PATCH] refactor(layout): remove Inbox component and integrate NotificationsDropdown - Eliminated the Inbox component and its related logic from FreeLayoutDataProvider and LayoutDataProvider. - Introduced NotificationsDropdown to handle notifications in the sidebar and mobile sidebar. - Updated Sidebar and LayoutShell components to accommodate the new notifications structure, enhancing user experience and code clarity. --- .../providers/FreeLayoutDataProvider.tsx | 30 +- .../layout/providers/LayoutDataProvider.tsx | 57 +-- .../layout/ui/icon-rail/IconRail.tsx | 9 + .../layout/ui/shell/LayoutShell.tsx | 112 +----- .../layout/ui/sidebar/MobileSidebar.tsx | 11 + .../ui/sidebar/NotificationsDropdown.tsx | 378 ++++++++++++++++++ .../components/layout/ui/sidebar/Sidebar.tsx | 36 +- .../layout/ui/sidebar/SidebarUserProfile.tsx | 4 + .../components/layout/ui/sidebar/index.ts | 2 +- 9 files changed, 420 insertions(+), 219 deletions(-) create mode 100644 surfsense_web/components/layout/ui/sidebar/NotificationsDropdown.tsx diff --git a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx index 3a74d77b9..059872bac 100644 --- a/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/FreeLayoutDataProvider.tsx @@ -1,14 +1,13 @@ "use client"; -import { Inbox } from "lucide-react"; import { useRouter } from "next/navigation"; import type { ReactNode } from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useLoginGate } from "@/contexts/login-gate"; import { useAnnouncements } from "@/hooks/use-announcements"; import { anonymousChatApiService } from "@/lib/apis/anonymous-chat-api.service"; -import type { ChatItem, NavItem, PageUsage, Workspace } from "../types/layout.types"; +import type { ChatItem, PageUsage, Workspace } from "../types/layout.types"; import { LayoutShell } from "../ui/shell"; interface FreeLayoutDataProviderProps { @@ -47,34 +46,12 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps const gatedAction = useCallback((feature: string) => () => gate(feature), [gate]); - const navItems: NavItem[] = useMemo( - () => - ( - [ - { - title: "Inbox", - url: "#inbox", - icon: Inbox, - isActive: false, - }, - ] as (NavItem | null)[] - ).filter((item): item is NavItem => item !== null), - [] - ); - const pageUsage: PageUsage | undefined = quota ? { pagesUsed: quota.used, pagesLimit: quota.limit } : undefined; const handleChatSelect = useCallback((_chat: ChatItem) => gate("view chat history"), [gate]); - const handleNavItemClick = useCallback( - (item: NavItem) => { - if (item.title === "Inbox") gate("use the inbox"); - }, - [gate] - ); - const handleAnnouncements = useCallback(() => gate("see what's new"), [gate]); const handleWorkspaceSelect = useCallback((_id: number) => gate("switch workspaces"), [gate]); @@ -87,8 +64,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps onWorkspaceSettings={gatedAction("workspace settings")} onAddWorkspace={gatedAction("create workspaces")} workspace={GUEST_SPACE} - navItems={navItems} - onNavItemClick={handleNavItemClick} + navItems={[]} chats={[]} activeChatId={null} onNewChat={resetChat} diff --git a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx index 639965d38..bac179816 100644 --- a/surfsense_web/components/layout/providers/LayoutDataProvider.tsx +++ b/surfsense_web/components/layout/providers/LayoutDataProvider.tsx @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; -import { AlarmClock, AlertTriangle, Boxes, Inbox, SquareTerminal } from "lucide-react"; +import { AlarmClock, AlertTriangle, Boxes, SquareTerminal } from "lucide-react"; import { useParams, usePathname, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useTheme } from "next-themes"; @@ -60,17 +60,6 @@ interface LayoutDataProviderProps { children: React.ReactNode; } -/** - * Format count for display: shows numbers up to 999, then "1k+", "2k+", etc. - */ -function formatInboxCount(count: number): string { - if (count <= 999) { - return count.toString(); - } - const thousands = Math.floor(count / 1000); - return `${thousands}k+`; -} - export function LayoutDataProvider({ workspaceId, children }: LayoutDataProviderProps) { const t = useTranslations("dashboard"); const tCommon = useTranslations("common"); @@ -125,12 +114,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider enabled: !!workspaceId, }); - // Unified slide-out panel state (only one can be open at a time) - type SlideoutPanel = "inbox" | null; - const [activeSlideoutPanel, setActiveSlideoutPanel] = useState(null); - - const isInboxSidebarOpen = activeSlideoutPanel === "inbox"; - // Search space dialog state const [isCreateWorkspaceDialogOpen, setIsCreateWorkspaceDialogOpen] = useState(false); @@ -228,17 +211,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider const [isDeletingWorkspace, setIsDeletingWorkspace] = useState(false); const [isLeavingWorkspace, setIsLeavingWorkspace] = useState(false); - // Reset transient slide-out panels when switching workspaces. - // Tabs intentionally persist across spaces — opening tabs from multiple - // workspaces is a supported flow (browser-tab semantics). - const prevWorkspaceIdRef = useRef(workspaceId); - useEffect(() => { - if (prevWorkspaceIdRef.current !== workspaceId) { - prevWorkspaceIdRef.current = workspaceId; - setActiveSlideoutPanel(null); - } - }, [workspaceId]); - const workspaces: Workspace[] = useMemo(() => { if (!workspacesData || !Array.isArray(workspacesData)) return []; return workspacesData.map((space) => ({ @@ -318,9 +290,9 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider }, [threadsData, workspaceId]); // Navigation items - // Inbox, Automations, and Artifacts are rendered explicitly below "New chat" - // in the sidebar (also surfaced in the icon rail's collapsed mode via this - // list). Documents is embedded below Recents; announcements live in the avatar dropdown. + // Automations and Artifacts are rendered explicitly below "New chat" + // in the sidebar. Documents is embedded below Recents; notifications and + // announcements live in the avatar rail/dropdown. const isAutomationsActive = pathname?.includes("/automations") === true; const isArtifactsActive = pathname?.endsWith("/artifacts") === true; const isPlaygroundRoute = pathname?.includes("/playground") === true; @@ -328,13 +300,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider () => ( [ - { - title: "Inbox", - url: "#inbox", - icon: Inbox, - isActive: isInboxSidebarOpen, - badge: totalUnreadCount > 0 ? formatInboxCount(totalUnreadCount) : undefined, - }, { title: "Automations", url: `/dashboard/${workspaceId}/automations`, @@ -358,8 +323,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider ] as (NavItem | null)[] ).filter((item): item is NavItem => item !== null), [ - isInboxSidebarOpen, - totalUnreadCount, workspaceId, isAutomationsActive, isArtifactsActive, @@ -503,10 +466,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider const handleNavItemClick = useCallback( (item: NavItem) => { - if (item.url === "#inbox") { - setActiveSlideoutPanel((prev) => (prev === "inbox" ? null : "inbox")); - return; - } // Desktop: Playground is a persistent toggle, not a plain link — it just // opens the second-level sidebar (which holds the whole API playground) // and only closes on a second click, never navigating away from the @@ -518,7 +477,7 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider } router.push(item.url); }, - [router, setPlaygroundSidebarOpen, setActiveSlideoutPanel, isMobile] + [router, setPlaygroundSidebarOpen, isMobile] ); const handleNewChat = useCallback(() => { @@ -688,7 +647,6 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider const isPlaygroundPage = pathname?.includes("/playground") === true; const isAllChatsPage = pathname?.endsWith("/chats") === true; const handleViewAllChats = useCallback(() => { - setActiveSlideoutPanel(null); router.push( isAllChatsPage ? `/dashboard/${workspaceId}/new-chat` : `/dashboard/${workspaceId}/chats` ); @@ -764,10 +722,7 @@ export function LayoutDataProvider({ workspaceId, children }: LayoutDataProvider : undefined } isLoadingChats={isLoadingThreads} - activeSlideoutPanel={activeSlideoutPanel} - onSlideoutPanelChange={setActiveSlideoutPanel} - inbox={{ - isOpen: isInboxSidebarOpen, + notifications={{ totalUnreadCount, comments: { items: commentsInbox.inboxItems, diff --git a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx index 5170fa770..1f700c89f 100644 --- a/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx +++ b/surfsense_web/components/layout/ui/icon-rail/IconRail.tsx @@ -6,6 +6,10 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import type { NavItem, User, Workspace } from "../../types/layout.types"; +import { + NotificationsDropdown, + type NotificationsDropdownData, +} from "../sidebar/NotificationsDropdown"; import { SidebarUserProfile } from "../sidebar/SidebarUserProfile"; import { WorkspaceAvatar } from "./WorkspaceAvatar"; @@ -24,6 +28,7 @@ interface IconRailProps { onUserSettings?: () => void; onAnnouncements?: () => void; announcementUnreadCount?: number; + notifications?: NotificationsDropdownData; onLogout?: () => void; theme?: string; setTheme?: (theme: "light" | "dark" | "system") => void; @@ -45,6 +50,7 @@ export function IconRail({ onUserSettings, onAnnouncements, announcementUnreadCount = 0, + notifications, onLogout, theme, setTheme, @@ -145,6 +151,9 @@ export function IconRail({ isCollapsed theme={theme} setTheme={setTheme} + topContent={ + notifications ? : undefined + } /> ); diff --git a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx index 7adad0cc3..00e606024 100644 --- a/surfsense_web/components/layout/ui/shell/LayoutShell.tsx +++ b/surfsense_web/components/layout/ui/shell/LayoutShell.tsx @@ -1,14 +1,12 @@ "use client"; import { useAtomValue } from "jotai"; -import { AnimatePresence, motion } from "motion/react"; import dynamic from "next/dynamic"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { activeTabAtom, type Tab } from "@/atoms/tabs/tabs.atom"; import { Logo } from "@/components/Logo"; import { Spinner } from "@/components/ui/spinner"; import { TooltipProvider } from "@/components/ui/tooltip"; -import type { InboxItem } from "@/hooks/use-inbox"; import { useIsMobile } from "@/hooks/use-mobile"; import { useElectronAPI } from "@/hooks/use-platform"; import { cn } from "@/lib/utils"; @@ -27,14 +25,13 @@ import { RightPanelToggleButton, } from "../right-panel/RightPanel"; import { - InboxSidebarContent, MobileSidebar, MobileSidebarTrigger, PlaygroundSidebar, Sidebar, SidebarCollapseButton, } from "../sidebar"; -import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel"; +import type { NotificationsDropdownData } from "../sidebar/NotificationsDropdown"; import { TabBar } from "../tabs/TabBar"; import { WorkspacePanel } from "./WorkspacePanel"; @@ -80,28 +77,6 @@ function MacDesktopTitleBar({ ); } -// Per-tab data source -interface TabDataSource { - items: InboxItem[]; - unreadCount: number; - loading: boolean; - loadingMore: boolean; - hasMore: boolean; - loadMore: () => void; - markAsRead: (id: number) => Promise; - markAllAsRead: () => Promise; -} - -export type ActiveSlideoutPanel = "inbox" | null; - -// Inbox-related props — per-tab data sources with independent loading/pagination -interface InboxProps { - isOpen: boolean; - totalUnreadCount: number; - comments: TabDataSource; - status: TabDataSource; -} - interface LayoutShellProps { workspaces: Workspace[]; activeWorkspaceId: number | null; @@ -140,11 +115,7 @@ interface LayoutShellProps { workspacePanelContentClassName?: string; children: React.ReactNode; className?: string; - // Unified slide-out panel state - activeSlideoutPanel?: ActiveSlideoutPanel; - onSlideoutPanelChange?: (panel: ActiveSlideoutPanel) => void; - // Inbox props - inbox?: InboxProps; + notifications?: NotificationsDropdownData; isLoadingChats?: boolean; onTabSwitch?: (tab: Tab) => void; onTabPrefetch?: (tab: Tab) => void; @@ -245,9 +216,7 @@ export function LayoutShell({ workspacePanelContentClassName, children, className, - activeSlideoutPanel = null, - onSlideoutPanelChange, - inbox, + notifications, isLoadingChats = false, onTabSwitch, onTabPrefetch, @@ -269,17 +238,6 @@ export function LayoutShell({ [isCollapsed, setIsCollapsed, toggleCollapsed] ); - const closeSlideout = useCallback( - (open: boolean) => { - if (!open) onSlideoutPanelChange?.(null); - }, - [onSlideoutPanelChange] - ); - - const anySlideOutOpen = activeSlideoutPanel !== null; - - const panelAriaLabel = activeSlideoutPanel === "inbox" ? "Inbox" : "Panel"; - // Mobile layout if (isMobile) { return ( @@ -316,6 +274,7 @@ export function LayoutShell({ onUserSettings={onUserSettings} onAnnouncements={onAnnouncements} announcementUnreadCount={announcementUnreadCount} + notifications={notifications} onLogout={onLogout} pageUsage={pageUsage} theme={theme} @@ -335,34 +294,6 @@ export function LayoutShell({ {children} )} - - {/* Mobile unified slide-out panel */} - - - {activeSlideoutPanel === "inbox" && inbox && ( - - closeSlideout(open)} - comments={inbox.comments} - status={inbox.status} - totalUnreadCount={inbox.totalUnreadCount} - onCloseMobileSidebar={() => setMobileMenuOpen(false)} - /> - - )} - - @@ -400,6 +331,7 @@ export function LayoutShell({ onUserSettings={onUserSettings} onAnnouncements={onAnnouncements} announcementUnreadCount={announcementUnreadCount} + notifications={notifications} onLogout={onLogout} theme={theme} setTheme={setTheme} @@ -425,10 +357,7 @@ export function LayoutShell({ className={cn( "relative hidden md:flex shrink-0 z-20 -mr-2 bg-panel", isMacDesktop - ? cn( - "border-t border-r", - !showPlaygroundSidebar && "rounded-tl-xl border-l" - ) + ? cn("border-t border-r", !showPlaygroundSidebar && "rounded-tl-xl border-l") : "border-r" )} > @@ -491,33 +420,6 @@ export function LayoutShell({ )} /> )} - - {/* Unified slide-out panel — shell stays open, content cross-fades */} - - - {activeSlideoutPanel === "inbox" && inbox && ( - - closeSlideout(open)} - comments={inbox.comments} - status={inbox.status} - totalUnreadCount={inbox.totalUnreadCount} - /> - - )} - - diff --git a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx index f9901d7cb..94fe2d413 100644 --- a/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/MobileSidebar.tsx @@ -6,6 +6,7 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import type { ChatItem, NavItem, PageUsage, User, Workspace } from "../../types/layout.types"; import { WorkspaceAvatar } from "../icon-rail/WorkspaceAvatar"; +import { NotificationsDropdown, type NotificationsDropdownData } from "./NotificationsDropdown"; import { Sidebar } from "./Sidebar"; import { SidebarUserProfile } from "./SidebarUserProfile"; @@ -35,6 +36,7 @@ interface MobileSidebarProps { onUserSettings?: () => void; onAnnouncements?: () => void; announcementUnreadCount?: number; + notifications?: NotificationsDropdownData; onLogout?: () => void; pageUsage?: PageUsage; theme?: string; @@ -83,6 +85,7 @@ export function MobileSidebar({ onUserSettings, onAnnouncements, announcementUnreadCount = 0, + notifications, onLogout, pageUsage, theme, @@ -161,6 +164,14 @@ export function MobileSidebar({ isCollapsed theme={theme} setTheme={setTheme} + topContent={ + notifications ? ( + onOpenChange(false)} + /> + ) : undefined + } /> diff --git a/surfsense_web/components/layout/ui/sidebar/NotificationsDropdown.tsx b/surfsense_web/components/layout/ui/sidebar/NotificationsDropdown.tsx new file mode 100644 index 000000000..e432e077a --- /dev/null +++ b/surfsense_web/components/layout/ui/sidebar/NotificationsDropdown.tsx @@ -0,0 +1,378 @@ +"use client"; + +import { useAtom } from "jotai"; +import { Bell } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useCallback, useMemo, useState } from "react"; +import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Spinner } from "@/components/ui/spinner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import { + isCommentReplyMetadata, + isInsufficientCreditsMetadata, + isNewMentionMetadata, +} from "@/contracts/types/inbox.types"; +import type { InboxItem } from "@/hooks/use-inbox"; +import { cn } from "@/lib/utils"; + +export interface NotificationsDataSource { + items: InboxItem[]; + unreadCount: number; + loading: boolean; + loadingMore?: boolean; + hasMore?: boolean; + loadMore?: () => void; + markAsRead: (id: number) => Promise; + markAllAsRead: () => Promise; +} + +export interface NotificationsDropdownData { + totalUnreadCount: number; + comments: NotificationsDataSource; + status: NotificationsDataSource; +} + +interface NotificationsDropdownProps { + notifications: NotificationsDropdownData; + onCloseMobileSidebar?: () => void; +} + +type NotificationFilter = "mentions" | "status" | null; + +function formatNotificationCount(count: number): string { + if (count <= 999) { + return count.toString(); + } + const thousands = Math.floor(count / 1000); + return `${thousands}k+`; +} + +function formatTime(dateString: string): string { + try { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / (1000 * 60)); + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffMins < 1) return "now"; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return `${Math.floor(diffDays / 7)}w ago`; + } catch { + return "now"; + } +} + +function isCommentNotification(item: InboxItem): boolean { + return item.type === "new_mention" || item.type === "comment_reply"; +} + +export function NotificationsDropdown({ + notifications, + onCloseMobileSidebar, +}: NotificationsDropdownProps) { + const router = useRouter(); + const [, setTargetCommentId] = useAtom(setTargetCommentIdAtom); + const [open, setOpen] = useState(false); + const [activeFilter, setActiveFilter] = useState(null); + const [markingAsReadId, setMarkingAsReadId] = useState(null); + const [markingAllAsRead, setMarkingAllAsRead] = useState(false); + + const unreadLabel = formatNotificationCount(notifications.totalUnreadCount); + const visibleUnreadCount = + activeFilter === "mentions" + ? notifications.comments.unreadCount + : activeFilter === "status" + ? notifications.status.unreadCount + : notifications.totalUnreadCount; + const visibleUnreadLabel = formatNotificationCount(visibleUnreadCount); + const isLoading = + activeFilter === "mentions" + ? notifications.comments.loading + : activeFilter === "status" + ? notifications.status.loading + : notifications.comments.loading || notifications.status.loading; + const items = useMemo(() => { + const sourceItems = + activeFilter === "mentions" + ? notifications.comments.items + : activeFilter === "status" + ? notifications.status.items + : [...notifications.comments.items, ...notifications.status.items]; + + return sourceItems + .toSorted((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 8); + }, [activeFilter, notifications.comments.items, notifications.status.items]); + + const markItemAsRead = useCallback( + async (item: InboxItem) => { + if (item.read) return; + setMarkingAsReadId(item.id); + try { + await (isCommentNotification(item) + ? notifications.comments.markAsRead(item.id) + : notifications.status.markAsRead(item.id)); + } finally { + setMarkingAsReadId(null); + } + }, + [notifications.comments, notifications.status] + ); + + const handleItemClick = useCallback( + async (item: InboxItem) => { + await markItemAsRead(item); + + if (item.type === "new_mention" && isNewMentionMetadata(item.metadata)) { + const threadId = item.metadata.thread_id; + const commentId = item.metadata.comment_id; + if (item.workspace_id && threadId) { + if (commentId) setTargetCommentId(commentId); + setOpen(false); + onCloseMobileSidebar?.(); + router.push( + commentId + ? `/dashboard/${item.workspace_id}/new-chat/${threadId}?commentId=${commentId}` + : `/dashboard/${item.workspace_id}/new-chat/${threadId}` + ); + } + return; + } + + if (item.type === "comment_reply" && isCommentReplyMetadata(item.metadata)) { + const threadId = item.metadata.thread_id; + const replyId = item.metadata.reply_id; + if (item.workspace_id && threadId) { + if (replyId) setTargetCommentId(replyId); + setOpen(false); + onCloseMobileSidebar?.(); + router.push( + replyId + ? `/dashboard/${item.workspace_id}/new-chat/${threadId}?commentId=${replyId}` + : `/dashboard/${item.workspace_id}/new-chat/${threadId}` + ); + } + return; + } + + if (item.type === "insufficient_credits" && isInsufficientCreditsMetadata(item.metadata)) { + if (item.metadata.action_url) { + setOpen(false); + onCloseMobileSidebar?.(); + router.push(item.metadata.action_url); + } + } + }, + [markItemAsRead, onCloseMobileSidebar, router, setTargetCommentId] + ); + + const handleMarkAllAsRead = useCallback(async () => { + if (visibleUnreadCount === 0 || markingAllAsRead) return; + setMarkingAllAsRead(true); + try { + if (activeFilter === "mentions") { + await notifications.comments.markAllAsRead(); + } else if (activeFilter === "status") { + await notifications.status.markAllAsRead(); + } else { + await Promise.all([ + notifications.comments.markAllAsRead(), + notifications.status.markAllAsRead(), + ]); + } + } finally { + setMarkingAllAsRead(false); + } + }, [activeFilter, markingAllAsRead, notifications, visibleUnreadCount]); + + const handleFilterClick = useCallback((filter: Exclude) => { + setActiveFilter((current) => (current === filter ? null : filter)); + }, []); + + const emptyStateCopy = + activeFilter === "mentions" + ? { + title: "No mentions", + description: "Mentions and replies will appear here.", + } + : activeFilter === "status" + ? { + title: "No status updates", + description: "Connector and document updates will appear here.", + } + : { + title: "No notifications", + description: "Mentions, replies, and status updates will appear here.", + }; + + return ( + + + + + + + + + Notifications + + + +
+
+

Notifications

+

+ {visibleUnreadCount > 0 ? `${visibleUnreadLabel} unread` : "You're all caught up"} +

+
+ +
+ +
+ + +
+ +
+ {isLoading ? ( +
+ {[82, 64, 74].map((width) => ( +
+
+ + +
+
+ ))} +
+ ) : items.length > 0 ? ( +
+ {items.map((item) => { + const isMarkingAsRead = markingAsReadId === item.id; + return ( + + ); + })} +
+ ) : ( +
+

{emptyStateCopy.title}

+

{emptyStateCopy.description}

+
+ )} +
+
+
+ ); +} diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index 35746acf1..28264a77f 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -46,21 +46,6 @@ function ChatListSkeletonRows() { ); } -function CollapsedInboxIcon({ item }: { item: NavItem }) { - const Icon = item.icon; - - return ( - - - {typeof item.badge === "string" ? ( - - {item.badge} - - ) : null} - - ); -} - interface SidebarProps { workspace: Workspace | null; isCollapsed?: boolean; @@ -138,10 +123,9 @@ export function Sidebar({ const [openDropdownChatId, setOpenDropdownChatId] = useState(null); const [isSidebarNavScrolled, setIsSidebarNavScrolled] = useState(false); - // Inbox, Automations, and Artifacts are rendered explicitly right below + // Automations, Artifacts, and Playground are rendered explicitly right below // New Chat. Pull them out of the nav items list so they don't also appear // in the bottom NavSection. Documents is embedded below Recents. - const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]); const automationsItem = useMemo( () => navItems.find((item) => item.url.endsWith("/automations")), [navItems] @@ -158,7 +142,6 @@ export function Sidebar({ () => navItems.filter( (item) => - item.url !== "#inbox" && !item.url.endsWith("/automations") && !item.url.endsWith("/artifacts") && !item.url.endsWith("/playground") @@ -235,23 +218,6 @@ export function Sidebar({ onScroll={(event) => setIsSidebarNavScrolled(event.currentTarget.scrollTop > 0)} >
- {inboxItem && ( - onNavItemClick?.(inboxItem)} - isCollapsed={isCollapsed} - isActive={inboxItem.isActive} - badge={inboxItem.badge} - collapsedIconNode={} - tooltipContent={isCollapsed ? inboxItem.title : undefined} - buttonProps={ - { - "data-joyride": "inbox-sidebar", - } as React.ButtonHTMLAttributes - } - /> - )} {automationsItem && ( void; + topContent?: React.ReactNode; } function formatAnnouncementCount(count: number): string { @@ -134,6 +136,7 @@ export function SidebarUserProfile({ isCollapsed = false, theme, setTheme, + topContent, }: SidebarUserProfileProps) { const t = useTranslations("sidebar"); const { locale, setLocale } = useLocaleContext(); @@ -171,6 +174,7 @@ export function SidebarUserProfile({ return (
+ {topContent} {showDownloadCta && ( diff --git a/surfsense_web/components/layout/ui/sidebar/index.ts b/surfsense_web/components/layout/ui/sidebar/index.ts index d1a1f85e5..b74e31b53 100644 --- a/surfsense_web/components/layout/ui/sidebar/index.ts +++ b/surfsense_web/components/layout/ui/sidebar/index.ts @@ -2,9 +2,9 @@ export { AllChatsWorkspaceContent } from "./AllChatsSidebar"; export { ChatListItem } from "./ChatListItem"; export { CreditBalanceDisplay } from "./CreditBalanceDisplay"; export { DocumentsSidebar } from "./DocumentsSidebar"; -export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar"; export { MobileSidebar, MobileSidebarTrigger } from "./MobileSidebar"; export { NavSection } from "./NavSection"; +export { NotificationsDropdown } from "./NotificationsDropdown"; export { PlaygroundSidebar } from "./PlaygroundSidebar"; export { Sidebar } from "./Sidebar"; export { SidebarCollapseButton } from "./SidebarCollapseButton";