From 31b3cf63c68fdf3d7e93eac734a39004bc351578 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 13 Sep 2026 00:56:22 -0400 Subject: [PATCH] refactor(calls): one ScreenshareConfirm and one room-policy hook for both call bars Fixes #101 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/call-status/CallControl.tsx | 93 +++-------------- src/app/features/call/CallControls.tsx | 101 ++++--------------- src/app/features/call/ScreenshareConfirm.tsx | 92 +++++++++++++++++ 3 files changed, 123 insertions(+), 163 deletions(-) create mode 100644 src/app/features/call/ScreenshareConfirm.tsx diff --git a/src/app/features/call-status/CallControl.tsx b/src/app/features/call-status/CallControl.tsx index a8f2a945e..1eb9f15c6 100644 --- a/src/app/features/call-status/CallControl.tsx +++ b/src/app/features/call-status/CallControl.tsx @@ -1,18 +1,5 @@ -import { - Box, - Button, - Chip, - color, - config, - Icon, - IconButton, - Icons, - Spinner, - Text, - Tooltip, - TooltipProvider, -} from 'folds'; -import React, { useCallback, useEffect, useState } from 'react'; +import { Box, Chip, Icon, IconButton, Icons, Spinner, Text, Tooltip, TooltipProvider } from 'folds'; +import React, { useCallback, useState } from 'react'; import { useSetAtom } from 'jotai'; import { StatusDivider } from './components'; import { CallEmbed, useCallControlState } from '../../plugins/call'; @@ -20,6 +7,7 @@ import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { callEmbedAtom } from '../../state/callEmbed'; import { MobileTouchTarget } from '../../styles/mobile.css'; import { useRoomCallPolicy } from '../../hooks/useRoomCallPolicy'; +import { ScreenshareConfirm } from '../call/ScreenshareConfirm'; type MicrophoneButtonProps = { enabled: boolean; @@ -199,14 +187,6 @@ export function CallControl({ 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(), @@ -230,64 +210,15 @@ 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.toggleScreenshare(); + setShareConfirm(false); + }} + onCancel={() => setShareConfirm(false)} + /> (); const [shareConfirm, setShareConfirm] = useState(false); - useEffect(() => { - if (!shareConfirm) return; - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') setShareConfirm(false); - }; - window.addEventListener('keydown', onKeyDown); - return () => window.removeEventListener('keydown', onKeyDown); - }, [shareConfirm]); const [pttMode] = useSetting(settingsAtom, 'pttMode'); const [pttKey] = useSetting(settingsAtom, 'pttKey'); const [soundboardEnabled] = useSetting(settingsAtom, 'soundboardEnabled'); @@ -107,16 +97,15 @@ export function CallControls({ callEmbed }: CallControlsProps) { // visual PTT chip remains here. const pttActive = useAtomValue(pttActiveAtom); - // [P5-31] Hard room publish policy — hide controls the server will refuse so - // users don't click dead buttons. Absent/true = allowed. - const roomQualityEvent = useStateEvent(callEmbed.room, StateEvent.LotusRoomQuality); - const roomQuality = roomQualityEvent?.getContent(); - const cameraAllowed = roomQuality?.allow_camera !== false; - const screenshareAllowed = roomQuality?.allow_screenshare !== false; + // [P5-31 / Gitea #101] Hard room publish policy — hide controls the server + // will refuse so users don't click dead buttons. Absent/true = allowed. + // Shared with the app-wide CallStatus bar's CallControl via useRoomCallPolicy + // so both surfaces apply the same gating. + 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 = cameraAllowed || video; - const showScreenshare = screenshareAllowed || screenshare; + const showCamera = allowCamera || video; + const showScreenshare = allowScreenshare || screenshare; const showVideoGroup = showCamera || showScreenshare || !!document.fullscreenEnabled; const handleOpenMenu: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); @@ -191,67 +180,15 @@ export function CallControls({ callEmbed }: CallControlsProps) { )} - {shareConfirm && ( - <> -
setShareConfirm(false)} - aria-hidden="true" - /> - - - Share your screen? - - - Your screen will be visible to all participants in this call. - - - - - - - - )} + { + callEmbed.control.toggleScreenshare(); + setShareConfirm(false); + }} + onCancel={() => setShareConfirm(false)} + /> void; + onCancel: () => void; + /** + * Horizontal placement relative to the trigger button. `Center` (the + * in-call bar's centered layout) transforms to center itself over the + * anchor; `Start` (the app-wide status bar, anchored to its own left edge) + * hugs the anchor's left edge instead. + */ + align?: 'Center' | 'Start'; +}; + +/** + * [Gitea #101] Shared "Share your screen?" confirmation popover, used by both + * the in-call `CallControls` bar and the app-wide `CallStatus` bar's + * `CallControl`. Previously each bar carried its own near-identical copy; + * hoisted here so their behaviour (Escape / click-outside to close, confirm + * starts the share) can't drift apart. + */ +export function ScreenshareConfirm({ + open, + onConfirm, + onCancel, + align = 'Center', +}: ScreenshareConfirmProps) { + useEffect(() => { + if (!open) return undefined; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [open, onCancel]); + + if (!open) return null; + + return ( + <> +