From b552ccd547f265091934ba67b3c3d1be8e53b40a Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 6 Mar 2026 12:40:50 +0100
Subject: [PATCH] chat: periodischer Scan fuer Teilnehmer-Nachrichten,
Chat-Panel-Reopen wenn geschlossen
Made-with: Cursor
---
src/bot/chatProcedure.ts | 72 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/src/bot/chatProcedure.ts b/src/bot/chatProcedure.ts
index 4302eba..dc3ea54 100644
--- a/src/bot/chatProcedure.ts
+++ b/src/bot/chatProcedure.ts
@@ -23,6 +23,7 @@ export class ChatProcedure {
private _lastMessageText: string = '';
private _botJoinedAtMs: number = 0;
private _recentMessageKeyTimestamps: Map = new Map();
+ private _scanIntervalId: ReturnType | 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();
+ 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 {
this._isSubscribed = false;
+ if (this._scanIntervalId) {
+ clearInterval(this._scanIntervalId);
+ this._scanIntervalId = null;
+ }
+
try {
await this._page.evaluate(() => {
if ((window as any).__chatObserver) {