Files
dashboard/frontend/src/contexts/ChatContext.tsx
Matthias Hochmeister 3c72fe627f update
2026-03-16 14:44:15 +01:00

119 lines
3.7 KiB
TypeScript

import React, { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { nextcloudApi } from '../services/nextcloud';
import { useLayout } from './LayoutContext';
import { useNotification } from './NotificationContext';
import type { NextcloudConversation } from '../types/nextcloud.types';
interface ChatContextType {
rooms: NextcloudConversation[];
selectedRoomToken: string | null;
selectRoom: (token: string | null) => void;
connected: boolean;
loginName: string | null;
}
const ChatContext = createContext<ChatContextType | undefined>(undefined);
interface ChatProviderProps {
children: ReactNode;
}
export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
const [selectedRoomToken, setSelectedRoomToken] = useState<string | null>(null);
const { chatPanelOpen } = useLayout();
const { showNotificationToast } = useNotification();
const queryClient = useQueryClient();
const prevPanelOpenRef = useRef(chatPanelOpen);
const prevUnreadRef = useRef<Map<string, number>>(new Map());
// Invalidate rooms/connection when panel opens so data is fresh immediately
useEffect(() => {
if (chatPanelOpen && !prevPanelOpenRef.current) {
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'connection'] });
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
}
prevPanelOpenRef.current = chatPanelOpen;
}, [chatPanelOpen, queryClient]);
const { data: connData } = useQuery({
queryKey: ['nextcloud', 'connection'],
queryFn: () => nextcloudApi.getConversations(),
refetchInterval: chatPanelOpen ? 5000 : 15000,
retry: false,
});
const isConnected = connData?.connected ?? false;
const { data } = useQuery({
queryKey: ['nextcloud', 'rooms'],
queryFn: () => nextcloudApi.getRooms(),
refetchInterval: chatPanelOpen ? 5000 : 15000,
enabled: isConnected,
});
const rooms = data?.rooms ?? [];
const connected = data?.connected ?? false;
const loginName = data?.loginName ?? null;
const isInitializedRef = useRef(false);
// Reset initialization flag when disconnected
useEffect(() => {
if (!isConnected) {
isInitializedRef.current = false;
}
}, [isConnected]);
// Detect new unread messages while panel is closed and show toast
useEffect(() => {
if (!rooms.length) return;
const prev = prevUnreadRef.current;
if (!isInitializedRef.current) {
// First load (or after reconnect) — initialize without toasting
for (const room of rooms) {
prev.set(room.token, room.unreadMessages);
}
isInitializedRef.current = true;
return;
}
for (const room of rooms) {
const prevCount = prev.get(room.token) ?? 0;
if (!chatPanelOpen && room.unreadMessages > prevCount) {
showNotificationToast(room.displayName, 'info');
}
prev.set(room.token, room.unreadMessages);
}
// Prune entries for rooms no longer in the list
const currentTokens = new Set(rooms.map((r) => r.token));
for (const key of prev.keys()) {
if (!currentTokens.has(key)) prev.delete(key);
}
}, [rooms, chatPanelOpen, showNotificationToast]);
const selectRoom = useCallback((token: string | null) => {
setSelectedRoomToken(token);
}, []);
const value: ChatContextType = {
rooms,
selectedRoomToken,
selectRoom,
connected,
loginName,
};
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
};
export const useChat = (): ChatContextType => {
const context = useContext(ChatContext);
if (context === undefined) {
throw new Error('useChat must be used within a ChatProvider');
}
return context;
};