feat(notifications): TODO 7 — draft decision toast + email + bell badge
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation } from '@tanstack/react-router'
|
||||
import { useDictionaries, useDictionaryDetail } from '@/api/queries'
|
||||
import { useDictionaries, useDictionaryDetail, useMyNotifications } from '@/api/queries'
|
||||
import { VersionBadge } from '@/components/version/VersionBadge'
|
||||
import { WhatsNewButton } from '@/components/version/WhatsNewButton'
|
||||
import { NotificationsDrawer } from '@/components/notifications/NotificationsDrawer'
|
||||
import { toast } from '@/ui'
|
||||
import { Bell, Menu } from 'lucide-react'
|
||||
|
||||
/**
|
||||
@@ -101,25 +104,72 @@ export function TopBar({ onMenuClick }: { onMenuClick?: () => void }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder notifications bell — будет показывать N unread сообщений
|
||||
* (webhook errors, draft reviews, system notices). Click → drawer/popup.
|
||||
* Сейчас static bell без counter — wire'ится когда backend notifications
|
||||
* stream появится.
|
||||
* Notifications bell с unread counter + drawer. TODO 7 wiring:
|
||||
* <ul>
|
||||
* <li>{@link useMyNotifications} с {@code unreadOnly=true} polls /me/notifications
|
||||
* каждые 30s (cheap query — фильтр backend-side).</li>
|
||||
* <li>Counter badge показывается если {@code totalUnread > 0}. Capped на "9+"
|
||||
* чтобы layout не плыл при бурном reviewer flow.</li>
|
||||
* <li>Click → opens {@link NotificationsDrawer} с full feed (unreadOnly=false).</li>
|
||||
* <li>Delta-detection: если {@code totalUnread} вырос между poll'ами и drawer
|
||||
* закрыт — fires `toast.info` с preview top-most new item. Reviewer'у
|
||||
* не нужно держать tab focused чтобы заметить approve.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>«Что нового» (WhatsNewButton) — adjacent button показывает release
|
||||
* notes, отделён от operational notifications (разные mental models).
|
||||
*/
|
||||
function NotificationsBell() {
|
||||
const { t } = useTranslation()
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
// Light query: только totalUnread + top item, used to drive badge + toast.
|
||||
const q = useMyNotifications(true, 5)
|
||||
const totalUnread = q.data?.totalUnread ?? 0
|
||||
|
||||
// Delta toast: detect новые notifications между polls. Hold prev count
|
||||
// в ref чтобы не trigger'ить toast на первый mount (initial load).
|
||||
const prevUnreadRef = useRef<number | null>(null)
|
||||
useEffect(() => {
|
||||
const prev = prevUnreadRef.current
|
||||
if (prev != null && totalUnread > prev && !drawerOpen) {
|
||||
const top = q.data?.items[0]
|
||||
toast(
|
||||
top?.payloadPreview ??
|
||||
t('notifications.newDefault', { defaultValue: 'Новое уведомление' }),
|
||||
{
|
||||
description: t('notifications.toastDescription', {
|
||||
defaultValue: 'Откройте колокольчик чтобы увидеть подробности.',
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
prevUnreadRef.current = totalUnread
|
||||
}, [totalUnread, drawerOpen, q.data, t])
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title={t('topbar.notifications', { defaultValue: 'Уведомления' })}
|
||||
aria-label={t('topbar.notifications', { defaultValue: 'Уведомления' })}
|
||||
className="size-7 rounded-md text-ink-2 hover:text-ink hover:bg-surface-2 inline-flex items-center justify-center transition-colors"
|
||||
>
|
||||
<Bell size={14} strokeWidth={1.75} />
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
title={t('topbar.notifications', { defaultValue: 'Уведомления' })}
|
||||
aria-label={t('topbar.notifications', { defaultValue: 'Уведомления' })}
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
className="relative size-7 rounded-md text-ink-2 hover:text-ink hover:bg-surface-2 inline-flex items-center justify-center transition-colors"
|
||||
>
|
||||
<Bell size={14} strokeWidth={1.75} />
|
||||
{totalUnread > 0 && (
|
||||
<span
|
||||
aria-label={t('notifications.unreadCount', {
|
||||
count: totalUnread,
|
||||
defaultValue: `${totalUnread} непрочитанных`,
|
||||
})}
|
||||
className="absolute -top-1 -right-1 min-w-[1rem] h-[1rem] px-1 rounded-full bg-pink text-on-accent text-[0.625rem] leading-none font-medium tabular-nums inline-flex items-center justify-center"
|
||||
>
|
||||
{totalUnread > 9 ? '9+' : totalUnread}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<NotificationsDrawer open={drawerOpen} onClose={() => setDrawerOpen(false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user