119 lines
3.7 KiB
TypeScript
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;
|
|
};
|