#265: in the compact layout (phones, narrow windows) the control bar stacked into four rows (~330px, 40% of a portrait phone). It is now one row: mic, deafen, camera, chat, soundboard, More and an icon-only End. Screenshare, its audio mute and fullscreen move into More; Share Screen is only offered where getDisplayMedia exists. (The self-view comes from the fork, lotus.27.) #277: "Mute Screenshare Audio" showed whenever screensharing was allowed, as a speaker icon that did nothing. It now shows while the fork reports remote screenshare audio (io.lotus.screenshare_audio_state), or while it is muted, so a hidden mute cannot silence the next share. Fixes #265 Fixes #277 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5d2d7ca63c
commit
3418a8865d
@@ -44,6 +44,7 @@ import { stopPropagation } from '../../utils/keyboard';
|
|||||||
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
import { useCallEmbedRef } from '../../hooks/useCallEmbed';
|
import { useCallEmbedRef } from '../../hooks/useCallEmbed';
|
||||||
import { pttActiveAtom } from '../../hooks/useCallHotkeys';
|
import { pttActiveAtom } from '../../hooks/useCallHotkeys';
|
||||||
|
import { SCREENSHARE_AUDIO_STATE } from '../../plugins/call/CallControl';
|
||||||
import { CallSoundboard } from './CallSoundboard';
|
import { CallSoundboard } from './CallSoundboard';
|
||||||
import { useRoomCallPolicy } from '../../hooks/useRoomCallPolicy';
|
import { useRoomCallPolicy } from '../../hooks/useRoomCallPolicy';
|
||||||
import { ScreenshareConfirm } from './ScreenshareConfirm';
|
import { ScreenshareConfirm } from './ScreenshareConfirm';
|
||||||
@@ -90,6 +91,21 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
|
|
||||||
const { microphone, video, sound, screenshare, spotlight, screenshareAudioMuted } =
|
const { microphone, video, sound, screenshare, spotlight, screenshareAudioMuted } =
|
||||||
useCallControlState(callEmbed.control);
|
useCallControlState(callEmbed.control);
|
||||||
|
// [Gitea #277] Only offer "Mute Screenshare Audio" while a remote share's
|
||||||
|
// audio is playing, or while it is muted (so a hidden mute can't silently
|
||||||
|
// swallow the next share).
|
||||||
|
const [remoteScreenshareAudio, setRemoteScreenshareAudio] = useState(
|
||||||
|
callEmbed.control.remoteScreenshareAudio,
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
const onState = () => setRemoteScreenshareAudio(callEmbed.control.remoteScreenshareAudio);
|
||||||
|
onState();
|
||||||
|
callEmbed.control.on(SCREENSHARE_AUDIO_STATE, onState);
|
||||||
|
return () => {
|
||||||
|
callEmbed.control.off(SCREENSHARE_AUDIO_STATE, onState);
|
||||||
|
};
|
||||||
|
}, [callEmbed]);
|
||||||
|
const showScreenshareAudio = remoteScreenshareAudio || screenshareAudioMuted;
|
||||||
const micLevel = useCallMicLevel(callEmbed);
|
const micLevel = useCallMicLevel(callEmbed);
|
||||||
|
|
||||||
const [cords, setCords] = useState<RectCords>();
|
const [cords, setCords] = useState<RectCords>();
|
||||||
@@ -122,6 +138,8 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
};
|
};
|
||||||
const showScreenshare = allowScreenshare || screenshare;
|
const showScreenshare = allowScreenshare || screenshare;
|
||||||
const showVideoGroup = showCamera || showScreenshare || !!document.fullscreenEnabled;
|
const showVideoGroup = showCamera || showScreenshare || !!document.fullscreenEnabled;
|
||||||
|
// [Gitea #265] Phones can't share their screen from a browser; don't offer it.
|
||||||
|
const canShareScreen = typeof navigator.mediaDevices?.getDisplayMedia === 'function';
|
||||||
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
|
||||||
setCords(evt.currentTarget.getBoundingClientRect());
|
setCords(evt.currentTarget.getBoundingClientRect());
|
||||||
};
|
};
|
||||||
@@ -166,6 +184,111 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
return () => clearTimeout(id);
|
return () => clearTimeout(id);
|
||||||
}, [hangupState.status, callEmbed, setCallEmbed]);
|
}, [hangupState.status, callEmbed, setCallEmbed]);
|
||||||
|
|
||||||
|
const moreMenu = (
|
||||||
|
<PopOut
|
||||||
|
anchor={cords}
|
||||||
|
position="Top"
|
||||||
|
align="Center"
|
||||||
|
content={
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: () => setCords(undefined),
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
||||||
|
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu>
|
||||||
|
<Box direction="Column" style={{ padding: config.space.S100 }}>
|
||||||
|
{compact && showScreenshare && screenshareMode !== 'hidden' && canShareScreen && (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
setCords(undefined);
|
||||||
|
handleScreenshareToggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{screenshare ? 'Stop Screenshare' : 'Share Screen'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{compact && showScreenshareAudio && (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
setCords(undefined);
|
||||||
|
callEmbed.control.toggleScreenshareAudio();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{screenshareAudioMuted ? 'Unmute Screenshare Audio' : 'Mute Screenshare Audio'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{compact && !!document.fullscreenEnabled && (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
variant="Surface"
|
||||||
|
radii="300"
|
||||||
|
onClick={() => {
|
||||||
|
setCords(undefined);
|
||||||
|
handleFullscreen();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem size="300" variant="Surface" radii="300" onClick={handleSpotlightClick}>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
{spotlight ? 'Grid View' : 'Spotlight View'}
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem size="300" variant="Surface" radii="300" onClick={handleReactionsClick}>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Reactions
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem size="300" variant="Surface" radii="300" onClick={handleSettingsClick}>
|
||||||
|
<Text size="B300" truncate>
|
||||||
|
Settings
|
||||||
|
</Text>
|
||||||
|
</MenuItem>
|
||||||
|
</Box>
|
||||||
|
</Menu>
|
||||||
|
</FocusTrap>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="Surface"
|
||||||
|
fill="Soft"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={handleOpenMenu}
|
||||||
|
outlined
|
||||||
|
aria-label="More options"
|
||||||
|
aria-expanded={!!cords}
|
||||||
|
aria-haspopup="menu"
|
||||||
|
>
|
||||||
|
<Icon size="400" src={Icons.VerticalDots} />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
);
|
||||||
|
|
||||||
|
const hangupIcon = exiting ? (
|
||||||
|
<Spinner variant="Critical" fill="Solid" size="200" />
|
||||||
|
) : (
|
||||||
|
<Icon src={Icons.PhoneDown} size="200" filled />
|
||||||
|
);
|
||||||
|
|
||||||
const pttKeyLabel = pttKey === 'Space' ? 'SPACE' : pttKey.replace('Key', '').replace('Digit', '');
|
const pttKeyLabel = pttKey === 'Space' ? 'SPACE' : pttKey.replace('Key', '').replace('Digit', '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -204,6 +327,44 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
}}
|
}}
|
||||||
onCancel={() => setShareConfirm(false)}
|
onCancel={() => setShareConfirm(false)}
|
||||||
/>
|
/>
|
||||||
|
{compact ? (
|
||||||
|
// [Gitea #265] Phones / narrow windows: ONE row. Screenshare, its audio
|
||||||
|
// mute and fullscreen live in More (they rarely matter on a phone), and
|
||||||
|
// End is icon-only. The old stacked layout took four rows (~40% of a
|
||||||
|
// portrait phone).
|
||||||
|
<SequenceCard
|
||||||
|
className={css.ControlCard}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
gap="200"
|
||||||
|
radii="500"
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="SpaceBetween"
|
||||||
|
wrap="NoWrap"
|
||||||
|
>
|
||||||
|
<MicrophoneButton
|
||||||
|
enabled={microphone}
|
||||||
|
onToggle={handleMicrophoneToggle}
|
||||||
|
level={micLevel}
|
||||||
|
/>
|
||||||
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
|
{showCamera && <VideoButton enabled={video} onToggle={handleVideoToggle} />}
|
||||||
|
<ChatButton />
|
||||||
|
{soundboardEnabled && <CallSoundboard callEmbed={callEmbed} />}
|
||||||
|
{moreMenu}
|
||||||
|
<Button
|
||||||
|
variant="Critical"
|
||||||
|
fill="Solid"
|
||||||
|
radii="400"
|
||||||
|
size="400"
|
||||||
|
onClick={hangup}
|
||||||
|
disabled={exiting}
|
||||||
|
aria-label="End Call"
|
||||||
|
style={{ paddingInline: config.space.S300 }}
|
||||||
|
>
|
||||||
|
{hangupIcon}
|
||||||
|
</Button>
|
||||||
|
</SequenceCard>
|
||||||
|
) : (
|
||||||
<SequenceCard
|
<SequenceCard
|
||||||
className={css.ControlCard}
|
className={css.ControlCard}
|
||||||
variant="SurfaceVariant"
|
variant="SurfaceVariant"
|
||||||
@@ -213,7 +374,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
justifyContent="SpaceBetween"
|
justifyContent="SpaceBetween"
|
||||||
wrap="Wrap"
|
wrap="Wrap"
|
||||||
>
|
>
|
||||||
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
<Box alignItems="Center" gap="Inherit" grow="Yes">
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
<MicrophoneButton
|
<MicrophoneButton
|
||||||
enabled={microphone}
|
enabled={microphone}
|
||||||
@@ -222,7 +383,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
/>
|
/>
|
||||||
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
<SoundButton enabled={sound} onToggle={() => callEmbed.control.toggleSound()} />
|
||||||
</Box>
|
</Box>
|
||||||
{!compact && showVideoGroup && <ControlDivider />}
|
{showVideoGroup && <ControlDivider />}
|
||||||
{showVideoGroup && (
|
{showVideoGroup && (
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
{/* Show a forbidden control while its track is still live so the
|
{/* Show a forbidden control while its track is still live so the
|
||||||
@@ -238,10 +399,12 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
)}
|
)}
|
||||||
{/* Mute-screenshare-audio sits directly next to the screenshare
|
{/* Mute-screenshare-audio sits directly next to the screenshare
|
||||||
control since they're the same concern. */}
|
control since they're the same concern. */}
|
||||||
|
{showScreenshareAudio && (
|
||||||
<ScreenshareAudioButton
|
<ScreenshareAudioButton
|
||||||
muted={screenshareAudioMuted}
|
muted={screenshareAudioMuted}
|
||||||
onToggle={() => callEmbed.control.toggleScreenshareAudio()}
|
onToggle={() => callEmbed.control.toggleScreenshareAudio()}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!!document.fullscreenEnabled && (
|
{!!document.fullscreenEnabled && (
|
||||||
@@ -250,77 +413,12 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
{!compact && <ControlDivider />}
|
<ControlDivider />
|
||||||
<Box alignItems="Center" gap="Inherit" grow="Yes" direction={compact ? 'Column' : 'Row'}>
|
<Box alignItems="Center" gap="Inherit" grow="Yes">
|
||||||
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
<Box shrink="No" alignItems="Inherit" justifyContent="Inherit" gap="200">
|
||||||
<ChatButton />
|
<ChatButton />
|
||||||
{soundboardEnabled && <CallSoundboard callEmbed={callEmbed} />}
|
{soundboardEnabled && <CallSoundboard callEmbed={callEmbed} />}
|
||||||
<PopOut
|
{moreMenu}
|
||||||
anchor={cords}
|
|
||||||
position="Top"
|
|
||||||
align="Center"
|
|
||||||
content={
|
|
||||||
<FocusTrap
|
|
||||||
focusTrapOptions={{
|
|
||||||
initialFocus: false,
|
|
||||||
onDeactivate: () => setCords(undefined),
|
|
||||||
clickOutsideDeactivates: true,
|
|
||||||
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
|
|
||||||
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
|
|
||||||
escapeDeactivates: stopPropagation,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Menu>
|
|
||||||
<Box direction="Column" style={{ padding: config.space.S100 }}>
|
|
||||||
<MenuItem
|
|
||||||
size="300"
|
|
||||||
variant="Surface"
|
|
||||||
radii="300"
|
|
||||||
onClick={handleSpotlightClick}
|
|
||||||
>
|
|
||||||
<Text size="B300" truncate>
|
|
||||||
{spotlight ? 'Grid View' : 'Spotlight View'}
|
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
size="300"
|
|
||||||
variant="Surface"
|
|
||||||
radii="300"
|
|
||||||
onClick={handleReactionsClick}
|
|
||||||
>
|
|
||||||
<Text size="B300" truncate>
|
|
||||||
Reactions
|
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
size="300"
|
|
||||||
variant="Surface"
|
|
||||||
radii="300"
|
|
||||||
onClick={handleSettingsClick}
|
|
||||||
>
|
|
||||||
<Text size="B300" truncate>
|
|
||||||
Settings
|
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
</Box>
|
|
||||||
</Menu>
|
|
||||||
</FocusTrap>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
variant="Surface"
|
|
||||||
fill="Soft"
|
|
||||||
radii="400"
|
|
||||||
size="400"
|
|
||||||
onClick={handleOpenMenu}
|
|
||||||
outlined
|
|
||||||
aria-label="More options"
|
|
||||||
aria-expanded={!!cords}
|
|
||||||
aria-haspopup="menu"
|
|
||||||
>
|
|
||||||
<Icon size="400" src={Icons.VerticalDots} />
|
|
||||||
</IconButton>
|
|
||||||
</PopOut>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box shrink="No" direction="Column">
|
<Box shrink="No" direction="Column">
|
||||||
<Button
|
<Button
|
||||||
@@ -328,13 +426,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
variant="Critical"
|
variant="Critical"
|
||||||
fill="Solid"
|
fill="Solid"
|
||||||
onClick={hangup}
|
onClick={hangup}
|
||||||
before={
|
before={hangupIcon}
|
||||||
exiting ? (
|
|
||||||
<Spinner variant="Critical" fill="Solid" size="200" />
|
|
||||||
) : (
|
|
||||||
<Icon src={Icons.PhoneDown} size="200" filled />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={exiting}
|
disabled={exiting}
|
||||||
>
|
>
|
||||||
<Text size="B400">End</Text>
|
<Text size="B400">End</Text>
|
||||||
@@ -342,6 +434,7 @@ export function CallControls({ callEmbed }: CallControlsProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ export function parseForkHotkeyReport(data: unknown): ForkHotkeyReport | null {
|
|||||||
|
|
||||||
export const CALL_ERROR_CHANGED = 'call_error_changed';
|
export const CALL_ERROR_CHANGED = 'call_error_changed';
|
||||||
|
|
||||||
|
export const SCREENSHARE_AUDIO_STATE = 'screenshare_audio_state';
|
||||||
|
|
||||||
|
/** Pure: parse the fork's `io.lotus.screenshare_audio_state` (#277). */
|
||||||
|
export function parseScreenshareAudioState(data: unknown): boolean | undefined {
|
||||||
|
if (typeof data !== 'object' || data === null) return undefined;
|
||||||
|
const { present } = data as { present?: unknown };
|
||||||
|
return typeof present === 'boolean' ? present : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export type AudioOutputOption = { id: string; label: string };
|
export type AudioOutputOption = { id: string; label: string };
|
||||||
export const AUDIO_OUTPUT_STATE = 'audio_output_state';
|
export const AUDIO_OUTPUT_STATE = 'audio_output_state';
|
||||||
|
|
||||||
@@ -281,6 +290,22 @@ export class CallControl extends EventEmitter implements CallControlState {
|
|||||||
this.emit(AUDIO_OUTPUT_STATE);
|
this.emit(AUDIO_OUTPUT_STATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Gitea #277] Whether a remote screenshare's audio is being received (the
|
||||||
|
// fork's `io.lotus.screenshare_audio_state`), so the call bar only offers
|
||||||
|
// "Mute Screenshare Audio" when there is something to mute.
|
||||||
|
private _remoteScreenshareAudio = false;
|
||||||
|
|
||||||
|
public get remoteScreenshareAudio(): boolean {
|
||||||
|
return this._remoteScreenshareAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onScreenshareAudioState(data: unknown) {
|
||||||
|
const present = parseScreenshareAudioState(data);
|
||||||
|
if (present === undefined || present === this._remoteScreenshareAudio) return;
|
||||||
|
this._remoteScreenshareAudio = present;
|
||||||
|
this.emit(SCREENSHARE_AUDIO_STATE);
|
||||||
|
}
|
||||||
|
|
||||||
private sendAudioOutput(deviceId: string): void {
|
private sendAudioOutput(deviceId: string): void {
|
||||||
if (!this.joined) return;
|
if (!this.joined) return;
|
||||||
this.call.transport.send('io.lotus.set_audio_output', { deviceId }).catch(() => undefined);
|
this.call.transport.send('io.lotus.set_audio_output', { deviceId }).catch(() => undefined);
|
||||||
|
|||||||
@@ -465,6 +465,12 @@ export class CallEmbed {
|
|||||||
this.control.onAudioOutputState((evt.detail as { data?: unknown } | undefined)?.data);
|
this.control.onAudioOutputState((evt.detail as { data?: unknown } | undefined)?.data);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
// [Gitea #277] Remote screenshare audio being received, or not.
|
||||||
|
this.disposables.push(
|
||||||
|
this.listenAction('io.lotus.screenshare_audio_state', (evt) => {
|
||||||
|
this.control.onScreenshareAudioState((evt.detail as { data?: unknown } | undefined)?.data);
|
||||||
|
}),
|
||||||
|
);
|
||||||
// [Gitea #43] PTT / deafen keys pressed while focus is inside the frame.
|
// [Gitea #43] PTT / deafen keys pressed while focus is inside the frame.
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
this.listenAction('io.lotus.hotkey', (evt) => {
|
this.listenAction('io.lotus.hotkey', (evt) => {
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { parseScreenshareAudioState } from './CallControl';
|
||||||
|
|
||||||
|
test('parseScreenshareAudioState: reads present, ignores anything else (#277)', () => {
|
||||||
|
assert.equal(parseScreenshareAudioState({ present: true }), true);
|
||||||
|
assert.equal(parseScreenshareAudioState({ present: false }), false);
|
||||||
|
assert.equal(parseScreenshareAudioState({ present: 'yes' }), undefined);
|
||||||
|
assert.equal(parseScreenshareAudioState({}), undefined);
|
||||||
|
assert.equal(parseScreenshareAudioState(null), undefined);
|
||||||
|
assert.equal(parseScreenshareAudioState('present'), undefined);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user