2026-01-21 19:43:20 +05:30
"use client" ;
2026-02-06 16:45:54 +05:30
import { useQuery } from "@tanstack/react-query" ;
2026-01-28 22:15:43 -08:00
import { useAtom } from "jotai" ;
2026-01-21 19:43:20 +05:30
import {
AlertCircle ,
2026-02-01 18:02:17 -08:00
AlertTriangle ,
2026-01-21 19:43:20 +05:30
BellDot ,
Check ,
CheckCheck ,
CheckCircle2 ,
2026-01-27 20:59:03 +05:30
ChevronLeft ,
2026-01-21 19:43:20 +05:30
History ,
Inbox ,
2026-01-22 17:46:44 +05:30
LayoutGrid ,
2026-01-21 19:43:20 +05:30
ListFilter ,
2026-02-04 17:17:33 +02:00
MessageSquare ,
2026-01-21 19:43:20 +05:30
Search ,
X ,
} from "lucide-react" ;
2026-02-06 16:45:54 +05:30
import { useParams , useRouter } from "next/navigation" ;
2026-01-21 19:43:20 +05:30
import { useTranslations } from "next-intl" ;
2026-04-02 01:45:18 +05:30
import { useCallback , useDeferredValue , useEffect , useMemo , useRef , useState } from "react" ;
2026-04-07 23:01:47 +05:30
import { getDocumentTypeLabel } from "@/components/documents/DocumentTypeIcon" ;
2026-03-10 14:00:29 +05:30
import { setTargetCommentIdAtom } from "@/atoms/chat/current-thread.atom" ;
2026-01-21 19:43:20 +05:30
import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item" ;
2026-03-22 00:43:53 +05:30
import { Tabs , TabsList , TabsTrigger } from "@/components/ui/animated-tabs" ;
2026-01-21 19:43:20 +05:30
import { Avatar , AvatarFallback , AvatarImage } from "@/components/ui/avatar" ;
import { Button } from "@/components/ui/button" ;
2026-01-22 04:02:32 +05:30
import {
Drawer ,
DrawerContent ,
DrawerHandle ,
DrawerHeader ,
DrawerTitle ,
} from "@/components/ui/drawer" ;
2026-01-21 19:43:20 +05:30
import {
DropdownMenu ,
DropdownMenuContent ,
DropdownMenuItem ,
DropdownMenuLabel ,
DropdownMenuTrigger ,
} from "@/components/ui/dropdown-menu" ;
import { Input } from "@/components/ui/input" ;
2026-02-06 16:58:38 +05:30
import { Skeleton } from "@/components/ui/skeleton" ;
2026-01-21 22:22:28 +05:30
import { Spinner } from "@/components/ui/spinner" ;
2026-01-21 19:43:20 +05:30
import { Tooltip , TooltipContent , TooltipTrigger } from "@/components/ui/tooltip" ;
2026-01-22 02:13:35 +05:30
import { getConnectorIcon } from "@/contracts/enums/connectorIcons" ;
2026-02-01 18:02:17 -08:00
import {
2026-02-04 14:47:03 +02:00
isCommentReplyMetadata ,
2026-02-01 18:02:17 -08:00
isConnectorIndexingMetadata ,
2026-03-06 17:02:19 +05:30
isDocumentProcessingMetadata ,
2026-02-01 18:02:17 -08:00
isNewMentionMetadata ,
isPageLimitExceededMetadata ,
} from "@/contracts/types/inbox.types" ;
2026-02-06 16:45:54 +05:30
import { useDebouncedValue } from "@/hooks/use-debounced-value" ;
2026-02-09 16:49:11 -08:00
import type { InboxItem } from "@/hooks/use-inbox" ;
2026-01-22 16:43:08 -08:00
import { useMediaQuery } from "@/hooks/use-media-query" ;
2026-02-06 16:45:54 +05:30
import { notificationsApiService } from "@/lib/apis/notifications-api.service" ;
import { cacheKeys } from "@/lib/query-client/cache-keys" ;
2026-01-21 19:43:20 +05:30
import { cn } from "@/lib/utils" ;
2026-02-09 11:41:55 -05:00
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel" ;
2026-01-21 19:43:20 +05:30
function getInitials ( name : string | null | undefined , email : string | null | undefined ) : string {
if ( name ) {
return name
. split ( " " )
. map ( ( n ) = > n [ 0 ] )
. join ( "" )
. toUpperCase ( )
. slice ( 0 , 2 ) ;
}
if ( email ) {
const localPart = email . split ( "@" ) [ 0 ] ;
return localPart . slice ( 0 , 2 ) . toUpperCase ( ) ;
}
return "U" ;
}
2026-01-24 01:20:51 +05:30
function formatInboxCount ( count : number ) : string {
if ( count <= 999 ) {
return count . toString ( ) ;
}
const thousands = Math . floor ( count / 1000 ) ;
return ` ${ thousands } k+ ` ;
}
2026-01-22 04:02:32 +05:30
function getConnectorTypeDisplayName ( connectorType : string ) : string {
const displayNames : Record < string , string > = {
GITHUB_CONNECTOR : "GitHub" ,
GOOGLE_CALENDAR_CONNECTOR : "Google Calendar" ,
GOOGLE_GMAIL_CONNECTOR : "Gmail" ,
GOOGLE_DRIVE_CONNECTOR : "Google Drive" ,
2026-01-23 14:12:57 +05:30
COMPOSIO_GOOGLE_DRIVE_CONNECTOR : "Composio Google Drive" ,
COMPOSIO_GMAIL_CONNECTOR : "Composio Gmail" ,
COMPOSIO_GOOGLE_CALENDAR_CONNECTOR : "Composio Google Calendar" ,
2026-01-22 04:02:32 +05:30
LINEAR_CONNECTOR : "Linear" ,
NOTION_CONNECTOR : "Notion" ,
SLACK_CONNECTOR : "Slack" ,
TEAMS_CONNECTOR : "Microsoft Teams" ,
DISCORD_CONNECTOR : "Discord" ,
JIRA_CONNECTOR : "Jira" ,
CONFLUENCE_CONNECTOR : "Confluence" ,
BOOKSTACK_CONNECTOR : "BookStack" ,
CLICKUP_CONNECTOR : "ClickUp" ,
AIRTABLE_CONNECTOR : "Airtable" ,
LUMA_CONNECTOR : "Luma" ,
ELASTICSEARCH_CONNECTOR : "Elasticsearch" ,
WEBCRAWLER_CONNECTOR : "Web Crawler" ,
YOUTUBE_CONNECTOR : "YouTube" ,
CIRCLEBACK_CONNECTOR : "Circleback" ,
MCP_CONNECTOR : "MCP" ,
2026-01-27 19:46:43 +05:30
OBSIDIAN_CONNECTOR : "Obsidian" ,
2026-03-30 23:09:36 +05:30
ONEDRIVE_CONNECTOR : "OneDrive" ,
2026-03-30 22:54:43 +05:30
DROPBOX_CONNECTOR : "Dropbox" ,
2026-01-22 04:02:32 +05:30
TAVILY_API : "Tavily" ,
SEARXNG_API : "SearXNG" ,
LINKUP_API : "Linkup" ,
BAIDU_SEARCH_API : "Baidu" ,
} ;
2026-01-22 17:46:44 +05:30
return (
displayNames [ connectorType ] ||
connectorType
. replace ( /_/g , " " )
. replace ( /CONNECTOR|API/gi , "" )
. trim ( )
) ;
2026-01-22 04:02:32 +05:30
}
2026-02-04 17:17:33 +02:00
type InboxTab = "comments" | "status" ;
2026-03-06 17:29:42 +05:30
type InboxFilter = "all" | "unread" | "errors" ;
2026-01-21 19:43:20 +05:30
2026-03-06 19:35:35 +05:30
interface TabDataSource {
items : InboxItem [ ] ;
unreadCount : number ;
loading : boolean ;
loadingMore : boolean ;
hasMore : boolean ;
loadMore : ( ) = > void ;
markAsRead : ( id : number ) = > Promise < boolean > ;
markAllAsRead : ( ) = > Promise < boolean > ;
}
2026-01-28 02:14:36 +05:30
2026-03-22 00:01:31 +05:30
export interface InboxSidebarContentProps {
2026-01-28 02:14:36 +05:30
onOpenChange : ( open : boolean ) = > void ;
2026-03-06 19:35:35 +05:30
comments : TabDataSource ;
status : TabDataSource ;
2026-01-28 02:14:36 +05:30
totalUnreadCount : number ;
2026-01-21 19:43:20 +05:30
onCloseMobileSidebar ? : ( ) = > void ;
}
2026-03-22 00:01:31 +05:30
interface InboxSidebarProps extends InboxSidebarContentProps {
open : boolean ;
}
export function InboxSidebarContent ( {
2026-01-21 19:43:20 +05:30
onOpenChange ,
2026-03-06 19:35:35 +05:30
comments ,
status ,
2026-01-28 02:14:36 +05:30
totalUnreadCount ,
2026-01-21 19:43:20 +05:30
onCloseMobileSidebar ,
2026-03-22 00:01:31 +05:30
} : InboxSidebarContentProps ) {
2026-01-21 19:43:20 +05:30
const t = useTranslations ( "sidebar" ) ;
const router = useRouter ( ) ;
2026-02-06 16:45:54 +05:30
const params = useParams ( ) ;
2026-01-22 04:02:32 +05:30
const isMobile = ! useMediaQuery ( "(min-width: 640px)" ) ;
2026-02-06 16:45:54 +05:30
const searchSpaceId = params ? . search_space_id ? Number ( params . search_space_id ) : null ;
2026-01-21 19:43:20 +05:30
2026-01-27 22:14:02 +05:30
const [ , setTargetCommentId ] = useAtom ( setTargetCommentIdAtom ) ;
2026-01-27 20:59:03 +05:30
2026-01-21 19:43:20 +05:30
const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
2026-02-06 16:45:54 +05:30
const debouncedSearch = useDebouncedValue ( searchQuery , 300 ) ;
const isSearchMode = ! ! debouncedSearch . trim ( ) ;
2026-02-04 17:17:33 +02:00
const [ activeTab , setActiveTab ] = useState < InboxTab > ( "comments" ) ;
2026-01-21 19:43:20 +05:30
const [ activeFilter , setActiveFilter ] = useState < InboxFilter > ( "all" ) ;
2026-03-06 17:02:19 +05:30
const [ selectedSource , setSelectedSource ] = useState < string | null > ( null ) ;
2026-01-21 19:43:20 +05:30
const [ mounted , setMounted ] = useState ( false ) ;
2026-01-21 23:07:08 +05:30
const [ openDropdown , setOpenDropdown ] = useState < "filter" | null > ( null ) ;
2026-02-06 16:45:54 +05:30
const [ connectorScrollPos , setConnectorScrollPos ] = useState < "top" | "middle" | "bottom" > ( "top" ) ;
2026-04-02 23:21:57 -07:00
const connectorRafRef = useRef < number > ( ) ;
2026-02-06 16:45:54 +05:30
const handleConnectorScroll = useCallback ( ( e : React.UIEvent < HTMLDivElement > ) = > {
const el = e . currentTarget ;
2026-04-02 23:21:57 -07:00
if ( connectorRafRef . current ) return ;
connectorRafRef . current = requestAnimationFrame ( ( ) = > {
const atTop = el . scrollTop <= 2 ;
const atBottom = el . scrollHeight - el . scrollTop - el . clientHeight <= 2 ;
setConnectorScrollPos ( atTop ? "top" : atBottom ? "bottom" : "middle" ) ;
connectorRafRef . current = undefined ;
} ) ;
2026-02-06 16:45:54 +05:30
} , [ ] ) ;
2026-04-02 23:43:19 -07:00
useEffect (
( ) = > ( ) = > {
if ( connectorRafRef . current ) cancelAnimationFrame ( connectorRafRef . current ) ;
} ,
[ ]
) ;
2026-01-22 04:02:32 +05:30
const [ filterDrawerOpen , setFilterDrawerOpen ] = useState ( false ) ;
2026-01-21 19:43:20 +05:30
const [ markingAsReadId , setMarkingAsReadId ] = useState < number | null > ( null ) ;
2026-01-22 17:46:44 +05:30
2026-01-22 11:27:45 +05:30
const prefetchTriggerRef = useRef < HTMLDivElement > ( null ) ;
2026-01-21 19:43:20 +05:30
2026-03-06 19:35:21 +05:30
// Server-side search query
2026-02-06 18:22:19 +05:30
const searchTypeFilter = activeTab === "comments" ? ( "new_mention" as const ) : undefined ;
2026-02-06 16:45:54 +05:30
const { data : searchResponse , isLoading : isSearchLoading } = useQuery ( {
queryKey : cacheKeys.notifications.search ( searchSpaceId , debouncedSearch . trim ( ) , activeTab ) ,
queryFn : ( ) = >
notificationsApiService . getNotifications ( {
queryParams : {
search_space_id : searchSpaceId ? ? undefined ,
type : searchTypeFilter ,
search : debouncedSearch.trim ( ) ,
limit : 50 ,
} ,
} ) ,
2026-03-06 19:35:21 +05:30
staleTime : 30 * 1000 ,
2026-03-22 00:01:31 +05:30
enabled : isSearchMode ,
2026-02-06 16:45:54 +05:30
} ) ;
2026-01-21 19:43:20 +05:30
useEffect ( ( ) = > {
setMounted ( true ) ;
} , [ ] ) ;
useEffect ( ( ) = > {
2026-03-22 00:01:31 +05:30
if ( ! isMobile ) return ;
2026-01-27 20:59:03 +05:30
const originalOverflow = document . body . style . overflow ;
document . body . style . overflow = "hidden" ;
2026-01-21 19:43:20 +05:30
return ( ) = > {
2026-01-27 20:59:03 +05:30
document . body . style . overflow = originalOverflow ;
2026-01-21 19:43:20 +05:30
} ;
2026-03-22 00:01:31 +05:30
} , [ isMobile ] ) ;
2026-01-21 19:43:20 +05:30
2026-01-22 02:13:35 +05:30
useEffect ( ( ) = > {
if ( activeTab !== "status" ) {
2026-03-06 17:02:19 +05:30
setSelectedSource ( null ) ;
2026-01-22 02:13:35 +05:30
}
} , [ activeTab ] ) ;
2026-03-06 19:35:35 +05:30
// Active tab's data source — fully independent loading, pagination, and counts
const activeSource = activeTab === "comments" ? comments : status ;
2026-02-06 16:45:54 +05:30
2026-03-06 19:35:21 +05:30
// Fetch source types for the status tab filter
2026-03-06 17:25:07 +05:30
const { data : sourceTypesData } = useQuery ( {
queryKey : cacheKeys.notifications.sourceTypes ( searchSpaceId ) ,
queryFn : ( ) = > notificationsApiService . getSourceTypes ( searchSpaceId ? ? undefined ) ,
staleTime : 60 * 1000 ,
2026-03-22 00:01:31 +05:30
enabled : activeTab === "status" ,
2026-03-06 17:25:07 +05:30
} ) ;
2026-01-22 02:13:35 +05:30
2026-03-06 17:25:07 +05:30
const statusSourceOptions = useMemo ( ( ) = > {
if ( ! sourceTypesData ? . sources ) return [ ] ;
return sourceTypesData . sources . map ( ( source ) = > ( {
key : source.key ,
type : source . type ,
category : source.category ,
displayName :
source . category === "connector"
? getConnectorTypeDisplayName ( source . type )
: getDocumentTypeLabel ( source . type ) ,
} ) ) ;
} , [ sourceTypesData ] ) ;
2026-01-22 02:13:35 +05:30
2026-03-06 19:35:21 +05:30
// Client-side filter: source type
2026-03-06 17:25:07 +05:30
const matchesSourceFilter = useCallback (
( item : InboxItem ) : boolean = > {
if ( ! selectedSource ) return true ;
if ( selectedSource . startsWith ( "connector:" ) ) {
const connectorType = selectedSource . slice ( "connector:" . length ) ;
return (
item . type === "connector_indexing" &&
isConnectorIndexingMetadata ( item . metadata ) &&
item . metadata . connector_type === connectorType
) ;
}
if ( selectedSource . startsWith ( "doctype:" ) ) {
const docType = selectedSource . slice ( "doctype:" . length ) ;
return (
item . type === "document_processing" &&
isDocumentProcessingMetadata ( item . metadata ) &&
item . metadata . document_type === docType
) ;
}
return true ;
} ,
[ selectedSource ]
) ;
2026-03-06 19:35:21 +05:30
// Client-side filter: unread / errors
2026-03-06 18:32:28 +05:30
const matchesActiveFilter = useCallback (
( item : InboxItem ) : boolean = > {
if ( activeFilter === "unread" ) return ! item . read ;
if ( activeFilter === "errors" ) {
if ( item . type === "page_limit_exceeded" ) return true ;
const meta = item . metadata as Record < string , unknown > | undefined ;
return typeof meta ? . status === "string" && meta . status === "failed" ;
}
return true ;
} ,
[ activeFilter ]
) ;
2026-04-02 01:45:18 +05:30
// Defer non-urgent list updates so the search input stays responsive.
// The deferred snapshot lags one render behind the live value intentionally.
const deferredTabItems = useDeferredValue ( activeSource . items ) ;
const deferredSearchItems = useDeferredValue ( searchResponse ? . items ? ? [ ] ) ;
2026-03-06 19:35:35 +05:30
// Two data paths: search mode (API) or default (per-tab data source)
2026-01-21 19:43:20 +05:30
const filteredItems = useMemo ( ( ) = > {
2026-04-02 01:45:18 +05:30
const tabItems : InboxItem [ ] = isSearchMode ? deferredSearchItems : deferredTabItems ;
2026-01-21 19:43:20 +05:30
2026-03-06 19:35:21 +05:30
let result = tabItems ;
if ( activeFilter !== "all" ) {
result = result . filter ( matchesActiveFilter ) ;
}
if ( activeTab === "status" && selectedSource ) {
result = result . filter ( matchesSourceFilter ) ;
}
return result ;
2026-03-06 17:25:07 +05:30
} , [
isSearchMode ,
2026-04-02 01:45:18 +05:30
deferredSearchItems ,
deferredTabItems ,
2026-03-06 17:25:07 +05:30
activeTab ,
2026-03-06 19:35:21 +05:30
activeFilter ,
selectedSource ,
matchesActiveFilter ,
matchesSourceFilter ,
2026-03-06 17:25:07 +05:30
] ) ;
2026-01-21 19:43:20 +05:30
2026-03-06 19:35:35 +05:30
// Infinite scroll — uses active tab's pagination
2026-01-22 11:27:45 +05:30
useEffect ( ( ) = > {
2026-03-22 00:01:31 +05:30
if ( ! activeSource . hasMore || activeSource . loadingMore || isSearchMode ) return ;
2026-01-22 11:27:45 +05:30
const observer = new IntersectionObserver (
( entries ) = > {
if ( entries [ 0 ] ? . isIntersecting ) {
2026-03-06 19:35:35 +05:30
activeSource . loadMore ( ) ;
2026-01-22 11:27:45 +05:30
}
} ,
{
2026-03-06 19:35:21 +05:30
root : null ,
rootMargin : "100px" ,
2026-01-22 11:27:45 +05:30
threshold : 0 ,
}
) ;
if ( prefetchTriggerRef . current ) {
observer . observe ( prefetchTriggerRef . current ) ;
}
return ( ) = > observer . disconnect ( ) ;
2026-03-22 00:01:31 +05:30
} , [ activeSource . hasMore , activeSource . loadingMore , activeSource . loadMore , isSearchMode ] ) ;
2026-01-21 19:43:20 +05:30
const handleItemClick = useCallback (
async ( item : InboxItem ) = > {
if ( ! item . read ) {
setMarkingAsReadId ( item . id ) ;
2026-03-06 19:35:35 +05:30
await activeSource . markAsRead ( item . id ) ;
2026-01-21 19:43:20 +05:30
setMarkingAsReadId ( null ) ;
}
if ( item . type === "new_mention" ) {
2026-01-22 18:32:25 +05:30
if ( isNewMentionMetadata ( item . metadata ) ) {
const searchSpaceId = item . search_space_id ;
const threadId = item . metadata . thread_id ;
const commentId = item . metadata . comment_id ;
if ( searchSpaceId && threadId ) {
2026-01-27 22:14:02 +05:30
if ( commentId ) {
setTargetCommentId ( commentId ) ;
}
2026-02-04 14:47:03 +02:00
const url = commentId
? ` /dashboard/ ${ searchSpaceId } /new-chat/ ${ threadId } ?commentId= ${ commentId } `
: ` /dashboard/ ${ searchSpaceId } /new-chat/ ${ threadId } ` ;
onOpenChange ( false ) ;
onCloseMobileSidebar ? . ( ) ;
router . push ( url ) ;
}
}
} else if ( item . type === "comment_reply" ) {
if ( isCommentReplyMetadata ( item . metadata ) ) {
const searchSpaceId = item . search_space_id ;
const threadId = item . metadata . thread_id ;
2026-02-04 15:18:25 +02:00
const replyId = item . metadata . reply_id ;
2026-01-27 22:14:02 +05:30
2026-02-04 14:47:03 +02:00
if ( searchSpaceId && threadId ) {
2026-02-04 15:18:25 +02:00
if ( replyId ) {
setTargetCommentId ( replyId ) ;
2026-02-04 14:47:03 +02:00
}
2026-02-04 15:18:25 +02:00
const url = replyId
? ` /dashboard/ ${ searchSpaceId } /new-chat/ ${ threadId } ?commentId= ${ replyId } `
2026-01-22 18:32:25 +05:30
: ` /dashboard/ ${ searchSpaceId } /new-chat/ ${ threadId } ` ;
onOpenChange ( false ) ;
onCloseMobileSidebar ? . ( ) ;
router . push ( url ) ;
}
2026-01-21 19:43:20 +05:30
}
2026-02-01 18:02:17 -08:00
} else if ( item . type === "page_limit_exceeded" ) {
if ( isPageLimitExceededMetadata ( item . metadata ) ) {
const actionUrl = item . metadata . action_url ;
if ( actionUrl ) {
onOpenChange ( false ) ;
onCloseMobileSidebar ? . ( ) ;
router . push ( actionUrl ) ;
}
}
2026-01-21 19:43:20 +05:30
}
} ,
2026-03-06 19:35:35 +05:30
[ activeSource . markAsRead , router , onOpenChange , onCloseMobileSidebar , setTargetCommentId ]
2026-01-21 19:43:20 +05:30
) ;
const handleMarkAllAsRead = useCallback ( async ( ) = > {
2026-03-06 19:35:35 +05:30
await Promise . all ( [ comments . markAllAsRead ( ) , status . markAllAsRead ( ) ] ) ;
} , [ comments . markAllAsRead , status . markAllAsRead ] ) ;
2026-01-21 19:43:20 +05:30
const handleClearSearch = useCallback ( ( ) = > {
setSearchQuery ( "" ) ;
} , [ ] ) ;
const formatTime = ( dateString : 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 ` ;
if ( diffHours < 24 ) return ` ${ diffHours } h ` ;
if ( diffDays < 7 ) return ` ${ diffDays } d ` ;
return ` ${ Math . floor ( diffDays / 7 ) } w ` ;
} catch {
return "now" ;
}
} ;
const getStatusIcon = ( item : InboxItem ) = > {
2026-02-04 14:47:03 +02:00
if ( item . type === "new_mention" || item . type === "comment_reply" ) {
const metadata =
item . type === "new_mention"
? isNewMentionMetadata ( item . metadata )
? item . metadata
: null
: isCommentReplyMetadata ( item . metadata )
? item . metadata
: null ;
if ( metadata ) {
2026-01-22 18:32:25 +05:30
return (
< Avatar className = "h-8 w-8" >
2026-02-04 14:47:03 +02:00
{ metadata . author_avatar_url && (
< AvatarImage src = { metadata . author_avatar_url } alt = { metadata . author_name || "User" } / >
) }
2026-01-22 18:32:25 +05:30
< AvatarFallback className = "text-[10px] bg-primary/10 text-primary" >
2026-02-04 14:47:03 +02:00
{ getInitials ( metadata . author_name , metadata . author_email ) }
2026-01-22 18:32:25 +05:30
< / AvatarFallback >
< / Avatar >
) ;
}
2026-01-21 19:43:20 +05:30
return (
< Avatar className = "h-8 w-8" >
< AvatarFallback className = "text-[10px] bg-primary/10 text-primary" >
2026-01-22 18:32:25 +05:30
{ getInitials ( null , null ) }
2026-01-21 19:43:20 +05:30
< / AvatarFallback >
< / Avatar >
) ;
}
2026-02-01 18:02:17 -08:00
if ( item . type === "page_limit_exceeded" ) {
return (
< div className = "h-8 w-8 flex items-center justify-center rounded-full bg-amber-500/10" >
< AlertTriangle className = "h-4 w-4 text-amber-500" / >
< / div >
) ;
}
2026-01-22 18:32:25 +05:30
const metadata = item . metadata as Record < string , unknown > ;
const status = typeof metadata ? . status === "string" ? metadata.status : undefined ;
2026-01-21 19:43:20 +05:30
switch ( status ) {
case "in_progress" :
return (
< div className = "h-8 w-8 flex items-center justify-center rounded-full bg-muted" >
2026-01-21 22:22:28 +05:30
< Spinner size = "sm" className = "text-foreground" / >
2026-01-21 19:43:20 +05:30
< / div >
) ;
case "completed" :
return (
< div className = "h-8 w-8 flex items-center justify-center rounded-full bg-green-500/10" >
< CheckCircle2 className = "h-4 w-4 text-green-500" / >
< / div >
) ;
case "failed" :
return (
< div className = "h-8 w-8 flex items-center justify-center rounded-full bg-red-500/10" >
< AlertCircle className = "h-4 w-4 text-red-500" / >
< / div >
) ;
default :
return (
< div className = "h-8 w-8 flex items-center justify-center rounded-full bg-muted" >
< History className = "h-4 w-4 text-muted-foreground" / >
< / div >
) ;
}
} ;
const getEmptyStateMessage = ( ) = > {
2026-02-04 17:17:33 +02:00
if ( activeTab === "comments" ) {
2026-01-21 19:43:20 +05:30
return {
2026-02-04 17:17:33 +02:00
title : t ( "no_comments" ) || "No comments" ,
hint : t ( "no_comments_hint" ) || "You'll see mentions and replies here" ,
2026-01-21 19:43:20 +05:30
} ;
}
return {
title : t ( "no_status_updates" ) || "No status updates" ,
hint : t ( "no_status_updates_hint" ) || "Document and connector updates will appear here" ,
} ;
} ;
2026-01-27 19:46:43 +05:30
if ( ! mounted ) return null ;
2026-03-06 19:35:35 +05:30
const isLoading = isSearchMode ? isSearchLoading : activeSource.loading ;
2026-03-06 19:35:21 +05:30
2026-03-22 00:01:31 +05:30
return (
2026-01-27 19:46:43 +05:30
< >
< div className = "shrink-0 p-4 pb-2 space-y-3" >
2026-01-28 02:14:36 +05:30
< div className = "flex items-center justify-between" >
< div className = "flex items-center gap-2" >
2026-02-06 18:59:52 +05:30
{ isMobile && (
< Button
variant = "ghost"
size = "icon"
className = "h-8 w-8 rounded-full"
onClick = { ( ) = > onOpenChange ( false ) }
>
< ChevronLeft className = "h-4 w-4 text-muted-foreground" / >
< span className = "sr-only" > { t ( "close" ) || "Close" } < / span >
< / Button >
) }
2026-01-28 02:14:36 +05:30
< h2 className = "text-lg font-semibold" > { t ( "inbox" ) || "Inbox" } < / h2 >
< / div >
< div className = "flex items-center gap-1" >
{ isMobile ? (
< >
2026-02-06 19:45:22 +05:30
< Button
variant = "ghost"
size = "icon"
2026-03-22 00:01:31 +05:30
className = "h-7 w-7 rounded-full"
2026-02-06 19:45:22 +05:30
onClick = { ( ) = > setFilterDrawerOpen ( true ) }
>
< ListFilter className = "h-4 w-4 text-muted-foreground" / >
< span className = "sr-only" > { t ( "filter" ) || "Filter" } < / span >
< / Button >
2026-01-28 02:14:36 +05:30
< Drawer
open = { filterDrawerOpen }
onOpenChange = { setFilterDrawerOpen }
shouldScaleBackground = { false }
>
< DrawerContent className = "max-h-[70vh] z-80" overlayClassName = "z-80" >
< DrawerHandle / >
< DrawerHeader className = "px-4 pb-3 pt-2" >
< DrawerTitle className = "flex items-center gap-2 text-base font-semibold" >
< ListFilter className = "size-5" / >
{ t ( "filter" ) || "Filter" }
< / DrawerTitle >
< / DrawerHeader >
< div className = "flex-1 overflow-y-auto p-4 space-y-4" >
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground/80 font-medium px-1" >
2026-01-22 04:02:32 +05:30
{ t ( "filter" ) || "Filter" }
2026-01-28 02:14:36 +05:30
< / p >
< div className = "space-y-1" >
< button
type = "button"
onClick = { ( ) = > {
setActiveFilter ( "all" ) ;
setFilterDrawerOpen ( false ) ;
} }
className = { cn (
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors" ,
activeFilter === "all"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
) }
>
< span className = "flex items-center gap-2" >
< Inbox className = "h-4 w-4" / >
< span > { t ( "all" ) || "All" } < / span >
< / span >
{ activeFilter === "all" && < Check className = "h-4 w-4" / > }
< / button >
< button
type = "button"
onClick = { ( ) = > {
setActiveFilter ( "unread" ) ;
setFilterDrawerOpen ( false ) ;
} }
className = { cn (
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors" ,
activeFilter === "unread"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
) }
>
< span className = "flex items-center gap-2" >
< BellDot className = "h-4 w-4" / >
< span > { t ( "unread" ) || "Unread" } < / span >
< / span >
{ activeFilter === "unread" && < Check className = "h-4 w-4" / > }
< / button >
2026-03-06 17:29:42 +05:30
{ activeTab === "status" && (
< button
type = "button"
onClick = { ( ) = > {
setActiveFilter ( "errors" ) ;
setFilterDrawerOpen ( false ) ;
} }
className = { cn (
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors" ,
activeFilter === "errors"
? "bg-primary/10 text-primary"
: "hover:bg-muted"
) }
>
< span className = "flex items-center gap-2" >
< AlertCircle className = "h-4 w-4" / >
< span > { t ( "errors_only" ) || "Errors only" } < / span >
< / span >
{ activeFilter === "errors" && < Check className = "h-4 w-4" / > }
< / button >
) }
2026-01-28 02:14:36 +05:30
< / div >
< / div >
2026-03-07 04:46:48 +05:30
{ activeTab === "status" && statusSourceOptions . length > 0 && (
< div className = "space-y-2" >
< p className = "text-xs text-muted-foreground/80 font-medium px-1" >
{ t ( "sources" ) || "Sources" }
< / p >
< div className = "space-y-1" >
2026-01-28 02:14:36 +05:30
< button
type = "button"
onClick = { ( ) = > {
2026-03-07 04:46:48 +05:30
setSelectedSource ( null ) ;
2026-01-28 02:14:36 +05:30
setFilterDrawerOpen ( false ) ;
} }
className = { cn (
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors" ,
2026-03-07 04:46:48 +05:30
selectedSource === null
2026-01-28 02:14:36 +05:30
? "bg-primary/10 text-primary"
: "hover:bg-muted"
) }
2026-01-22 04:02:32 +05:30
>
2026-01-22 17:46:44 +05:30
< span className = "flex items-center gap-2" >
2026-03-07 04:46:48 +05:30
< LayoutGrid className = "h-4 w-4" / >
< span > { t ( "all_sources" ) || "All sources" } < / span >
2026-01-22 17:46:44 +05:30
< / span >
2026-03-07 04:46:48 +05:30
{ selectedSource === null && < Check className = "h-4 w-4" / > }
2026-01-28 02:14:36 +05:30
< / button >
2026-03-07 04:46:48 +05:30
{ statusSourceOptions . map ( ( source ) = > (
< button
key = { source . key }
type = "button"
onClick = { ( ) = > {
setSelectedSource ( source . key ) ;
setFilterDrawerOpen ( false ) ;
} }
className = { cn (
"flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-sm transition-colors" ,
selectedSource === source . key
? "bg-primary/10 text-primary"
: "hover:bg-muted"
) }
>
< span className = "flex items-center gap-2" >
{ getConnectorIcon ( source . type , "h-4 w-4" ) }
< span > { source . displayName } < / span >
< / span >
{ selectedSource === source . key && < Check className = "h-4 w-4" / > }
< / button >
) ) }
< / div >
2026-01-28 02:14:36 +05:30
< / div >
2026-03-07 04:46:48 +05:30
) }
2026-01-28 02:14:36 +05:30
< / div >
< / DrawerContent >
< / Drawer >
< / >
) : (
< DropdownMenu
open = { openDropdown === "filter" }
onOpenChange = { ( isOpen ) = > setOpenDropdown ( isOpen ? "filter" : null ) }
>
< Tooltip >
< TooltipTrigger asChild >
< DropdownMenuTrigger asChild >
2026-03-22 00:01:31 +05:30
< Button variant = "ghost" size = "icon" className = "h-7 w-7 rounded-full" >
2026-01-28 02:14:36 +05:30
< ListFilter className = "h-4 w-4 text-muted-foreground" / >
< span className = "sr-only" > { t ( "filter" ) || "Filter" } < / span >
2026-01-21 23:07:08 +05:30
< / Button >
2026-01-28 02:14:36 +05:30
< / DropdownMenuTrigger >
< / TooltipTrigger >
< TooltipContent className = "z-80" > { t ( "filter" ) || "Filter" } < / TooltipContent >
< / Tooltip >
< DropdownMenuContent
align = "end"
2026-03-07 04:46:48 +05:30
className = { cn (
"z-80 select-none max-h-[60vh] overflow-hidden flex flex-col" ,
activeTab === "status" ? "w-52" : "w-44"
) }
2026-01-28 02:14:36 +05:30
>
< DropdownMenuLabel className = "text-xs text-muted-foreground/80 font-normal" >
{ t ( "filter" ) || "Filter" }
< / DropdownMenuLabel >
< DropdownMenuItem
onClick = { ( ) = > setActiveFilter ( "all" ) }
className = "flex items-center justify-between"
>
< span className = "flex items-center gap-2" >
< Inbox className = "h-4 w-4" / >
< span > { t ( "all" ) || "All" } < / span >
< / span >
{ activeFilter === "all" && < Check className = "h-4 w-4" / > }
< / DropdownMenuItem >
< DropdownMenuItem
onClick = { ( ) = > setActiveFilter ( "unread" ) }
className = "flex items-center justify-between"
>
< span className = "flex items-center gap-2" >
< BellDot className = "h-4 w-4" / >
< span > { t ( "unread" ) || "Unread" } < / span >
< / span >
{ activeFilter === "unread" && < Check className = "h-4 w-4" / > }
< / DropdownMenuItem >
2026-03-06 17:29:42 +05:30
{ activeTab === "status" && (
< DropdownMenuItem
onClick = { ( ) = > setActiveFilter ( "errors" ) }
className = "flex items-center justify-between"
>
< span className = "flex items-center gap-2" >
< AlertCircle className = "h-4 w-4" / >
< span > { t ( "errors_only" ) || "Errors only" } < / span >
< / span >
{ activeFilter === "errors" && < Check className = "h-4 w-4" / > }
< / DropdownMenuItem >
) }
2026-03-07 04:46:48 +05:30
{ activeTab === "status" && statusSourceOptions . length > 0 && (
< >
< DropdownMenuLabel className = "text-xs text-muted-foreground/80 font-normal mt-2" >
{ t ( "sources" ) || "Sources" }
< / DropdownMenuLabel >
< div
className = "relative max-h-[30vh] overflow-y-auto overflow-x-hidden -mb-1"
onScroll = { handleConnectorScroll }
style = { {
maskImage : ` linear-gradient(to bottom, ${ connectorScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ connectorScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
WebkitMaskImage : ` linear-gradient(to bottom, ${ connectorScrollPos === "top" ? "black" : "transparent" } , black 16px, black calc(100% - 16px), ${ connectorScrollPos === "bottom" ? "black" : "transparent" } ) ` ,
} }
2026-01-28 02:14:36 +05:30
>
< DropdownMenuItem
2026-03-07 04:46:48 +05:30
onClick = { ( ) = > setSelectedSource ( null ) }
2026-01-28 02:14:36 +05:30
className = "flex items-center justify-between"
2026-01-27 19:46:43 +05:30
>
2026-01-28 02:14:36 +05:30
< span className = "flex items-center gap-2" >
2026-03-07 04:46:48 +05:30
< LayoutGrid className = "h-4 w-4" / >
< span > { t ( "all_sources" ) || "All sources" } < / span >
2026-01-27 19:46:43 +05:30
< / span >
2026-03-07 04:46:48 +05:30
{ selectedSource === null && < Check className = "h-4 w-4" / > }
2026-01-28 02:14:36 +05:30
< / DropdownMenuItem >
2026-03-07 04:46:48 +05:30
{ statusSourceOptions . map ( ( source ) = > (
< DropdownMenuItem
key = { source . key }
onClick = { ( ) = > setSelectedSource ( source . key ) }
className = "flex items-center justify-between"
>
< span className = "flex items-center gap-2" >
{ getConnectorIcon ( source . type , "h-4 w-4" ) }
< span > { source . displayName } < / span >
< / span >
{ selectedSource === source . key && < Check className = "h-4 w-4" / > }
< / DropdownMenuItem >
) ) }
< / div >
< / >
) }
2026-01-28 02:14:36 +05:30
< / DropdownMenuContent >
< / DropdownMenu >
) }
2026-04-05 23:02:17 +05:30
< Tooltip >
< TooltipTrigger asChild >
< Button
variant = "ghost"
size = "icon"
className = "h-7 w-7 rounded-full"
onClick = { handleMarkAllAsRead }
disabled = { totalUnreadCount === 0 }
>
< CheckCheck className = "h-4 w-4 text-muted-foreground" / >
< span className = "sr-only" > { t ( "mark_all_read" ) || "Mark all as read" } < / span >
< / Button >
< / TooltipTrigger >
< TooltipContent className = "z-80" >
{ t ( "mark_all_read" ) || "Mark all as read" }
< / TooltipContent >
< / Tooltip >
2026-01-28 02:14:36 +05:30
< / div >
< / div >
< div className = "relative" >
< Search className = "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" / >
< Input
type = "text"
placeholder = { t ( "search_inbox" ) || "Search inbox" }
value = { searchQuery }
onChange = { ( e ) = > setSearchQuery ( e . target . value ) }
className = "pl-9 pr-8 h-9"
/ >
{ searchQuery && (
< Button
variant = "ghost"
size = "icon"
className = "absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6"
onClick = { handleClearSearch }
2026-01-21 19:43:20 +05:30
>
2026-01-28 02:14:36 +05:30
< X className = "h-3.5 w-3.5" / >
< span className = "sr-only" > { t ( "clear_search" ) || "Clear search" } < / span >
< / Button >
) }
< / div >
< / div >
< Tabs
value = { activeTab }
2026-03-06 17:29:42 +05:30
onValueChange = { ( value ) = > {
const tab = value as InboxTab ;
setActiveTab ( tab ) ;
if ( tab !== "status" && activeFilter === "errors" ) {
setActiveFilter ( "all" ) ;
}
} }
2026-03-21 22:42:17 +05:30
className = "shrink-0 mx-4 mt-2"
2026-01-28 02:14:36 +05:30
>
2026-03-21 22:42:17 +05:30
< TabsList stretch showBottomBorder size = "sm" >
< TabsTrigger value = "comments" >
< span className = "inline-flex items-center gap-1.5" >
2026-02-04 17:17:33 +02:00
< MessageSquare className = "h-4 w-4" / >
< span > { t ( "comments" ) || "Comments" } < / span >
2026-01-28 02:14:36 +05:30
< span className = "inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium" >
2026-03-06 19:35:35 +05:30
{ formatInboxCount ( comments . unreadCount ) }
2026-01-28 02:14:36 +05:30
< / span >
< / span >
< / TabsTrigger >
2026-03-21 22:42:17 +05:30
< TabsTrigger value = "status" >
< span className = "inline-flex items-center gap-1.5" >
2026-01-28 02:14:36 +05:30
< History className = "h-4 w-4" / >
< span > { t ( "status" ) || "Status" } < / span >
< span className = "inline-flex items-center justify-center min-w-5 h-5 px-1.5 rounded-full bg-primary/20 text-muted-foreground text-xs font-medium" >
2026-03-06 19:35:35 +05:30
{ formatInboxCount ( status . unreadCount ) }
2026-01-28 02:14:36 +05:30
< / span >
< / span >
< / TabsTrigger >
< / TabsList >
< / Tabs >
2026-02-06 18:22:19 +05:30
< div className = "flex-1 overflow-y-auto overflow-x-hidden p-2" >
2026-03-06 19:35:21 +05:30
{ isLoading ? (
2026-02-06 18:22:19 +05:30
< div className = "space-y-2" >
{ activeTab === "comments"
2026-03-21 22:42:17 +05:30
? [ 85 , 60 , 90 , 70 , 50 , 75 ] . map ( ( titleWidth ) = > (
2026-02-06 18:22:19 +05:30
< div
2026-03-21 22:42:17 +05:30
key = { ` skeleton-comment- ${ titleWidth } ` }
2026-02-06 18:22:19 +05:30
className = "flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
< Skeleton className = "h-8 w-8 rounded-full shrink-0" / >
< div className = "flex-1 min-w-0 space-y-2" >
< Skeleton className = "h-3 rounded" style = { { width : ` ${ titleWidth } % ` } } / >
< Skeleton className = "h-2.5 w-[70%] rounded" / >
< / div >
< Skeleton className = "h-3 w-6 shrink-0 rounded" / >
2026-02-06 16:58:38 +05:30
< / div >
2026-02-06 18:22:19 +05:30
) )
2026-03-21 22:42:17 +05:30
: [ 75 , 90 , 55 , 80 , 65 , 85 ] . map ( ( titleWidth ) = > (
2026-02-06 18:22:19 +05:30
< div
2026-03-21 22:42:17 +05:30
key = { ` skeleton-status- ${ titleWidth } ` }
2026-02-06 18:22:19 +05:30
className = "flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
< Skeleton className = "h-8 w-8 rounded-full shrink-0" / >
< div className = "flex-1 min-w-0 space-y-2" >
< Skeleton className = "h-3 rounded" style = { { width : ` ${ titleWidth } % ` } } / >
< Skeleton className = "h-2.5 w-[60%] rounded" / >
< / div >
< div className = "flex items-center gap-1.5 shrink-0" >
< Skeleton className = "h-3 w-6 rounded" / >
< Skeleton className = "h-2 w-2 rounded-full" / >
< / div >
2026-02-06 16:58:38 +05:30
< / div >
2026-02-06 18:22:19 +05:30
) ) }
< / div >
) : filteredItems . length > 0 ? (
< div className = "space-y-2" >
{ filteredItems . map ( ( item , index ) = > {
const isMarkingAsRead = markingAsReadId === item . id ;
const isPrefetchTrigger =
2026-03-06 19:35:35 +05:30
! isSearchMode && activeSource . hasMore && index === filteredItems . length - 5 ;
2026-01-28 02:14:36 +05:30
return (
< div
key = { item . id }
ref = { isPrefetchTrigger ? prefetchTriggerRef : undefined }
className = { cn (
"group flex items-center gap-3 rounded-lg px-3 py-3 text-sm h-[80px] overflow-hidden" ,
"hover:bg-accent hover:text-accent-foreground" ,
"transition-colors cursor-pointer" ,
isMarkingAsRead && "opacity-50 pointer-events-none"
) }
2026-04-02 02:26:55 +05:30
style = { { contentVisibility : "auto" , containIntrinsicSize : "0 80px" } }
2026-01-21 19:43:20 +05:30
>
2026-04-06 12:14:17 +05:30
{ activeTab === "status" ? (
< Tooltip delayDuration = { 600 } >
< TooltipTrigger asChild >
< button
type = "button"
onClick = { ( ) = > handleItemClick ( item ) }
disabled = { isMarkingAsRead }
className = "flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
>
< div className = "shrink-0" > { getStatusIcon ( item ) } < / div >
< div className = "flex-1 min-w-0 overflow-hidden" >
< p
className = { cn (
"text-xs font-medium line-clamp-2" ,
! item . read && "font-semibold"
) }
>
{ item . title }
< / p >
< p className = "text-[11px] text-muted-foreground line-clamp-2 mt-0.5" >
{ convertRenderedToDisplay ( item . message ) }
< / p >
< / div >
< / button >
< / TooltipTrigger >
< TooltipContent side = "bottom" align = "start" className = "max-w-[250px]" >
< p className = "font-medium" > { item . title } < / p >
< p className = "text-muted-foreground mt-1" >
{ convertRenderedToDisplay ( item . message ) }
< / p >
< / TooltipContent >
< / Tooltip >
) : (
< button
type = "button"
onClick = { ( ) = > handleItemClick ( item ) }
disabled = { isMarkingAsRead }
className = "flex items-center gap-3 flex-1 min-w-0 text-left overflow-hidden"
>
< div className = "shrink-0" > { getStatusIcon ( item ) } < / div >
< div className = "flex-1 min-w-0 overflow-hidden" >
< p
className = { cn (
"text-xs font-medium line-clamp-2" ,
! item . read && "font-semibold"
) }
>
{ item . title }
< / p >
< p className = "text-[11px] text-muted-foreground line-clamp-2 mt-0.5" >
{ convertRenderedToDisplay ( item . message ) }
< / p >
< / div >
< / button >
) }
2026-01-28 02:14:36 +05:30
< div className = "flex items-center justify-end gap-1.5 shrink-0 w-10" >
< span className = "text-[10px] text-muted-foreground" >
{ formatTime ( item . created_at ) }
< / span >
{ ! item . read && < span className = "h-2 w-2 rounded-full bg-blue-500 shrink-0" / > }
< / div >
2026-01-21 19:43:20 +05:30
< / div >
2026-01-28 02:14:36 +05:30
) ;
} ) }
2026-03-06 19:35:35 +05:30
{ ! isSearchMode && filteredItems . length < 5 && activeSource . hasMore && (
2026-02-06 18:22:19 +05:30
< div ref = { prefetchTriggerRef } className = "h-1" / >
) }
2026-03-06 19:35:35 +05:30
{ activeSource . loadingMore &&
2026-02-06 18:22:19 +05:30
( activeTab === "comments"
2026-03-21 22:42:17 +05:30
? [ 80 , 60 , 90 ] . map ( ( titleWidth ) = > (
2026-02-06 18:22:19 +05:30
< div
2026-03-21 22:42:17 +05:30
key = { ` loading-more-comment- ${ titleWidth } ` }
2026-02-06 18:22:19 +05:30
className = "flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
< Skeleton className = "h-8 w-8 rounded-full shrink-0" / >
< div className = "flex-1 min-w-0 space-y-2" >
< Skeleton className = "h-3 rounded" style = { { width : ` ${ titleWidth } % ` } } / >
< Skeleton className = "h-2.5 w-[70%] rounded" / >
< / div >
< Skeleton className = "h-3 w-6 shrink-0 rounded" / >
< / div >
) )
2026-03-21 22:42:17 +05:30
: [ 70 , 85 , 55 ] . map ( ( titleWidth ) = > (
2026-02-06 18:22:19 +05:30
< div
2026-03-21 22:42:17 +05:30
key = { ` loading-more-status- ${ titleWidth } ` }
2026-02-06 18:22:19 +05:30
className = "flex items-center gap-3 rounded-lg px-3 py-3 h-[80px]"
>
< Skeleton className = "h-8 w-8 rounded-full shrink-0" / >
< div className = "flex-1 min-w-0 space-y-2" >
< Skeleton className = "h-3 rounded" style = { { width : ` ${ titleWidth } % ` } } / >
< Skeleton className = "h-2.5 w-[60%] rounded" / >
< / div >
< div className = "flex items-center gap-1.5 shrink-0" >
< Skeleton className = "h-3 w-6 rounded" / >
< Skeleton className = "h-2 w-2 rounded-full" / >
< / div >
< / div >
) ) ) }
< / div >
) : isSearchMode ? (
< div className = "text-center py-8" >
< Search className = "h-12 w-12 mx-auto text-muted-foreground mb-3" / >
< p className = "text-sm text-muted-foreground" >
{ t ( "no_results_found" ) || "No results found" }
< / p >
< p className = "text-xs text-muted-foreground/70 mt-1" >
{ t ( "try_different_search" ) || "Try a different search term" }
< / p >
2026-01-28 02:14:36 +05:30
< / div >
) : (
< div className = "text-center py-8" >
2026-02-04 17:17:33 +02:00
{ activeTab === "comments" ? (
< MessageSquare className = "h-12 w-12 mx-auto text-muted-foreground mb-3" / >
2026-01-28 02:14:36 +05:30
) : (
< History className = "h-12 w-12 mx-auto text-muted-foreground mb-3" / >
) }
< p className = "text-sm text-muted-foreground" > { getEmptyStateMessage ( ) . title } < / p >
< p className = "text-xs text-muted-foreground/70 mt-1" > { getEmptyStateMessage ( ) . hint } < / p >
< / div >
) }
2026-01-27 19:46:43 +05:30
< / div >
< / >
) ;
2026-03-22 00:01:31 +05:30
}
export function InboxSidebar ( {
open ,
onOpenChange ,
comments ,
status ,
totalUnreadCount ,
onCloseMobileSidebar ,
} : InboxSidebarProps ) {
const t = useTranslations ( "sidebar" ) ;
2026-01-27 19:46:43 +05:30
return (
2026-02-09 11:41:55 -05:00
< SidebarSlideOutPanel open = { open } onOpenChange = { onOpenChange } ariaLabel = { t ( "inbox" ) || "Inbox" } >
2026-03-22 00:01:31 +05:30
< InboxSidebarContent
onOpenChange = { onOpenChange }
comments = { comments }
status = { status }
totalUnreadCount = { totalUnreadCount }
onCloseMobileSidebar = { onCloseMobileSidebar }
/ >
2026-02-09 11:41:55 -05:00
< / SidebarSlideOutPanel >
2026-01-21 19:43:20 +05:30
) ;
}