chat: periodischer Scan fuer Teilnehmer-Nachrichten, Chat-Panel-Reopen wenn geschlossen

Made-with: Cursor
This commit is contained in:
ValueOn AG 2026-03-06 12:40:50 +01:00
parent cb337ec377
commit b552ccd547

View file

@ -23,6 +23,7 @@ export class ChatProcedure {
private _lastMessageText: string = '';
private _botJoinedAtMs: number = 0;
private _recentMessageKeyTimestamps: Map<string, number> = new Map();
private _scanIntervalId: ReturnType<typeof setInterval> | null = null;
constructor(
page: Page,
@ -357,6 +358,72 @@ export class ChatProcedure {
});
this._logger.info(`Chat MutationObserver set up (target: ${chatObserverTarget})`);
this._startPeriodicChatScan();
}
/**
* Periodic scan as fallback: participant messages may load async and miss
* the MutationObserver (addedNode with empty placeholder, then content update).
* Also reopens the chat panel if it was closed (e.g. by user or Teams).
*/
private _startPeriodicChatScan(): void {
if (this._scanIntervalId) return;
const intervalMs = 2500;
this._scanIntervalId = setInterval(async () => {
if (!this._isSubscribed || !this._page) return;
try {
const chatPanelOpen = await this._page.evaluate(() => {
const chatBtn = document.querySelector('button[id="chat-button"], button[data-tid="chat-button"]') as HTMLElement | null;
if (chatBtn?.getAttribute('aria-pressed') === 'true') return true;
const messageList = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') as HTMLElement | null;
return !!(messageList && messageList.offsetHeight > 50);
});
if (!chatPanelOpen) {
this._logger.info('Chat panel closed, reopening');
await this._openChatPanel();
}
const knownKeys = Array.from(this._recentMessageKeyTimestamps.keys());
const messages = await this._page.evaluate((knownKeysArr: string[]) => {
const known = new Set(knownKeysArr);
const noisePatterns = [
'meeting ended', 'meeting started', 'was invited', 'left the chat',
'joined the meeting', 'left the meeting', 'doesn\'t have a teams account',
];
function isNoise(t: string) {
const l = t.toLowerCase();
return noisePatterns.some(p => l.includes(p));
}
const results: Array<{ speaker: string; text: string; timestamp: string; teamsTimestamp?: string; messageKey: string }> = [];
const seenThisScan = new Set<string>();
const container = document.querySelector('[data-tid="message-pane-list"], [data-tid="chat-pane-list"], [data-tid="chat-pane"]') || document.body;
const candidates = container.querySelectorAll('[data-tid="chat-message"], .fui-ChatMessage, [data-tid*="chat-pane-message"]');
for (const el of Array.from(candidates) as HTMLElement[]) {
const messageEl = el.closest?.('[data-tid="chat-message"], .fui-ChatMessage') || el;
let author = 'Unknown';
const authorEl = messageEl.querySelector('[data-tid="message-author"], [data-tid="message-author-name"], .fui-ChatMessage__author, [data-tid*="author"]');
if (authorEl?.textContent) author = authorEl.textContent.trim();
const bodyEl = messageEl.querySelector('[data-tid="message-body"], .fui-ChatMessage__body, [data-tid*="message-body"]');
const text = (bodyEl as HTMLElement)?.innerText?.trim() || '';
if (!text || text.length < 2 || isNoise(text)) continue;
const key = `${author}::${text}`;
if (known.has(key) || seenThisScan.has(key)) continue;
seenThisScan.add(key);
const timeEl = messageEl.querySelector('time[datetime], [data-tid*="timestamp"] time');
const ts = timeEl?.getAttribute?.('datetime') || new Date().toISOString();
results.push({ speaker: author, text, timestamp: ts, teamsTimestamp: ts, messageKey: key });
}
return results;
}, knownKeys);
for (const msg of messages) {
this._handleChatMessage(msg);
}
} catch {
// Page may be closed
}
}, intervalMs);
this._logger.info(`Chat periodic scan started (interval: ${intervalMs}ms)`);
}
/**
@ -477,6 +544,11 @@ export class ChatProcedure {
async unsubscribe(): Promise<void> {
this._isSubscribed = false;
if (this._scanIntervalId) {
clearInterval(this._scanIntervalId);
this._scanIntervalId = null;
}
try {
await this._page.evaluate(() => {
if ((window as any).__chatObserver) {