From fc68e0a76974a104b51edfd1d4716dc49939cea7 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 12 Sep 2026 19:46:06 -0400 Subject: [PATCH] fix(calls): app-wide call bar honours room camera/screenshare policy The persistent call-status bar exposed Video and ScreenShare with no io.lotus.room_quality check and no share confirmation, bypassing the in-room bar's gating. Add useRoomCallPolicy and apply the same hiding plus a "Share your screen?" confirm. Fixes #26 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/call-status/CallControl.tsx | 107 +++++++++++++++++-- src/app/hooks/useRoomCallPolicy.ts | 28 +++++ 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/app/hooks/useRoomCallPolicy.ts diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx index 1f3e929ed..a8f2a945e 100644 --- a/src/app/features/call-status/CallControl.tsx +++ b/src/app/features/call-status/CallControl.tsx @@ -1,11 +1,25 @@ -import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds'; -import React, { useCallback } from 'react'; +import { + Box, + Button, + Chip, + color, + config, + Icon, + IconButton, + Icons, + Spinner, + Text, + Tooltip, + TooltipProvider, +} from 'folds'; +import React, { useCallback, useEffect, useState } from 'react'; import { useSetAtom } from 'jotai'; import { StatusDivider } from './components'; import { CallEmbed, useCallControlState } from '../../plugins/call'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { callEmbedAtom } from '../../state/callEmbed'; import { MobileTouchTarget } from '../../styles/mobile.css'; +import { useRoomCallPolicy } from '../../hooks/useRoomCallPolicy'; type MicrophoneButtonProps = { enabled: boolean; @@ -177,6 +191,23 @@ export function CallControl({ const { microphone, video, sound, screenshare } = useCallControlState(callEmbed.control); const setCallEmbed = useSetAtom(callEmbedAtom); + // [Gitea #26] Apply the same room-level camera/screenshare policy as the + // in-room CallControls bar, so the status bar can't be used to bypass it. + const { allowCamera, allowScreenshare } = useRoomCallPolicy(callEmbed.room); + // Keep a forbidden control visible while its track is still live (so the user + // can stop it); otherwise hide it entirely. + const showCamera = allowCamera || video; + const showScreenshare = allowScreenshare || screenshare; + const [shareConfirm, setShareConfirm] = useState(false); + useEffect(() => { + if (!shareConfirm) return undefined; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setShareConfirm(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [shareConfirm]); + const handleMicrophoneToggle = useCallback( () => callEmbed.control.toggleMicrophone(), [callEmbed], @@ -198,7 +229,65 @@ export function CallControl({ }; return ( - + + {shareConfirm && ( + <> +
setShareConfirm(false)} + aria-hidden="true" + /> + + + Share your screen? + + + Your screen will be visible to all participants in this call. + + + + + + + + )} callEmbed.control.toggleSound()} disabled={!callJoined} /> - {!compact && } - - {!compact && ( + {!compact && (showCamera || showScreenshare) && } + {showCamera && ( + + )} + {!compact && showScreenshare && ( callEmbed.control.toggleScreenshare()} + onToggle={() => + screenshare ? callEmbed.control.toggleScreenshare() : setShareConfirm(true) + } disabled={!callJoined} /> )} diff --git a/src/app/hooks/useRoomCallPolicy.ts b/src/app/hooks/useRoomCallPolicy.ts new file mode 100644 index 000000000..d6477cf6e --- /dev/null +++ b/src/app/hooks/useRoomCallPolicy.ts @@ -0,0 +1,28 @@ +import { Room } from 'matrix-js-sdk'; +import { useMemo } from 'react'; +import { useStateEvent } from './useStateEvent'; +import { StateEvent } from '../../types/matrix/room'; +import { RoomQualityContent } from '../utils/callQuality'; + +export type RoomCallPolicy = { + allowCamera: boolean; + allowScreenshare: boolean; +}; + +/** + * [Gitea #26] Shared room-level camera/screenshare policy, read from the + * `io.lotus.room_quality` state event. Absent/true = allowed; only an explicit + * `false` forbids. Hoisted out of `CallControls` so other call surfaces (e.g. + * the app-wide `CallStatus` bar) can apply the same gating. + */ +export const useRoomCallPolicy = (room: Room): RoomCallPolicy => { + const roomQualityEvent = useStateEvent(room, StateEvent.LotusRoomQuality); + + return useMemo(() => { + const roomQuality = roomQualityEvent?.getContent(); + return { + allowCamera: roomQuality?.allow_camera !== false, + allowScreenshare: roomQuality?.allow_screenshare !== false, + }; + }, [roomQualityEvent]); +};