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(undefined); interface ChatProviderProps { children: ReactNode; } export const ChatProvider: React.FC = ({ children }) => { const [selectedRoomToken, setSelectedRoomToken] = useState(null); const { chatPanelOpen } = useLayout(); const { showNotificationToast } = useNotification(); const queryClient = useQueryClient(); const prevPanelOpenRef = useRef(chatPanelOpen); const prevUnreadRef = useRef>(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 {children}; }; export const useChat = (): ChatContextType => { const context = useContext(ChatContext); if (context === undefined) { throw new Error('useChat must be used within a ChatProvider'); } return context; };