fix(rooms): Room Insights refreshes on new timeline events (throttled) with a Refresh button
Fixes #83 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -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<typeof setTimeout> | 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 ── */}
|
||||
<SequenceCard variant="SurfaceVariant" gap="200" alignItems="Center">
|
||||
<Icon src={Icons.Warning} size="200" style={{ color: color.Warning.Main }} />
|
||||
<Box direction="Column" gap="100">
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Text size="T300">
|
||||
<strong>
|
||||
Based on {stats.totalMessages} locally cached message
|
||||
@@ -179,6 +237,20 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) {
|
||||
from {formatDate(stats.oldestTs)} to {formatDate(stats.newestTs)}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="T200" priority="300">
|
||||
Last updated {formatUpdatedAt(lastUpdated)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton
|
||||
onClick={recomputeNow}
|
||||
variant="SurfaceVariant"
|
||||
size="300"
|
||||
radii="300"
|
||||
aria-label="Refresh insights"
|
||||
>
|
||||
<Icon src={Icons.Reload} size="100" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</SequenceCard>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user