feat(calls): live-call dot on the space rail (#148)
CI / Build & Quality Checks (push) Canceled after 0s
CI / Trigger Desktop Build (push) Canceled after 0s
CI / Secret scan (gitleaks) (push) Canceled after 0s
CI / Docker image build & smoke test (push) Canceled after 0s
CI / Playwright smoke (e2e) (push) Canceled after 0s

A small red dot on a space tab when any room in that space (recursively)
has an active MatrixRTC session, so a live call is visible even when you
are looking at a different space or your DMs. Hidden while the space is
selected — the room list already shows the Live badge there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-19 23:25:49 -04:00
co-authored by Claude Opus 5
parent b69099a862
commit 4fe9c87010
3 changed files with 81 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { useEffect, useMemo, useState } from 'react';
import { MatrixEvent, RoomStateEvent } from 'matrix-js-sdk';
import { MatrixRTCSessionManagerEvents } from 'matrix-js-sdk/lib/matrixrtc/MatrixRTCSessionManager';
import { useMatrixClient } from './useMatrixClient';
import { StateEvent } from '../../types/matrix/room';
/**
* [Gitea #148] Whether any of the given rooms has a live MatrixRTC call.
* Derived from the same session memberships the sidebar voice rows use;
* recomputed only when a session starts/ends or a call-member state event
* lands in one of the rooms — no per-render scans.
*/
export function useAnyRoomLiveCall(roomIds: string[]): boolean {
const mx = useMatrixClient();
const idSet = useMemo(() => new Set(roomIds), [roomIds]);
const compute = () =>
roomIds.some((id) => {
const room = mx.getRoom(id);
return !!room && mx.matrixRTC.getRoomSession(room).memberships.length > 0;
});
const [live, setLive] = useState(compute);
useEffect(() => {
setLive(compute());
const onSession = (roomId: string) => {
if (idSet.has(roomId)) setLive(compute());
};
const onState = (event: MatrixEvent) => {
if (event.getType() !== StateEvent.GroupCallMemberPrefix) return;
const roomId = event.getRoomId();
if (roomId && idSet.has(roomId)) setLive(compute());
};
mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onSession);
mx.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onSession);
mx.on(RoomStateEvent.Events, onState);
return () => {
mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onSession);
mx.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onSession);
mx.off(RoomStateEvent.Events, onState);
};
// compute closes over roomIds; idSet changes exactly when roomIds does
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mx, idSet]);
return live;
}