update
This commit is contained in:
@@ -16,6 +16,7 @@ interface ChatMessageProps {
|
||||
isOwnMessage: boolean;
|
||||
onReplyClick?: (message: NextcloudMessage) => void;
|
||||
onReactionToggled?: (messageId: number) => void;
|
||||
reactionsOverride?: { reactions: Record<string, number>; reactionsSelf: string[] };
|
||||
}
|
||||
|
||||
function hasFileParams(params?: Record<string, any>): boolean {
|
||||
@@ -27,7 +28,7 @@ function hasPollParam(params?: Record<string, any>): boolean {
|
||||
return params?.object?.type === 'talk-poll';
|
||||
}
|
||||
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, onReplyClick, onReactionToggled }) => {
|
||||
const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, onReplyClick, onReactionToggled, reactionsOverride }) => {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -79,8 +80,8 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isOwnMessage, onRepl
|
||||
|
||||
const textBeforeFile = isFileMessage ? message.message.split('{file}')[0].trim() : null;
|
||||
|
||||
const reactions = message.reactions ?? {};
|
||||
const reactionsSelf = message.reactionsSelf ?? [];
|
||||
const reactions = reactionsOverride?.reactions ?? message.reactions ?? {};
|
||||
const reactionsSelf = reactionsOverride?.reactionsSelf ?? message.reactionsSelf ?? [];
|
||||
const hasReactions = Object.keys(reactions).length > 0;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
@@ -33,6 +33,25 @@ const ChatMessageView: React.FC = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const lastMsgIdRef = useRef<number>(0);
|
||||
|
||||
// reactions override map: messageId → { reactions, reactionsSelf }
|
||||
const [reactionsMap, setReactionsMap] = useState<Map<number, { reactions: Record<string, number>; reactionsSelf: string[] }>>(new Map());
|
||||
|
||||
const fetchReactions = useCallback(async (msgId: number) => {
|
||||
if (!selectedRoomToken) return;
|
||||
try {
|
||||
const detailed = await nextcloudApi.getReactions(selectedRoomToken, msgId);
|
||||
const counts: Record<string, number> = {};
|
||||
const self: string[] = [];
|
||||
for (const [emoji, actors] of Object.entries(detailed)) {
|
||||
counts[emoji] = actors.length;
|
||||
if (actors.some((a) => a.actorId === loginName)) self.push(emoji);
|
||||
}
|
||||
setReactionsMap((prev) => new Map(prev).set(msgId, { reactions: counts, reactionsSelf: self }));
|
||||
} catch {
|
||||
// ignore — reactions are best-effort
|
||||
}
|
||||
}, [selectedRoomToken, loginName]);
|
||||
|
||||
// Initial fetch + long-poll loop for near-instant message delivery
|
||||
useEffect(() => {
|
||||
if (!selectedRoomToken || !chatPanelOpen) return;
|
||||
@@ -43,6 +62,7 @@ const ChatMessageView: React.FC = () => {
|
||||
async function run() {
|
||||
setIsLoading(true);
|
||||
setMessages([]);
|
||||
setReactionsMap(new Map());
|
||||
lastMsgIdRef.current = 0;
|
||||
|
||||
// Step 1: Initial fetch — Nextcloud returns newest-first, so reverse for chronological display
|
||||
@@ -50,7 +70,11 @@ const ChatMessageView: React.FC = () => {
|
||||
try {
|
||||
const initial = await nextcloudApi.getMessages(selectedRoomToken!, undefined, currentAbort.signal);
|
||||
if (cancelled) return;
|
||||
setMessages([...initial].reverse());
|
||||
// Filter reaction system messages — these appear as chat events but should not be shown
|
||||
const displayable = initial.filter(
|
||||
(m) => m.systemMessage !== 'reaction' && m.systemMessage !== 'reaction_deleted',
|
||||
);
|
||||
setMessages([...displayable].reverse());
|
||||
lastMsgIdRef.current = initial.length > 0 ? Math.max(...initial.map(m => m.id)) : 0;
|
||||
setIsLoading(false);
|
||||
} catch {
|
||||
@@ -69,10 +93,29 @@ const ChatMessageView: React.FC = () => {
|
||||
);
|
||||
if (cancelled) break;
|
||||
if (newMsgs.length > 0) {
|
||||
// Long-poll returns ascending order — append directly
|
||||
setMessages(prev => [...prev, ...newMsgs]);
|
||||
// Separate reaction events from real messages
|
||||
const reactionEvents = newMsgs.filter(
|
||||
(m) => m.systemMessage === 'reaction' || m.systemMessage === 'reaction_deleted',
|
||||
);
|
||||
const displayMsgs = newMsgs.filter(
|
||||
(m) => m.systemMessage !== 'reaction' && m.systemMessage !== 'reaction_deleted',
|
||||
);
|
||||
|
||||
// Advance cursor using ALL messages (including reaction events)
|
||||
lastMsgIdRef.current = Math.max(...newMsgs.map(m => m.id));
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
|
||||
// Refresh reaction counts for affected messages
|
||||
const affectedIds = new Set<number>();
|
||||
for (const rm of reactionEvents) {
|
||||
const parentId = parseInt(rm.messageParameters?.message?.id ?? '0', 10);
|
||||
if (parentId > 0) affectedIds.add(parentId);
|
||||
}
|
||||
affectedIds.forEach((id) => fetchReactions(id));
|
||||
|
||||
if (displayMsgs.length > 0) {
|
||||
setMessages(prev => [...prev, ...displayMsgs]);
|
||||
queryClient.invalidateQueries({ queryKey: ['nextcloud', 'rooms'] });
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (cancelled) break;
|
||||
@@ -88,7 +131,7 @@ const ChatMessageView: React.FC = () => {
|
||||
cancelled = true;
|
||||
currentAbort?.abort();
|
||||
};
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient]);
|
||||
}, [selectedRoomToken, chatPanelOpen, queryClient, fetchReactions]);
|
||||
|
||||
const room = rooms.find((r) => r.token === selectedRoomToken);
|
||||
|
||||
@@ -128,8 +171,8 @@ const ChatMessageView: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReactionToggled = (_messageId: number) => {
|
||||
// reactions are stored in message objects; for now just mark rooms as potentially changed
|
||||
const handleReactionToggled = (messageId: number) => {
|
||||
fetchReactions(messageId);
|
||||
};
|
||||
|
||||
const handleUploadFile = async (file: File) => {
|
||||
@@ -212,6 +255,7 @@ const ChatMessageView: React.FC = () => {
|
||||
isOwnMessage={msg.actorType === 'users' && msg.actorId === loginName}
|
||||
onReplyClick={setReplyToMessage}
|
||||
onReactionToggled={handleReactionToggled}
|
||||
reactionsOverride={reactionsMap.get(msg.id)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -167,7 +167,9 @@ async function scrapeMembers(frame: Frame): Promise<FdiskMember[]> {
|
||||
const cells = Array.from(tr.querySelectorAll('td'));
|
||||
const val = (i: number) => {
|
||||
const a = cells[i]?.querySelector('a');
|
||||
return (a?.getAttribute('title') ?? cells[i]?.textContent ?? '').trim();
|
||||
const title = a?.getAttribute('title')?.trim();
|
||||
// Use title only if non-empty; otherwise fall back to textContent
|
||||
return (title || cells[i]?.textContent || '').trim();
|
||||
};
|
||||
const href = (tr.querySelector('a') as HTMLAnchorElement | null)?.href ?? null;
|
||||
return {
|
||||
@@ -186,10 +188,16 @@ async function scrapeMembers(frame: Frame): Promise<FdiskMember[]> {
|
||||
);
|
||||
|
||||
log(`Parsed ${rows.length} rows from member table`);
|
||||
for (const row of rows) {
|
||||
log(` Row: StNr="${row.standesbuchNr}" Vorname="${row.vorname}" Zuname="${row.zuname}" Status="${row.status}"`);
|
||||
}
|
||||
|
||||
const members: FdiskMember[] = [];
|
||||
for (const row of rows) {
|
||||
if (!row.standesbuchNr || !row.vorname || !row.zuname) continue;
|
||||
if (!row.standesbuchNr || !row.vorname || !row.zuname) {
|
||||
log(` SKIP: StNr="${row.standesbuchNr}" Vorname="${row.vorname}" Zuname="${row.zuname}" — missing required field`);
|
||||
continue;
|
||||
}
|
||||
const abmeldedatum = parseDate(row.abmeldedatum);
|
||||
members.push({
|
||||
standesbuchNr: row.standesbuchNr,
|
||||
|
||||
Reference in New Issue
Block a user