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 { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
import { roomToParentsAtom } from '../../../state/room/roomToParents';
|
||||||
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
import { allRoomsAtom } from '../../../state/room-list/roomList';
|
||||||
|
import { useAnyRoomLiveCall } from '../../../hooks/useSpaceLiveCall';
|
||||||
|
import { LiveDot } from './SpaceTabs.css';
|
||||||
import {
|
import {
|
||||||
getOriginBaseUrl,
|
getOriginBaseUrl,
|
||||||
getSpaceLobbyPath,
|
getSpaceLobbyPath,
|
||||||
@@ -450,6 +452,15 @@ function SpaceTab({
|
|||||||
|
|
||||||
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
|
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) => {
|
const handleContextMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const cords = evt.currentTarget.getBoundingClientRect();
|
const cords = evt.currentTarget.getBoundingClientRect();
|
||||||
@@ -497,6 +508,14 @@ function SpaceTab({
|
|||||||
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
<UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
|
||||||
</SidebarItemBadge>
|
</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 && (
|
{menuAnchor && (
|
||||||
<PopOut
|
<PopOut
|
||||||
anchor={menuAnchor}
|
anchor={menuAnchor}
|
||||||
|
|||||||
Reference in New Issue
Block a user