While unmuted, the microphone button (call view and the bottom call bar) shows three small green bars in the icon's corner driven by the fork's io.lotus.mic_level (element-call v0.25.0-lotus.16), so you can see the mic is actually picking you up. Nothing is drawn while muted or silent; the button's label is unchanged (the bars are decorative). Your own name no longer appears in the "… is speaking" line: that is now for everyone else, and your voice is on the meter. Avatar speaking rings still include you, and the "You're muted" talking-while-muted notice is unchanged. Verified in a local call with a fake-tone mic: data-mic-level cycles 3/2/1 with the tone on both buttons, disappears on mute and returns on unmute; the status line reads "bob is speaking..." only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
293 lines
8.1 KiB
TypeScript
293 lines
8.1 KiB
TypeScript
import React from 'react';
|
||
import { Icon, IconButton, Icons, Line, Text, Tooltip, TooltipProvider } from 'folds';
|
||
import { useAtom } from 'jotai';
|
||
import { MicLevelBars } from './MicLevelBars';
|
||
import * as css from './styles.css';
|
||
import { MobileTouchTarget } from '../../styles/mobile.css';
|
||
import { callChatAtom } from '../../state/callEmbed';
|
||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||
|
||
export function ControlDivider() {
|
||
return (
|
||
<Line variant="SurfaceVariant" size="300" direction="Vertical" className={css.ControlDivider} />
|
||
);
|
||
}
|
||
|
||
type MicrophoneButtonProps = {
|
||
enabled: boolean;
|
||
onToggle: () => Promise<unknown>;
|
||
/** [Gitea #146] Your input level, 0–3 bars; shown only while unmuted. */
|
||
level?: number;
|
||
};
|
||
export function MicrophoneButton({ enabled, onToggle, level = 0 }: MicrophoneButtonProps) {
|
||
const [micState, toggleMic] = useAsyncCallback(onToggle);
|
||
const loading = micState.status === AsyncStatus.Loading;
|
||
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={enabled ? 'Surface' : 'Warning'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={toggleMic}
|
||
aria-label={enabled ? 'Turn Off Microphone' : 'Turn On Microphone'}
|
||
outlined
|
||
disabled={loading}
|
||
>
|
||
<span style={{ position: 'relative', display: 'inline-flex' }}>
|
||
<Icon size="400" src={enabled ? Icons.Mic : Icons.MicMute} filled={!enabled} />
|
||
{enabled && <MicLevelBars level={level} />}
|
||
</span>
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
type SoundButtonProps = {
|
||
enabled: boolean;
|
||
onToggle: () => void;
|
||
};
|
||
export function SoundButton({ enabled, onToggle }: SoundButtonProps) {
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{enabled ? 'Deafen' : 'Undeafen'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={enabled ? 'Surface' : 'Warning'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={() => onToggle()}
|
||
// No aria-pressed on these call toggles (#187 DP7/8): their labels already
|
||
// name the action ("Deafen" / "Undeafen"), and pressed-state on top
|
||
// read as "Deafen, pressed" while NOT deafened.
|
||
aria-label={enabled ? 'Deafen' : 'Undeafen'}
|
||
outlined
|
||
>
|
||
<Icon
|
||
size="400"
|
||
src={enabled ? Icons.Headphone : Icons.HeadphoneMute}
|
||
filled={!enabled}
|
||
/>
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
type VideoButtonProps = {
|
||
enabled: boolean;
|
||
onToggle: () => Promise<unknown>;
|
||
disabled?: boolean;
|
||
};
|
||
export function VideoButton({ enabled, onToggle, disabled }: VideoButtonProps) {
|
||
const [videoState, toggleVideo] = useAsyncCallback(onToggle);
|
||
const loading = videoState.status === AsyncStatus.Loading;
|
||
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">
|
||
{disabled ? 'Camera disabled in settings' : enabled ? 'Stop Camera' : 'Start Camera'}
|
||
</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={enabled ? 'Success' : 'Surface'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={toggleVideo}
|
||
outlined
|
||
disabled={disabled || loading}
|
||
aria-label={
|
||
disabled ? 'Camera disabled in settings' : enabled ? 'Stop camera' : 'Start camera'
|
||
}
|
||
style={disabled ? { opacity: 0.4, cursor: 'not-allowed' } : undefined}
|
||
>
|
||
<Icon
|
||
size="400"
|
||
src={enabled ? Icons.VideoCamera : Icons.VideoCameraMute}
|
||
filled={enabled}
|
||
/>
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
type ScreenShareButtonProps = {
|
||
enabled: boolean;
|
||
onToggle: () => void;
|
||
};
|
||
export function ScreenShareButton({ enabled, onToggle }: ScreenShareButtonProps) {
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{enabled ? 'Stop Screenshare' : 'Start Screenshare'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={enabled ? 'Success' : 'Surface'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={() => onToggle()}
|
||
aria-label={enabled ? 'Stop Screenshare' : 'Start Screenshare'}
|
||
outlined
|
||
>
|
||
<Icon size="400" src={Icons.ScreenShare} filled={enabled} />
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
export const FullscreenIcon = () => (
|
||
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M3 3h6v2H5v4H3V3zm12 0h6v6h-2V5h-4V3zM3 15h2v4h4v2H3v-6zm16 4h-4v2h6v-6h-2v4z" />
|
||
</svg>
|
||
);
|
||
|
||
export const ExitFullscreenIcon = () => (
|
||
<svg width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M9 3H7v4H3v2h6V3zm6 0v6h6V7h-4V3h-2zM3 13v2h4v4h2v-6H3zm14 4v-4h2v6h-6v-2h4z" />
|
||
</svg>
|
||
);
|
||
|
||
type FullscreenButtonProps = {
|
||
isFullscreen: boolean;
|
||
onToggle: () => void;
|
||
};
|
||
export function FullscreenButton({ isFullscreen, onToggle }: FullscreenButtonProps) {
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant="Surface"
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={onToggle}
|
||
aria-label={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
||
outlined
|
||
>
|
||
{isFullscreen ? <ExitFullscreenIcon /> : <FullscreenIcon />}
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
type ScreenshareAudioButtonProps = {
|
||
muted: boolean;
|
||
onToggle: () => void;
|
||
};
|
||
export function ScreenshareAudioButton({ muted, onToggle }: ScreenshareAudioButtonProps) {
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{muted ? 'Unmute Screenshare Audio' : 'Mute Screenshare Audio'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={muted ? 'Warning' : 'Surface'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={onToggle}
|
||
aria-label={muted ? 'Unmute Screenshare Audio' : 'Mute Screenshare Audio'}
|
||
outlined
|
||
>
|
||
<Icon size="400" src={muted ? Icons.VolumeMute : Icons.VolumeHigh} filled={muted} />
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|
||
|
||
export function ChatButton() {
|
||
const [chat, setChat] = useAtom(callChatAtom);
|
||
|
||
return (
|
||
<TooltipProvider
|
||
position="Top"
|
||
delay={500}
|
||
tooltip={
|
||
<Tooltip>
|
||
<Text size="T200">{chat ? 'Close Chat' : 'Open Chat'}</Text>
|
||
</Tooltip>
|
||
}
|
||
>
|
||
{(anchorRef) => (
|
||
<IconButton
|
||
ref={anchorRef}
|
||
variant={chat ? 'Success' : 'Surface'}
|
||
fill="Soft"
|
||
radii="400"
|
||
size="400"
|
||
className={MobileTouchTarget}
|
||
onClick={() => setChat(!chat)}
|
||
aria-label={chat ? 'Close Chat' : 'Open Chat'}
|
||
outlined
|
||
>
|
||
<Icon size="400" src={Icons.Message} filled={chat} />
|
||
</IconButton>
|
||
)}
|
||
</TooltipProvider>
|
||
);
|
||
}
|