From 7f17940d348919718d630d944f52be8eb8bf07f8 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 19 Sep 2026 17:42:00 -0400 Subject: [PATCH] feat(calls): room header chip for voice limit / audio-only policy (#134) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One small Surface chip next to the header controls, only when a rule exists: '0/10' (participants / io.lotus.voice_limit) and/or a muted-camera glyph when the room's io.lotus.room_quality forbids camera and screenshare; the tooltip and aria-label carry the words ('Voice limit 10 (0 in call) · Audio-only room'). Desktop only — the mobile header is already tight. Verified headless: appears with the rules set, disappears when they are cleared. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/room/RoomViewHeader.tsx | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 08f4e95d7..41be23fa9 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -27,6 +27,8 @@ import { import { useAtom } from 'jotai'; import { useNavigate } from 'react-router-dom'; import { Room } from 'matrix-js-sdk'; +import { useVoiceChannelFull } from '../../hooks/useVoiceChannelFull'; +import { useRoomCallPolicy } from '../../hooks/useRoomCallPolicy'; import { useStateEvent } from '../../hooks/useStateEvent'; import { PageHeader } from '../../components/page'; import { RoomAvatar, RoomIcon } from '../../components/room-avatar'; @@ -415,6 +417,54 @@ const CallMenu = forwardRef( }, ); +/** + * [Gitea #134] One small chip that tells members a room's call rules before + * they hit the server-side guard: "8/10" when a voice limit is set, a muted + * camera glyph when the room is audio-only. Nothing when no rule exists; the + * tooltip carries the words. Desktop only (the mobile header is tight). + */ +function CallRulesChip({ room }: { room: Room }) { + const { current, max } = useVoiceChannelFull(room); + const { allowCamera, allowScreenshare } = useRoomCallPolicy(room); + const audioOnly = !allowCamera && !allowScreenshare; + if (max <= 0 && !audioOnly) return null; + const words = [ + max > 0 ? `Voice limit ${max} (${current} in call)` : undefined, + audioOnly ? 'Audio-only room' : undefined, + ] + .filter(Boolean) + .join(' · '); + return ( + + {words} + + } + > + {(triggerRef) => ( + : undefined} + > + {max > 0 && ( + + {current}/{max} + + )} + + )} + + ); +} + function CallButton() { const room = useRoom(); const direct = useIsDirectRoom(); @@ -731,6 +781,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) { } /> + {screenSize === ScreenSize.Desktop && } {!room.isCallRoom() && livekitSupported && rtcSupported &&