/* 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 { useQuery } from '@tanstack/react-query' import { useState, useMemo } from 'react' import { useStatus } from '@/hooks/use-status' import { getNotice } from '@/lib/api' import { useNotificationStore } from '@/stores/notification-store' function hashString(input: string): string { let hash = 0 if (!input) return '0' for (let i = 0; i < input.length; i += 1) { const chr = input.charCodeAt(i) hash = (hash << 5) - hash + chr hash |= 0 } return hash.toString(36) } /** * Generate a unique key for an announcement * Prefer backend id, fall back to a content hash so edits register */ function getAnnouncementKey(item: Record): string { if (!item) return '' if (item.id !== undefined && item.id !== null) { return `id:${item.id}` } const fingerprint = JSON.stringify({ publishDate: (item?.publishDate as string) || '', content: ((item?.content as string) || '').trim(), extra: ((item?.extra as string) || '').trim(), type: (item?.type as string) || '', title: ((item?.title as string) || '').trim(), link: ((item?.link as string) || '').trim(), }) return `hash:${hashString(fingerprint)}` } /** * Hook to manage notifications (Notice + Announcements) * Provides unread counts and read status management */ export function useNotifications() { const [popoverOpen, setPopoverOpen] = useState(false) const [activeTab, setActiveTab] = useState<'notice' | 'announcements'>( 'notice' ) // Fetch Notice from API const { data: noticeResponse, isLoading: noticeLoading, refetch: refetchNotice, } = useQuery({ queryKey: ['notice'], queryFn: getNotice, staleTime: 1000 * 60 * 5, // 5 minutes }) // Fetch Announcements from status const { status, loading: statusLoading } = useStatus() const announcementsEnabled = status?.announcements_enabled ?? false // eslint-disable-next-line react-hooks/exhaustive-deps const announcements: Record[] = announcementsEnabled ? ((status?.announcements || []) as Record[]).slice(0, 20) : [] // Notification store const { lastReadNotice, markNoticeRead, markAnnouncementsRead, isAnnouncementRead, } = useNotificationStore() // Extract notice content const noticeContent = noticeResponse?.success ? (noticeResponse.data || '').trim() : '' // Calculate unread counts const unreadCounts = useMemo(() => { const noticeUnread = noticeContent && noticeContent !== lastReadNotice ? 1 : 0 const announcementsUnread = announcements.filter( (item: Record) => { const key = getAnnouncementKey(item) return !isAnnouncementRead(key) } ).length return { notice: noticeUnread, announcements: announcementsUnread, total: noticeUnread + announcementsUnread, } }, [noticeContent, lastReadNotice, announcements, isAnnouncementRead]) const markAnnouncementsAsRead = () => { if (announcements.length > 0) { const allKeys = announcements.map((item: Record) => getAnnouncementKey(item) ) markAnnouncementsRead(allKeys) } } // Handle popover open const handleOpenPopover = (tab?: 'notice' | 'announcements') => { const nextTab = tab || activeTab // Mark currently visible content as read when opening the notification center if (noticeContent) { markNoticeRead(noticeContent) } if (nextTab === 'announcements') { markAnnouncementsAsRead() } setActiveTab(nextTab) setPopoverOpen(true) } const handlePopoverOpenChange = (open: boolean) => { if (open) { handleOpenPopover(activeTab) return } setPopoverOpen(false) } // Handle tab change - mark announcements as read when switching to that tab const handleTabChange = (tab: 'notice' | 'announcements') => { setActiveTab(tab) if (tab === 'announcements') { markAnnouncementsAsRead() } } return { // Data notice: noticeContent, announcements, loading: noticeLoading || statusLoading, // Unread counts unreadCount: unreadCounts.total, unreadNoticeCount: unreadCounts.notice, unreadAnnouncementsCount: unreadCounts.announcements, // Popover state popoverOpen, setPopoverOpen: handlePopoverOpenChange, activeTab, setActiveTab: handleTabChange, // Actions openPopover: handleOpenPopover, closePopover: () => setPopoverOpen(false), refetchNotice, } }