refactor dashboard room activity fetching

Refactor selected-room dashboard activity fetching, split Rooms i18n, and tighten mobile usage layout.
This commit is contained in:
Eyejoker
2026-04-27 20:36:18 +09:00
committed by GitHub
parent ea323cc787
commit eeba0334f9
5 changed files with 390 additions and 329 deletions

View File

@@ -34,7 +34,6 @@ import {
fetchAccounts, fetchAccounts,
fetchDashboardData, fetchDashboardData,
fetchModelConfig, fetchModelConfig,
fetchRoomsTimelineBatch,
runInboxAction, runInboxAction,
runServiceAction, runServiceAction,
runScheduledTaskAction, runScheduledTaskAction,
@@ -52,6 +51,10 @@ import {
type Locale, type Locale,
type Messages, type Messages,
} from './i18n'; } from './i18n';
import {
useSelectedRoomActivity,
type RoomActivityMap,
} from './useRoomActivity';
import './styles.css'; import './styles.css';
interface DashboardState { interface DashboardState {
@@ -60,8 +63,6 @@ interface DashboardState {
tasks: DashboardTask[]; tasks: DashboardTask[];
} }
type RoomActivityMap = Record<string, DashboardRoomActivity>;
type UsageRow = DashboardOverview['usage']['rows'][number]; type UsageRow = DashboardOverview['usage']['rows'][number];
type InboxItem = DashboardOverview['inbox'][number]; type InboxItem = DashboardOverview['inbox'][number];
type RiskLevel = 'ok' | 'warn' | 'critical'; type RiskLevel = 'ok' | 'warn' | 'critical';
@@ -2568,7 +2569,9 @@ function RoomBoardV2({
roomActivity, roomActivity,
roomActivityLoading, roomActivityLoading,
roomMessageKey, roomMessageKey,
selectedJid,
locale, locale,
onSelectedJidChange,
snapshots, snapshots,
t, t,
}: { }: {
@@ -2585,13 +2588,14 @@ function RoomBoardV2({
roomActivity: RoomActivityMap; roomActivity: RoomActivityMap;
roomActivityLoading: boolean; roomActivityLoading: boolean;
roomMessageKey: string | null; roomMessageKey: string | null;
selectedJid: string | null;
locale: Locale; locale: Locale;
onSelectedJidChange: (jid: string | null) => void;
snapshots: StatusSnapshot[]; snapshots: StatusSnapshot[];
t: Messages; t: Messages;
}) { }) {
const [filter, setFilter] = useState<RoomFilter>('all'); const [filter, setFilter] = useState<RoomFilter>('all');
const [sort, setSort] = useState<RoomSort>('recent'); const [sort, setSort] = useState<RoomSort>('recent');
const [selectedJid, setSelectedJid] = useState<string | null>(null);
const [drafts, setDrafts] = useState<Record<string, string>>({}); const [drafts, setDrafts] = useState<Record<string, string>>({});
const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) => const allEntries: RoomEntryWithService[] = snapshots.flatMap((snapshot) =>
@@ -2601,10 +2605,6 @@ function RoomBoardV2({
})), })),
); );
if (allEntries.length === 0) {
return <EmptyState>{t.rooms.empty}</EmptyState>;
}
const counts = { const counts = {
all: allEntries.length, all: allEntries.length,
processing: allEntries.filter((e) => e.status === 'processing').length, processing: allEntries.filter((e) => e.status === 'processing').length,
@@ -2632,6 +2632,15 @@ function RoomBoardV2({
const selectedEntry = const selectedEntry =
sorted.find((e) => e.jid === selectedJid) ?? sorted[0] ?? null; sorted.find((e) => e.jid === selectedJid) ?? sorted[0] ?? null;
useEffect(() => {
const nextJid = selectedEntry?.jid ?? null;
if (nextJid !== selectedJid) onSelectedJidChange(nextJid);
}, [onSelectedJidChange, selectedEntry?.jid, selectedJid]);
if (allEntries.length === 0) {
return <EmptyState>{t.rooms.empty}</EmptyState>;
}
function setDraft(jid: string, value: string) { function setDraft(jid: string, value: string) {
setDrafts((previous) => ({ ...previous, [jid]: value })); setDrafts((previous) => ({ ...previous, [jid]: value }));
} }
@@ -2705,7 +2714,7 @@ function RoomBoardV2({
aria-current={active ? 'page' : undefined} aria-current={active ? 'page' : undefined}
className={`rooms-list-item status-${entry.status}${active ? ' is-active' : ''}`} className={`rooms-list-item status-${entry.status}${active ? ' is-active' : ''}`}
key={`${entry.serviceId}:${entry.jid}`} key={`${entry.serviceId}:${entry.jid}`}
onClick={() => setSelectedJid(entry.jid)} onClick={() => onSelectedJidChange(entry.jid)}
type="button" type="button"
> >
<span className={`room-pulse pulse-${entry.status}`}> <span className={`room-pulse pulse-${entry.status}`}>
@@ -3788,8 +3797,15 @@ function App() {
const [serviceActionKey, setServiceActionKey] = const [serviceActionKey, setServiceActionKey] =
useState<ServiceActionKey | null>(null); useState<ServiceActionKey | null>(null);
const [roomMessageKey, setRoomMessageKey] = useState<string | null>(null); const [roomMessageKey, setRoomMessageKey] = useState<string | null>(null);
const [roomActivity, setRoomActivity] = useState<RoomActivityMap>({}); const [selectedRoomJid, setSelectedRoomJid] = useState<string | null>(null);
const [roomActivityLoading, setRoomActivityLoading] = useState(false); const {
refreshRoom: refreshRoomActivity,
roomActivity,
roomActivityLoading,
} = useSelectedRoomActivity({
active: activeView === 'rooms',
selectedRoomJid: selectedRoomJid,
});
const [pendingMessages, setPendingMessages] = useState< const [pendingMessages, setPendingMessages] = useState<
Record<string, Array<DashboardRoomActivity['messages'][number]>> Record<string, Array<DashboardRoomActivity['messages'][number]>>
>({}); >({});
@@ -3956,8 +3972,7 @@ function App() {
try { try {
await sendRoomMessage(roomJid, text, requestId, nickname || null); await sendRoomMessage(roomJid, text, requestId, nickname || null);
try { try {
const fresh = await fetchRoomsTimelineBatch(); await refreshRoomActivity(roomJid);
setRoomActivity(fresh);
} catch { } catch {
/* refresh will retry on next poll */ /* refresh will retry on next poll */
} }
@@ -4125,102 +4140,6 @@ function App() {
return () => window.clearInterval(id); return () => window.clearInterval(id);
}, []); }, []);
const roomsJidsKey = useMemo(() => {
if (!data) return '';
return [
...new Set(
data.snapshots.flatMap((snapshot) =>
snapshot.entries.map((entry) => entry.jid),
),
),
]
.sort()
.join('|');
}, [data]);
const roomsLastUpdatedKey = useMemo(() => {
if (!data) return '';
return data.snapshots
.map((s) => s.updatedAt)
.sort()
.join('|');
}, [data]);
useEffect(() => {
if (activeView !== 'rooms' || !data) return;
if (!roomsJidsKey) {
setRoomActivity({});
return;
}
let cancelled = false;
let inFlight = false;
let pollIntervalId: number | null = null;
let es: EventSource | null = null;
const applyMap = (map: RoomActivityMap) => {
if (!cancelled) setRoomActivity(map);
};
const fetchOnce = async (initial: boolean) => {
if (cancelled || inFlight) return;
inFlight = true;
if (initial) setRoomActivityLoading(true);
try {
applyMap(await fetchRoomsTimelineBatch());
} catch {
/* keep last good state */
} finally {
inFlight = false;
if (!cancelled && initial) setRoomActivityLoading(false);
}
};
const startPollingFallback = () => {
if (pollIntervalId !== null) return;
pollIntervalId = window.setInterval(() => void fetchOnce(false), 2000);
};
const stopPollingFallback = () => {
if (pollIntervalId === null) return;
window.clearInterval(pollIntervalId);
pollIntervalId = null;
};
if (typeof window !== 'undefined' && 'EventSource' in window) {
void fetchOnce(true);
try {
es = new EventSource('/api/stream');
es.addEventListener('rooms-timeline', (event) => {
try {
const parsed = JSON.parse(
(event as MessageEvent).data,
) as RoomActivityMap;
applyMap(parsed);
stopPollingFallback();
} catch {
/* malformed event, ignore */
}
});
es.onerror = () => {
// Browser auto-reconnects; meanwhile keep data fresh via polling.
startPollingFallback();
};
} catch {
startPollingFallback();
}
} else {
void fetchOnce(true);
startPollingFallback();
}
return () => {
cancelled = true;
stopPollingFallback();
es?.close();
};
}, [activeView, roomsJidsKey]);
useEffect(() => { useEffect(() => {
setPendingMessages((prev) => { setPendingMessages((prev) => {
const next: typeof prev = {}; const next: typeof prev = {};
@@ -4385,6 +4304,8 @@ function App() {
roomActivityLoading={roomActivityLoading} roomActivityLoading={roomActivityLoading}
roomMessageKey={roomMessageKey} roomMessageKey={roomMessageKey}
locale={locale} locale={locale}
onSelectedJidChange={setSelectedRoomJid}
selectedJid={selectedRoomJid}
snapshots={data.snapshots} snapshots={data.snapshots}
t={t} t={t}
/> />

View File

@@ -1,3 +1,5 @@
import { roomMessages, type RoomMessages } from './i18n/rooms';
export const LOCALES = ['ko', 'en', 'zh', 'ja'] as const; export const LOCALES = ['ko', 'en', 'zh', 'ja'] as const;
export type Locale = (typeof LOCALES)[number]; export type Locale = (typeof LOCALES)[number];
@@ -152,46 +154,7 @@ export interface Messages {
confirmDecline: string; confirmDecline: string;
}; };
}; };
rooms: { rooms: RoomMessages;
empty: string;
cardsAria: string;
room: string;
service: string;
agent: string;
status: string;
queue: string;
queueWaitingMessages: string;
tasks: string;
elapsed: string;
activity: string;
loadingActivity: string;
noActivity: string;
task: string;
noTask: string;
currentTurn: string;
noTurn: string;
round: string;
attempt: string;
updated: string;
output: string;
latestOutput: string;
outputHistory: string;
noOutput: string;
recentMessages: string;
messageHistory: string;
noMessages: string;
details: string;
message: string;
messagePlaceholder: string;
send: string;
sending: string;
error: string;
filterAll: string;
sortRecent: string;
sortName: string;
sortQueue: string;
sortLabel: string;
};
usage: { usage: {
empty: string; empty: string;
current: string; current: string;
@@ -465,46 +428,7 @@ export const messages = {
confirmDecline: 'Decline this item?', confirmDecline: 'Decline this item?',
}, },
}, },
rooms: { rooms: roomMessages.ko,
empty: '룸 없음.',
cardsAria: '룸 상태 카드',
room: '룸',
service: '서비스',
agent: '에이전트',
status: '상태',
queue: '큐',
queueWaitingMessages: '대기 메시지',
tasks: '태스크',
elapsed: '경과',
activity: '진행',
loadingActivity: '진행 로딩 중',
noActivity: '진행 없음',
task: '태스크',
noTask: '태스크 없음',
currentTurn: '현재 턴',
noTurn: '턴 없음',
round: '라운드',
attempt: '시도',
updated: '갱신',
output: '출력',
latestOutput: '마지막 출력',
outputHistory: '출력 기록',
noOutput: '출력 없음',
recentMessages: '메시지 기록',
messageHistory: '메시지 기록',
noMessages: '메시지 없음',
details: '세부',
message: '메시지',
messagePlaceholder: '요청 입력...',
send: '전송',
sending: '전송 중...',
error: '오류',
filterAll: '전체',
sortRecent: '최근 활동',
sortName: '이름순',
sortQueue: '큐 많은 순',
sortLabel: '정렬',
},
usage: { usage: {
empty: '사용량 스냅샷 없음. 수집기 확인.', empty: '사용량 스냅샷 없음. 수집기 확인.',
current: '사용중', current: '사용중',
@@ -762,46 +686,7 @@ export const messages = {
confirmDecline: 'Decline this item?', confirmDecline: 'Decline this item?',
}, },
}, },
rooms: { rooms: roomMessages.en,
empty: 'No rooms yet.',
cardsAria: 'Room status cards',
room: 'room',
service: 'service',
agent: 'agent',
status: 'status',
queue: 'queue',
queueWaitingMessages: 'pending messages',
tasks: 'tasks',
elapsed: 'elapsed',
activity: 'activity',
loadingActivity: 'Loading activity',
noActivity: 'No activity',
task: 'task',
noTask: 'No task',
currentTurn: 'Current turn',
noTurn: 'No turn',
round: 'round',
attempt: 'attempt',
updated: 'updated',
output: 'output',
latestOutput: 'latest output',
outputHistory: 'output log',
noOutput: 'No output',
recentMessages: 'Message log',
messageHistory: 'Message log',
noMessages: 'No messages',
details: 'details',
message: 'message',
messagePlaceholder: 'Type request...',
send: 'Send',
sending: 'Sending',
error: 'Error',
filterAll: 'All',
sortRecent: 'Recent activity',
sortName: 'Name',
sortQueue: 'Queue size',
sortLabel: 'Sort',
},
usage: { usage: {
empty: 'No usage snapshot. Check collector.', empty: 'No usage snapshot. Check collector.',
current: 'Active', current: 'Active',
@@ -1059,46 +944,7 @@ export const messages = {
confirmDecline: '确认拒绝此项?', confirmDecline: '确认拒绝此项?',
}, },
}, },
rooms: { rooms: roomMessages.zh,
empty: '暂无房间。',
cardsAria: '房间状态卡片',
room: '房间',
service: '服务',
agent: '代理',
status: '状态',
queue: '队列',
queueWaitingMessages: '待处理消息',
tasks: '任务',
elapsed: '耗时',
activity: '进展',
loadingActivity: '正在加载进展',
noActivity: '暂无进展',
task: '任务',
noTask: '无任务',
currentTurn: '当前回合',
noTurn: '无回合',
round: '轮次',
attempt: '尝试',
updated: '更新',
output: '输出',
latestOutput: '最新输出',
outputHistory: '输出记录',
noOutput: '暂无输出',
recentMessages: '消息记录',
messageHistory: '消息记录',
noMessages: '暂无消息',
details: '详情',
message: 'Message',
messagePlaceholder: '输入请求...',
send: '发送',
sending: '发送中...',
error: '错误',
filterAll: '全部',
sortRecent: '最近活动',
sortName: '名称',
sortQueue: '队列大小',
sortLabel: '排序',
},
usage: { usage: {
empty: '暂无用量快照。检查采集器。', empty: '暂无用量快照。检查采集器。',
current: '使用中', current: '使用中',
@@ -1358,46 +1204,7 @@ export const messages = {
confirmDecline: 'この項目を拒否しますか?', confirmDecline: 'この項目を拒否しますか?',
}, },
}, },
rooms: { rooms: roomMessages.ja,
empty: 'ルームなし。',
cardsAria: 'ルーム状態カード',
room: 'ルーム',
service: 'サービス',
agent: 'エージェント',
status: '状態',
queue: 'キュー',
queueWaitingMessages: '待機メッセージ',
tasks: 'タスク',
elapsed: '経過',
activity: '進行',
loadingActivity: '進行を読み込み中',
noActivity: '進行なし',
task: 'タスク',
noTask: 'タスクなし',
currentTurn: '現在ターン',
noTurn: 'ターンなし',
round: 'ラウンド',
attempt: '試行',
updated: '更新',
output: '出力',
latestOutput: '最新出力',
outputHistory: '出力履歴',
noOutput: '出力なし',
recentMessages: 'メッセージ履歴',
messageHistory: 'メッセージ履歴',
noMessages: 'メッセージなし',
details: '詳細',
message: 'メッセージ',
messagePlaceholder: '依頼を入力...',
send: '送信',
sending: '送信中...',
error: 'エラー',
filterAll: '全て',
sortRecent: '最近の活動',
sortName: '名前順',
sortQueue: 'キュー順',
sortLabel: '並び替え',
},
usage: { usage: {
empty: '使用量スナップショットなし。収集器を確認。', empty: '使用量スナップショットなし。収集器を確認。',
current: '使用中', current: '使用中',

View File

@@ -0,0 +1,205 @@
import type { Locale } from '../i18n';
export interface RoomMessages {
empty: string;
cardsAria: string;
room: string;
service: string;
agent: string;
status: string;
queue: string;
queueWaitingMessages: string;
tasks: string;
elapsed: string;
activity: string;
loadingActivity: string;
noActivity: string;
task: string;
noTask: string;
currentTurn: string;
noTurn: string;
round: string;
attempt: string;
updated: string;
output: string;
latestOutput: string;
outputHistory: string;
noOutput: string;
recentMessages: string;
messageHistory: string;
noMessages: string;
details: string;
message: string;
messagePlaceholder: string;
send: string;
sending: string;
error: string;
filterAll: string;
sortRecent: string;
sortName: string;
sortQueue: string;
sortLabel: string;
}
export const roomMessages = {
ko: {
empty: '룸 없음.',
cardsAria: '룸 상태 카드',
room: '룸',
service: '서비스',
agent: '에이전트',
status: '상태',
queue: '큐',
queueWaitingMessages: '대기 메시지',
tasks: '태스크',
elapsed: '경과',
activity: '진행',
loadingActivity: '진행 로딩 중',
noActivity: '진행 없음',
task: '태스크',
noTask: '태스크 없음',
currentTurn: '현재 턴',
noTurn: '턴 없음',
round: '라운드',
attempt: '시도',
updated: '갱신',
output: '출력',
latestOutput: '마지막 출력',
outputHistory: '출력 기록',
noOutput: '출력 없음',
recentMessages: '메시지 기록',
messageHistory: '메시지 기록',
noMessages: '메시지 없음',
details: '세부',
message: '메시지',
messagePlaceholder: '요청 입력...',
send: '전송',
sending: '전송 중...',
error: '오류',
filterAll: '전체',
sortRecent: '최근 활동',
sortName: '이름순',
sortQueue: '큐 많은 순',
sortLabel: '정렬',
},
en: {
empty: 'No rooms yet.',
cardsAria: 'Room status cards',
room: 'room',
service: 'service',
agent: 'agent',
status: 'status',
queue: 'queue',
queueWaitingMessages: 'pending messages',
tasks: 'tasks',
elapsed: 'elapsed',
activity: 'activity',
loadingActivity: 'Loading activity',
noActivity: 'No activity',
task: 'task',
noTask: 'No task',
currentTurn: 'Current turn',
noTurn: 'No turn',
round: 'round',
attempt: 'attempt',
updated: 'updated',
output: 'output',
latestOutput: 'latest output',
outputHistory: 'output log',
noOutput: 'No output',
recentMessages: 'Message log',
messageHistory: 'Message log',
noMessages: 'No messages',
details: 'details',
message: 'message',
messagePlaceholder: 'Type request...',
send: 'Send',
sending: 'Sending',
error: 'Error',
filterAll: 'All',
sortRecent: 'Recent activity',
sortName: 'Name',
sortQueue: 'Queue size',
sortLabel: 'Sort',
},
zh: {
empty: '暂无房间。',
cardsAria: '房间状态卡片',
room: '房间',
service: '服务',
agent: '代理',
status: '状态',
queue: '队列',
queueWaitingMessages: '待处理消息',
tasks: '任务',
elapsed: '耗时',
activity: '进展',
loadingActivity: '正在加载进展',
noActivity: '暂无进展',
task: '任务',
noTask: '无任务',
currentTurn: '当前回合',
noTurn: '无回合',
round: '轮次',
attempt: '尝试',
updated: '更新',
output: '输出',
latestOutput: '最新输出',
outputHistory: '输出记录',
noOutput: '暂无输出',
recentMessages: '消息记录',
messageHistory: '消息记录',
noMessages: '暂无消息',
details: '详情',
message: 'Message',
messagePlaceholder: '输入请求...',
send: '发送',
sending: '发送中...',
error: '错误',
filterAll: '全部',
sortRecent: '最近活动',
sortName: '名称',
sortQueue: '队列大小',
sortLabel: '排序',
},
ja: {
empty: 'ルームなし。',
cardsAria: 'ルーム状態カード',
room: 'ルーム',
service: 'サービス',
agent: 'エージェント',
status: '状態',
queue: 'キュー',
queueWaitingMessages: '待機メッセージ',
tasks: 'タスク',
elapsed: '経過',
activity: '進行',
loadingActivity: '進行を読み込み中',
noActivity: '進行なし',
task: 'タスク',
noTask: 'タスクなし',
currentTurn: '現在ターン',
noTurn: 'ターンなし',
round: 'ラウンド',
attempt: '試行',
updated: '更新',
output: '出力',
latestOutput: '最新出力',
outputHistory: '出力履歴',
noOutput: '出力なし',
recentMessages: 'メッセージ履歴',
messageHistory: 'メッセージ履歴',
noMessages: 'メッセージなし',
details: '詳細',
message: 'メッセージ',
messagePlaceholder: '依頼を入力...',
send: '送信',
sending: '送信中...',
error: 'エラー',
filterAll: '全て',
sortRecent: '最近の活動',
sortName: '名前順',
sortQueue: 'キュー順',
sortLabel: '並び替え',
},
} satisfies Record<Locale, RoomMessages>;

View File

@@ -3004,35 +3004,66 @@ progress::-moz-progress-bar {
.usage-row { .usage-row {
grid-template-columns: grid-template-columns:
minmax(86px, 0.86fr) minmax(62px, 0.54fr) minmax(62px, 0.54fr) minmax(72px, 0.82fr) minmax(52px, 0.48fr) minmax(52px, 0.48fr)
minmax(58px, 0.4fr); minmax(48px, 0.36fr);
gap: 5px; gap: 4px;
padding: 9px; padding: 7px 6px;
} }
.usage-account { .usage-account {
display: grid; display: grid;
gap: 5px; gap: 3px;
}
.usage-account div {
gap: 3px;
}
.usage-account strong {
font-size: 13px;
}
.usage-account .pill,
.usage-account .mono-chip {
padding: 3px 6px;
font-size: 10px;
} }
.usage-quota, .usage-quota,
.usage-speed { .usage-speed {
gap: 4px; gap: 3px;
} }
.usage-quota { .usage-quota {
padding: 5px; padding: 4px;
} }
.usage-quota div { .usage-quota div {
gap: 6px; gap: 4px;
}
.usage-quota span,
.usage-speed span {
font-size: 9px;
letter-spacing: 0.04em;
}
.usage-quota strong,
.usage-speed strong {
font-size: 14px;
}
.usage-speed {
min-height: 0;
padding: 4px;
} }
.usage-quota small { .usage-quota small {
overflow: hidden; display: none;
font-size: 10px; }
text-overflow: ellipsis;
white-space: nowrap; .usage-speed small {
display: none;
} }
.record-card-grid { .record-card-grid {
@@ -3168,7 +3199,7 @@ progress::-moz-progress-bar {
} }
.usage-summary { .usage-summary {
grid-template-columns: 1fr; grid-template-columns: minmax(0, 1fr) auto;
} }
.usage-summary div:last-child { .usage-summary div:last-child {
@@ -3176,19 +3207,19 @@ progress::-moz-progress-bar {
} }
.usage-row { .usage-row {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-columns:
minmax(68px, 0.8fr) minmax(48px, 0.48fr) minmax(48px, 0.48fr)
minmax(44px, 0.34fr);
} }
.usage-account { .usage-account {
grid-column: 1 / -1; grid-column: auto;
} }
.usage-speed { .usage-speed {
grid-column: 1 / -1; grid-column: auto;
display: flex; display: grid;
min-height: auto; min-height: 0;
align-items: center;
justify-content: space-between;
} }
.skeleton-grid, .skeleton-grid,
@@ -4582,6 +4613,35 @@ progress::-moz-progress-bar {
color: var(--muted); color: var(--muted);
} }
@media (max-width: 640px) {
.usage-matrix {
overflow-x: hidden;
}
.usage-row {
grid-template-columns: minmax(0, 1fr) 62px 62px !important;
align-items: center;
}
.usage-speed {
display: none;
}
.usage-account strong {
font-size: 13px;
}
.usage-account .pill,
.usage-account .mono-chip {
padding: 3px 6px;
font-size: 10px;
}
.usage-quota {
min-width: 0;
}
}
@media (max-width: 760px) { @media (max-width: 760px) {
.rooms-grid { .rooms-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;

View File

@@ -0,0 +1,68 @@
import { useCallback, useEffect, useState } from 'react';
import { fetchRoomTimeline, type DashboardRoomActivity } from './api';
export type RoomActivityMap = Record<string, DashboardRoomActivity>;
interface UseSelectedRoomActivityOptions {
active: boolean;
pollMs?: number;
selectedRoomJid: string | null;
}
export function useSelectedRoomActivity({
active,
pollMs = 2000,
selectedRoomJid,
}: UseSelectedRoomActivityOptions): {
refreshRoom: (roomJid: string) => Promise<DashboardRoomActivity>;
roomActivity: RoomActivityMap;
roomActivityLoading: boolean;
} {
const [roomActivity, setRoomActivity] = useState<RoomActivityMap>({});
const [roomActivityLoading, setRoomActivityLoading] = useState(false);
const refreshRoom = useCallback(async (roomJid: string) => {
const activity = await fetchRoomTimeline(roomJid);
setRoomActivity((previous) => ({
...previous,
[roomJid]: activity,
}));
return activity;
}, []);
useEffect(() => {
if (!active || !selectedRoomJid) {
setRoomActivityLoading(false);
return undefined;
}
let cancelled = false;
let inFlight = false;
let pollIntervalId: number | null = null;
const fetchOnce = async (initial: boolean) => {
if (cancelled || inFlight) return;
inFlight = true;
if (initial) setRoomActivityLoading(true);
try {
await refreshRoom(selectedRoomJid);
} catch {
/* Keep the last good timeline snapshot. */
} finally {
inFlight = false;
if (!cancelled && initial) setRoomActivityLoading(false);
}
};
void fetchOnce(true);
pollIntervalId = window.setInterval(() => void fetchOnce(false), pollMs);
return () => {
cancelled = true;
if (pollIntervalId !== null) window.clearInterval(pollIntervalId);
};
}, [active, pollMs, refreshRoom, selectedRoomJid]);
return { refreshRoom, roomActivity, roomActivityLoading };
}