diff --git a/src/components/layout/AppHeader.tsx b/src/components/layout/AppHeader.tsx index 381d12d..986b78a 100644 --- a/src/components/layout/AppHeader.tsx +++ b/src/components/layout/AppHeader.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { NavLink, useLocation, useNavigate } from 'react-router-dom' import { motion, AnimatePresence } from 'framer-motion' @@ -12,9 +12,11 @@ import { RestoreDown, X, Bell, + Film, + Tv, } from '../../lib/icons' import { isTauri } from '../../lib/tauri' -import { jellyfinClient, getActivityLogApi } from '../../api/jellyfin' +import { jellyfinClient, getItemsApi } from '../../api/jellyfin' /** * The single chrome bar at the top of the in-app shell. Replaces the @@ -62,14 +64,28 @@ export default function AppHeader({ pinned, onTogglePin }: Props) { const [maximized, setMaximized] = useState(false) const [notifsOpen, setNotifsOpen] = useState(false) const [notifs, setNotifs] = useState([]) + const [lastOpenedAt, setLastOpenedAt] = useState(() => { + try { return localStorage.getItem('jf_bell_opened') } catch { return null } + }) + const notifsRef = useRef(null) - // Poll activity log for the notification bell + // Poll recently-added items for the notification bell useEffect(() => { const api = jellyfinClient.getApi() if (!api) return async function poll() { try { - const res = await getActivityLogApi(api!).getLogEntries({ limit: 8 }) + const auth = jellyfinClient.getAuthState() + if (!auth?.userId) return + const res = await getItemsApi(api!).getItems({ + userId: auth.userId, + sortBy: ['DateCreated'], + sortOrder: ['Descending'], + limit: 15, + recursive: true, + includeItemTypes: ['Movie', 'Series', 'Episode'], + fields: ['DateCreated', 'PrimaryImageAspectRatio'], + } as any) setNotifs(res.data.Items || []) } catch { /* ignore */ } } @@ -78,6 +94,25 @@ export default function AppHeader({ pinned, onTogglePin }: Props) { return () => clearInterval(id) }, []) + // Click outside to close + useEffect(() => { + if (!notifsOpen) return + function onDocClick(e: MouseEvent) { + if (!notifsRef.current?.contains(e.target as Node)) { + setNotifsOpen(false) + } + } + document.addEventListener('mousedown', onDocClick) + return () => document.removeEventListener('mousedown', onDocClick) + }, [notifsOpen]) + + const hasUnread = notifs.some(n => { + const created = n.DateCreated + if (!created) return false + if (!lastOpenedAt) return true + return created > lastOpenedAt + }) + // Subtle background fade in when the main pane is scrolled - same UX // affordance the old TopBar had, just on the unified header. useEffect(() => { @@ -247,15 +282,25 @@ export default function AppHeader({ pinned, onTogglePin }: Props) { - {/* Notification bell */} -
+ {/* Notification bell — new releases only */} +
@@ -266,26 +311,49 @@ export default function AppHeader({ pinned, onTogglePin }: Props) { animate={{ opacity: 1, y: 0, scale: 1 }} exit={{ opacity: 0, y: 6, scale: 0.98 }} transition={{ duration: 0.18 }} - className="absolute right-0 top-9 w-72 rounded-xl bg-glass-strong backdrop-blur-2xl border border-white/10 shadow-[0_20px_60px_-15px_rgba(0,0,0,0.85)] z-modal overflow-hidden" + className="absolute right-0 top-9 w-80 rounded-xl bg-glass-strong backdrop-blur-2xl border border-white/10 shadow-[0_20px_60px_-15px_rgba(0,0,0,0.85)] z-modal overflow-hidden" > -
-

Recent activity

+
+

New releases

+ {notifs.length > 0 && ( + {notifs.length} new + )}
-
+
{notifs.length === 0 ? ( -

No recent activity

+

Nothing new yet

) : ( - notifs.map((entry: any, i: number) => ( -
-

{entry.Name}

-

- {entry.DateCreated ? new Date(entry.DateCreated).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : ''} -

-
- )) + notifs.map((entry: any) => { + const isUnread = !lastOpenedAt || (entry.DateCreated && entry.DateCreated > lastOpenedAt) + return ( + + ) + }) )}