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
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:
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { color, toRem } from 'folds';
|
||||
|
||||
/** [Gitea #148] Bottom-right live-call dot on a space tab. */
|
||||
export const LiveDot = style({
|
||||
position: 'absolute',
|
||||
right: toRem(-2),
|
||||
bottom: toRem(-2),
|
||||
width: toRem(10),
|
||||
height: toRem(10),
|
||||
borderRadius: '50%',
|
||||
backgroundColor: color.Critical.Main,
|
||||
boxShadow: `0 0 0 ${toRem(2)} ${color.Background.Container}`,
|
||||
pointerEvents: 'none',
|
||||
zIndex: 1,
|
||||
});
|
||||
@@ -47,6 +47,8 @@ import {
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||
import { useAnyRoomLiveCall } from '../../../hooks/useSpaceLiveCall';
|
||||
import { LiveDot } from './SpaceTabs.css';
|
||||
import {
|
||||
getOriginBaseUrl,
|
||||
getSpaceLobbyPath,
|
||||
@@ -450,6 +452,15 @@ function SpaceTab({
|
||||
|
||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
||||
|
||||
// [Gitea #148] Subtle live-call dot for a space you're not looking at.
|
||||
const roomToParentsForLive = useAtomValue(roomToParentsAtom);
|
||||
const liveChildren = useSpaceChildren(
|
||||
allRoomsAtom,
|
||||
space.roomId,
|
||||
useRecursiveChildScopeFactory(mx, roomToParentsForLive),
|
||||
);
|
||||
const liveCall = useAnyRoomLiveCall(liveChildren);
|
||||
|
||||
const handleContextMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||
evt.preventDefault();
|
||||
const cords = evt.currentTarget.getBoundingClientRect();
|
||||
@@ -497,6 +508,14 @@ function SpaceTab({
|
||||
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
||||
</SidebarItemBadge>
|
||||
)}
|
||||
{liveCall && !selected && (
|
||||
<span
|
||||
className={LiveDot}
|
||||
role="img"
|
||||
aria-label="A call is live in this space"
|
||||
title="A call is live in this space"
|
||||
/>
|
||||
)}
|
||||
{menuAnchor && (
|
||||
<PopOut
|
||||
anchor={menuAnchor}
|
||||
|
||||
Reference in New Issue
Block a user