/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import type { TFunction } from 'i18next' import { Bell, Megaphone } from 'lucide-react' import { useTranslation } from 'react-i18next' import { getAnnouncementColorClass } from '@/lib/colors' import { formatDateTimeObject } from '@/lib/time' import { cn } from '@/lib/utils' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '@/components/ui/empty' import { Markdown } from '@/components/ui/markdown' import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger, } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import { Separator } from '@/components/ui/separator' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' interface AnnouncementItem { type?: string content?: string extra?: string publishDate?: string | Date } interface NotificationPopoverProps { open: boolean onOpenChange: (open: boolean) => void unreadCount: number activeTab: 'notice' | 'announcements' onTabChange: (tab: 'notice' | 'announcements') => void notice: string announcements: AnnouncementItem[] loading: boolean className?: string } /** * Get relative time string from a date */ function getRelativeTime(publishDate: string | Date, t: TFunction): string { if (!publishDate) return '' const now = new Date() const pubDate = new Date(publishDate) // If invalid date, return original string if (isNaN(pubDate.getTime())) return typeof publishDate === 'string' ? publishDate : '' const diffMs = now.getTime() - pubDate.getTime() const diffSeconds = Math.floor(diffMs / 1000) const diffMinutes = Math.floor(diffSeconds / 60) const diffHours = Math.floor(diffMinutes / 60) const diffDays = Math.floor(diffHours / 24) const diffWeeks = Math.floor(diffDays / 7) const diffMonths = Math.floor(diffDays / 30) const diffYears = Math.floor(diffDays / 365) // If future time, show specific date if (diffMs < 0) return formatDateTimeObject(pubDate) // Return relative time based on difference if (diffSeconds < 60) return t('Just now') if (diffMinutes < 60) return diffMinutes === 1 ? t('1 minute ago') : t('{{count}} minutes ago', { count: diffMinutes }) if (diffHours < 24) return diffHours === 1 ? t('1 hour ago') : t('{{count}} hours ago', { count: diffHours }) if (diffDays < 7) return diffDays === 1 ? t('1 day ago') : t('{{count}} days ago', { count: diffDays }) if (diffWeeks < 4) return diffWeeks === 1 ? t('1 week ago') : t('{{count}} weeks ago', { count: diffWeeks }) if (diffMonths < 12) return diffMonths === 1 ? t('1 month ago') : t('{{count}} months ago', { count: diffMonths }) if (diffYears < 2) return t('1 year ago') // Over 2 years, show specific date return formatDateTimeObject(pubDate) } /** * Announcement status dot indicator */ function AnnouncementDot({ type }: { type?: string }) { return ( ) } /** * Empty state component */ function EmptyState({ icon, title, description, }: { icon: React.ReactNode title: string description?: string }) { return ( {icon} {title} {description ? ( {description} ) : null} ) } /** * Notice tab content */ function NoticeContent({ notice, loading, t, }: { notice: string loading: boolean t: TFunction }) { if (loading) { return ( } title={t('Loading...')} description={t('Latest platform updates and notices')} /> ) } if (!notice) { return ( } title={t('No announcements at this time')} /> ) } return ( {notice} ) } /** * Announcements tab content */ function AnnouncementsContent({ announcements, loading, t, }: { announcements: AnnouncementItem[] loading: boolean t: TFunction }) { if (loading) { return ( } title={t('Loading...')} description={t('Latest platform updates and notices')} /> ) } if (announcements.length === 0) { return ( } title={t('No system announcements')} /> ) } return (
{announcements.map((item, idx) => { const publishDate = item.publishDate ? new Date(item.publishDate) : null const relativeTime = publishDate ? getRelativeTime(publishDate, t) : '' const absoluteTime = publishDate ? formatDateTimeObject(publishDate) : '' return (
{item.content || ''}
{item.extra ? (
{item.extra}
) : null} {absoluteTime ? (
{relativeTime ? `${relativeTime} • ` : null} {absoluteTime}
) : null}
{idx < announcements.length - 1 ? : null}
) })}
) } /** * Notification popover with Notice and Announcements tabs */ export function NotificationPopover({ open, onOpenChange, unreadCount, activeTab, onTabChange, notice, announcements, loading, className, }: NotificationPopoverProps) { const { t } = useTranslation() return ( } > {unreadCount > 0 ? ( {unreadCount > 99 ? '99+' : unreadCount} ) : null} {t('System Announcements')}

{t('Latest platform updates and notices')}

void} > {t('Notice')} {t('Timeline')}
) }