From d0c13b1a493acbccf18b055b3536be31c9317170 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 12 Sep 2026 20:28:42 -0400 Subject: [PATCH] fix(rooms): Room Insights refreshes on new timeline events (throttled) with a Refresh button Fixes #83 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- .../features/room-settings/RoomInsights.tsx | 80 ++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/src/app/features/room-settings/RoomInsights.tsx b/src/app/features/room-settings/RoomInsights.tsx index c25c7c09f..a60667b04 100644 --- a/src/app/features/room-settings/RoomInsights.tsx +++ b/src/app/features/room-settings/RoomInsights.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Avatar, Box, Icon, IconButton, Icons, IconSrc, Scroll, Text, color, config } from 'folds'; -import { EventType } from 'matrix-js-sdk'; +import { EventType, MatrixEvent, Room, RoomEvent } from 'matrix-js-sdk'; import { Page, PageContent, PageHeader } from '../../components/page'; import { SequenceCard } from '../../components/sequence-card'; import { useRoom } from '../../hooks/useRoom'; @@ -20,6 +20,15 @@ function formatDate(ts: number): string { }); } +function formatUpdatedAt(ts: number): string { + return new Date(ts).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); +} + +// Throttle window for re-computing stats on new timeline events - avoids +// re-running every heatmap/list computation on every single incoming message +// during a burst. +const RECOMPUTE_THROTTLE_MS = 2000; + // ── Section header ──────────────────────────────────────────────────────────── function SectionHeader({ label }: { label: string }) { @@ -69,6 +78,52 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { const room = useRoom(); const useAuthentication = useMediaAuthentication(); + const [lastUpdated, setLastUpdated] = useState(() => Date.now()); + // Bumped to force the stats useMemo below to re-run; the value itself is unused. + const [recomputeTick, setRecomputeTick] = useState(0); + + const recomputeNow = useCallback(() => { + setRecomputeTick((n) => n + 1); + setLastUpdated(Date.now()); + }, []); + + // Stats were previously computed once (keyed only on `room`, whose reference + // never changes) and never reflected new activity while the panel stayed + // open. Re-run on new timeline events for this room, throttled so a burst of + // messages doesn't recompute on every single event. + useEffect(() => { + let throttleTimer: ReturnType | undefined; + let pending = false; + + const scheduleTrailing = () => { + throttleTimer = setTimeout(() => { + if (pending) { + pending = false; + recomputeNow(); + scheduleTrailing(); + } else { + throttleTimer = undefined; + } + }, RECOMPUTE_THROTTLE_MS); + }; + + const handleTimeline = (_event: MatrixEvent, eventRoom: Room | undefined) => { + if (eventRoom?.roomId !== room.roomId) return; + if (throttleTimer) { + pending = true; + return; + } + recomputeNow(); + scheduleTrailing(); + }; + + mx.on(RoomEvent.Timeline, handleTimeline); + return () => { + mx.removeListener(RoomEvent.Timeline, handleTimeline); + if (throttleTimer) clearTimeout(throttleTimer); + }; + }, [mx, room, recomputeNow]); + const stats = useMemo(() => { const events = room.getLiveTimeline().getEvents(); @@ -137,7 +192,10 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { newestTs, totalCached: events.length, }; - }, [room]); + // recomputeTick is intentionally in the deps (unused in the body) - it's the + // signal bumped by the timeline listener above to force this to re-run. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [room, recomputeTick]); const maxHour = Math.max(...stats.hourBuckets, 1); const maxMsgCount = stats.top5.length > 0 ? (stats.top5[0]?.[1] ?? 1) : 1; @@ -167,7 +225,7 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { {/* ── Disclaimer banner ── */} - + Based on {stats.totalMessages} locally cached message @@ -179,6 +237,20 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { from {formatDate(stats.oldestTs)} to {formatDate(stats.newestTs)} )} + + Last updated {formatUpdatedAt(lastUpdated)} + + + + + +