feat(calls): say when answering turns the camera on, offer audio-only (#135)
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

The only join path that skips the prescreen and can start with the camera on
is answering a video call with 'Join with Camera On' enabled. The incoming
dialog and the in-call banner now say 'Answering turns your camera on', label
the primary button 'Answer with camera' and add 'Answer without camera'.
Nothing changes for voice calls or when the setting is off. The other
no-prescreen paths already can't start on camera: the sidebar join uses a
session video pref that is only ever true after the user turned it on
themselves, and it is guarded by the same setting.

Verified headless: video call + setting on → notice + both buttons, 'Answer
without camera' joins with videoEnabled=false; voice call → plain Answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
2026-09-19 17:26:31 -04:00
co-authored by Claude Opus 5
parent 33e16e85b3
commit d2f56817b3
+46 -3
View File
@@ -117,6 +117,10 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
const rtcSupported = webRTCSupported();
const canAnswer = livekitSupported && rtcSupported;
const { room } = info;
// [Gitea #135] Answer follows cameraOnJoin: say so, and offer the audio-only
// alternative, rather than putting the user on video without warning.
const [cameraOnJoinPref] = useSetting(settingsAtom, 'cameraOnJoin');
const willAnswerWithCamera = info.intent === 'video' && !!cameraOnJoinPref;
const [ringtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
const [ringtoneId] = useSetting(settingsAtom, 'ringtoneId');
@@ -222,6 +226,11 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
</Text>
)}
<Box direction="Column" gap="300">
{willAnswerWithCamera && (
<Text size="T200" align="Center" priority="300">
Answering turns your camera on.
</Text>
)}
<Button
style={{ flexGrow: 1 }}
variant="Success"
@@ -238,9 +247,25 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
disabled={!canAnswer}
>
<Text as="span" size="B400">
Answer
{willAnswerWithCamera ? 'Answer with camera' : 'Answer'}
</Text>
</Button>
{willAnswerWithCamera && (
<Button
style={{ flexGrow: 1 }}
variant="Success"
fill="Soft"
size="400"
radii="400"
onClick={() => onAnswer(room, false)}
before={<Icon size="200" src={Icons.Phone} filled />}
disabled={!canAnswer}
>
<Text as="span" size="B400">
Answer without camera
</Text>
</Button>
)}
<Button
style={{ flexGrow: 1 }}
variant={dm ? 'Critical' : 'Secondary'}
@@ -282,6 +307,8 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
const useAuthentication = useMediaAuthentication();
const { room } = info;
const isVideo = info.intent === 'video';
const [cameraOnJoinPref] = useSetting(settingsAtom, 'cameraOnJoin');
const bannerCameraOn = isVideo && !!cameraOnJoinPref;
const [ringtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
const [ringtoneId] = useSetting(settingsAtom, 'ringtoneId');
@@ -380,7 +407,7 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
</Text>
</Box>
</Box>
<Box gap="200">
<Box gap="200" wrap="Wrap">
<Button
style={{ flexGrow: 1 }}
variant="Success"
@@ -389,11 +416,27 @@ function IncomingCallBanner({ dm, info, onIgnore, onAnswer, onReject }: Incoming
radii="300"
onClick={() => onAnswer(room, isVideo)}
before={<Icon size="100" src={isVideo ? Icons.VideoCamera : Icons.Phone} filled />}
title={bannerCameraOn ? 'Answering turns your camera on' : undefined}
>
<Text as="span" size="B300">
Answer
{bannerCameraOn ? 'Answer with camera' : 'Answer'}
</Text>
</Button>
{bannerCameraOn && (
<Button
style={{ flexGrow: 1 }}
variant="Success"
fill="Soft"
size="300"
radii="300"
onClick={() => onAnswer(room, false)}
before={<Icon size="100" src={Icons.Phone} filled />}
>
<Text as="span" size="B300">
Answer without camera
</Text>
</Button>
)}
<Button
style={{ flexGrow: 1 }}
variant={dm ? 'Critical' : 'Secondary'}