update nextcloud handling
This commit is contained in:
@@ -3,13 +3,17 @@ import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { nextcloudApi } from '../../services/nextcloud';
|
||||
import { useChat } from '../../contexts/ChatContext';
|
||||
import { useLayout } from '../../contexts/LayoutContext';
|
||||
import ChatMessage from './ChatMessage';
|
||||
import type { NextcloudMessage } from '../../types/nextcloud.types';
|
||||
|
||||
const LONG_POLL_TIMEOUT = 25;
|
||||
|
||||
const ChatMessageView: React.FC = () => {
|
||||
const { selectedRoomToken, selectRoom, rooms, loginName } = useChat();
|
||||
@@ -17,41 +21,84 @@ const ChatMessageView: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [input, setInput] = useState('');
|
||||
const prevActiveRef = useRef(false);
|
||||
const [messages, setMessages] = useState<NextcloudMessage[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const lastMsgIdRef = useRef<number>(0);
|
||||
|
||||
// Invalidate messages immediately when this room + panel becomes active
|
||||
// Initial fetch + long-poll loop for near-instant message delivery
|
||||
useEffect(() => {
|
||||
const active = !!selectedRoomToken && chatPanelOpen;
|
||||
if (active && !prevActiveRef.current) {
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'messages', selectedRoomToken] });
|
||||
if (!selectedRoomToken || !chatPanelOpen) return;
|
||||
|
||||
let cancelled = false;
|
||||
let currentAbort: AbortController | null = null;
|
||||
|
||||
async function run() {
|
||||
setIsLoading(true);
|
||||
setMessages([]);
|
||||
lastMsgIdRef.current = 0;
|
||||
|
||||
// Step 1: Initial fetch — Nextcloud returns newest-first, so reverse for chronological display
|
||||
currentAbort = new AbortController();
|
||||
try {
|
||||
const initial = await nextcloudApi.getMessages(selectedRoomToken!, undefined, currentAbort.signal);
|
||||
if (cancelled) return;
|
||||
setMessages([...initial].reverse());
|
||||
lastMsgIdRef.current = initial.length > 0 ? Math.max(...initial.map(m => m.id)) : 0;
|
||||
setIsLoading(false);
|
||||
} catch {
|
||||
if (cancelled) return;
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
// Step 2: Long-poll loop — blocks on server until new messages arrive
|
||||
while (!cancelled) {
|
||||
currentAbort = new AbortController();
|
||||
try {
|
||||
const newMsgs = await nextcloudApi.getMessages(
|
||||
selectedRoomToken!,
|
||||
{ lookIntoFuture: true, lastKnownMessageId: lastMsgIdRef.current, timeout: LONG_POLL_TIMEOUT },
|
||||
currentAbort.signal,
|
||||
);
|
||||
if (cancelled) break;
|
||||
if (newMsgs.length > 0) {
|
||||
// Long-poll returns ascending order — append directly
|
||||
setMessages(prev => [...prev, ...newMsgs]);
|
||||
lastMsgIdRef.current = Math.max(...newMsgs.map(m => m.id));
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (cancelled) break;
|
||||
if (err?.code === 'ERR_CANCELED' || err?.name === 'CanceledError') break;
|
||||
// Brief backoff before retrying on error
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
}
|
||||
prevActiveRef.current = active;
|
||||
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
currentAbort?.abort();
|
||||
};
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient]);
|
||||
|
||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||
|
||||
const { data: messages } = useQuery({
|
||||
queryKey: ['nextcloud', 'messages', selectedRoomToken],
|
||||
queryFn: () => nextcloudApi.getMessages(selectedRoomToken!),
|
||||
enabled: !!selectedRoomToken && chatPanelOpen,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: (message: string) => nextcloudApi.sendMessage(selectedRoomToken!, message),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'messages', selectedRoomToken] });
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
},
|
||||
});
|
||||
|
||||
// Mark room as read while viewing messages
|
||||
useEffect(() => {
|
||||
if (selectedRoomToken && chatPanelOpen) {
|
||||
nextcloudApi.markAsRead(selectedRoomToken).then(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
}).catch(() => {});
|
||||
}
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient, messages?.length]);
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient, messages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
@@ -93,13 +140,19 @@ const ChatMessageView: React.FC = () => {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, overflow: 'auto', py: 1 }}>
|
||||
{[...(messages ?? [])].reverse().map((msg) => (
|
||||
<ChatMessage
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
||||
/>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 2 }}>
|
||||
<CircularProgress size={24} />
|
||||
</Box>
|
||||
) : (
|
||||
messages.map((msg) => (
|
||||
<ChatMessage
|
||||
key={msg.id}
|
||||
message={msg}
|
||||
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</Box>
|
||||
|
||||
@@ -127,3 +180,4 @@ const ChatMessageView: React.FC = () => {
|
||||
};
|
||||
|
||||
export default ChatMessageView;
|
||||
|
||||
|
||||
@@ -37,9 +37,23 @@ export const nextcloudApi = {
|
||||
.then((r) => r.data.data);
|
||||
},
|
||||
|
||||
getMessages(token: string): Promise<NextcloudMessage[]> {
|
||||
getMessages(
|
||||
token: string,
|
||||
options?: { lookIntoFuture?: boolean; lastKnownMessageId?: number; timeout?: number },
|
||||
signal?: AbortSignal,
|
||||
): Promise<NextcloudMessage[]> {
|
||||
const params: Record<string, any> = {};
|
||||
if (options?.lookIntoFuture) {
|
||||
params.lookIntoFuture = '1';
|
||||
params.lastKnownMessageId = options.lastKnownMessageId ?? 0;
|
||||
params.timeout = options.timeout ?? 25;
|
||||
}
|
||||
return api
|
||||
.get<ApiResponse<NextcloudMessage[]>>(`/api/nextcloud/talk/rooms/${encodeURIComponent(token)}/messages`)
|
||||
.get<ApiResponse<NextcloudMessage[]>>(`/api/nextcloud/talk/rooms/${encodeURIComponent(token)}/messages`, {
|
||||
params,
|
||||
signal,
|
||||
...(options?.lookIntoFuture && { timeout: ((options.timeout ?? 25) + 7) * 1000 }),
|
||||
})
|
||||
.then((r) => r.data.data);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user