resolve issues with new features

This commit is contained in:
Matthias Hochmeister
2026-03-12 17:51:57 +01:00
parent 34ca007f9b
commit 67b7d5ccd2
10 changed files with 138 additions and 68 deletions

View File

@@ -37,30 +37,29 @@ const ChatPanelInner: React.FC = () => {
});
const nextcloudUrl = externalLinks?.nextcloud;
// Keep a ref to rooms so the effect can access the latest list without
// re-running every time room data refreshes.
const roomsRef = React.useRef(rooms);
roomsRef.current = rooms;
// Dismiss internal notifications when panel opens
React.useEffect(() => {
if (chatPanelOpen) {
// Dismiss our internal notification-centre entries
notificationsApi.dismissByType('nextcloud_talk').then(() => {
queryClient.invalidateQueries({ queryKey: ['notifications'] });
queryClient.invalidateQueries({ queryKey: ['unreadNotificationCount'] });
}).catch(() => {});
// Also mark all unread rooms as read directly in Nextcloud so that
// Nextcloud's own notification badges clear as well.
roomsRef.current
.filter((r) => r.unreadMessages > 0)
.forEach((r) => {
nextcloudApi.markAsRead(r.token).catch(() => {});
});
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
}
if (!chatPanelOpen) return;
notificationsApi.dismissByType('nextcloud_talk').then(() => {
queryClient.invalidateQueries({ queryKey: ['notifications'] });
queryClient.invalidateQueries({ queryKey: ['unreadNotificationCount'] });
}).catch(() => {});
}, [chatPanelOpen, queryClient]);
// Mark unread rooms as read in Nextcloud whenever panel is open and rooms update
React.useEffect(() => {
if (!chatPanelOpen) return;
rooms.filter((r) => r.unreadMessages > 0).forEach((r) => {
nextcloudApi.markAsRead(r.token).catch(() => {});
});
}, [chatPanelOpen, rooms]);
// Mark the selected room as read when a conversation is opened
React.useEffect(() => {
if (!selectedRoomToken) return;
nextcloudApi.markAsRead(selectedRoomToken).catch(() => {});
}, [selectedRoomToken]);
if (!chatPanelOpen) {
return (
<Paper

View File

@@ -21,9 +21,10 @@ import {
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
import { notificationsApi } from '../../services/notifications';
import { useNotification } from '../../contexts/NotificationContext';
import type { Notification, NotificationSchwere } from '../../types/notification.types';
const POLL_INTERVAL_MS = 60_000; // 60 seconds
const POLL_INTERVAL_MS = 15_000; // 15 seconds
/**
* Only allow window.open for URLs whose origin matches the current app origin.
@@ -58,29 +59,50 @@ function formatRelative(iso: string): string {
const NotificationBell: React.FC = () => {
const navigate = useNavigate();
const { showNotificationToast } = useNotification();
const [unreadCount, setUnreadCount] = useState(0);
const [notifications, setNotifications] = useState<Notification[]>([]);
const [loadingList, setLoadingList] = useState(false);
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Track known notification IDs to detect new ones; null = not yet initialized
const knownIdsRef = useRef<Set<string> | null>(null);
const fetchUnreadCount = useCallback(async () => {
const fetchAndToastNew = useCallback(async () => {
try {
const count = await notificationsApi.getUnreadCount();
const data = await notificationsApi.getNotifications();
const unread = data.filter((n) => !n.gelesen);
const count = unread.length;
setUnreadCount(count);
if (knownIdsRef.current === null) {
// First load — initialize without toasting
knownIdsRef.current = new Set(data.map((n) => n.id));
return;
}
// Find notifications we haven't seen before
const newOnes = unread.filter((n) => !knownIdsRef.current!.has(n.id));
newOnes.forEach((n) => {
knownIdsRef.current!.add(n.id);
const severity = n.schwere === 'fehler' ? 'error' : n.schwere === 'warnung' ? 'warning' : 'info';
showNotificationToast(n.titel, severity);
});
// Also add all known IDs to avoid re-toasting on re-fetch
data.forEach((n) => knownIdsRef.current!.add(n.id));
} catch {
// non-critical
}
}, []);
}, [showNotificationToast]);
// Poll unread count every 60 seconds
// Poll for new notifications every 15 seconds
useEffect(() => {
fetchUnreadCount();
pollTimerRef.current = setInterval(fetchUnreadCount, POLL_INTERVAL_MS);
fetchAndToastNew();
pollTimerRef.current = setInterval(fetchAndToastNew, POLL_INTERVAL_MS);
return () => {
if (pollTimerRef.current) clearInterval(pollTimerRef.current);
};
}, [fetchUnreadCount]);
}, [fetchAndToastNew]);
const handleOpen = async (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
@@ -88,6 +110,12 @@ const NotificationBell: React.FC = () => {
try {
const data = await notificationsApi.getNotifications();
setNotifications(data);
// Mark all as known so we don't toast them again
if (knownIdsRef.current === null) {
knownIdsRef.current = new Set(data.map((n) => n.id));
} else {
data.forEach((n) => knownIdsRef.current!.add(n.id));
}
// Refresh count after loading full list
const count = await notificationsApi.getUnreadCount();
setUnreadCount(count);

View File

@@ -135,7 +135,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
const vehicleSubItems: SubItem[] = useMemo(
() =>
(vehicleList ?? []).map((v) => ({
text: v.kurzname ?? v.bezeichnung,
text: v.bezeichnung ?? v.kurzname,
path: `/fahrzeuge/${v.id}`,
})),
[vehicleList],
@@ -196,7 +196,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
>
<ListItemButton
selected={isActive}
onClick={() => handleNavigation(hasSubItems ? item.subItems![0].path : item.path)}
onClick={() => handleNavigation(item.path)}
aria-label={`Zu ${item.text} navigieren`}
sx={{
justifyContent: sidebarCollapsed ? 'center' : 'initial',