mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-09 18:29:21 +00:00
first step (starting to mix views and viewModels... this has to become
better)
This commit is contained in:
@@ -201,7 +201,6 @@
|
|||||||
"camera_numbered": "Camera {{n}}",
|
"camera_numbered": "Camera {{n}}",
|
||||||
"change_device_button": "Change audio device",
|
"change_device_button": "Change audio device",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"default_named": "Default <2>({{name}})</2>",
|
|
||||||
"handset": "Handset",
|
"handset": "Handset",
|
||||||
"loudspeaker": "Loudspeaker",
|
"loudspeaker": "Loudspeaker",
|
||||||
"microphone": "Microphone",
|
"microphone": "Microphone",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import { type FC, type JSX, type Ref, useMemo } from "react";
|
import { type FC, type JSX, type Ref, useMemo } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
|
||||||
import LogoMark from "../icons/LogoMark.svg?react";
|
import LogoMark from "../icons/LogoMark.svg?react";
|
||||||
import LogoType from "../icons/LogoType.svg?react";
|
import LogoType from "../icons/LogoType.svg?react";
|
||||||
@@ -29,7 +30,18 @@ import {
|
|||||||
MediaMuteAndSwitchButton,
|
MediaMuteAndSwitchButton,
|
||||||
type MenuOptions,
|
type MenuOptions,
|
||||||
} from "./MediaMuteAndSwitchButton";
|
} from "./MediaMuteAndSwitchButton";
|
||||||
|
import {
|
||||||
|
type AudioOutputDeviceLabel,
|
||||||
|
type DeviceLabel,
|
||||||
|
type MediaDevice,
|
||||||
|
type SelectedDevice,
|
||||||
|
} from "../state/MediaDevices";
|
||||||
|
import { mediaDeviceLabelToString } from "../settings/DeviceSelection";
|
||||||
|
import {
|
||||||
|
backgroundBlur as backgroundBlurSettings,
|
||||||
|
useSetting,
|
||||||
|
} from "../settings/settings";
|
||||||
|
import { useTrackProcessor } from "../livekit/TrackProcessorContext";
|
||||||
export interface AudioOutputSwitcher {
|
export interface AudioOutputSwitcher {
|
||||||
targetOutput: string;
|
targetOutput: string;
|
||||||
switch: () => void;
|
switch: () => void;
|
||||||
@@ -85,6 +97,19 @@ export interface FooterProps {
|
|||||||
selectedVideo?: string;
|
selectedVideo?: string;
|
||||||
selectAudioDevice?: (deviceId: string) => void;
|
selectAudioDevice?: (deviceId: string) => void;
|
||||||
selectVideoDevice?: (deviceId: string) => void;
|
selectVideoDevice?: (deviceId: string) => void;
|
||||||
|
/**
|
||||||
|
* If provided the footer will use the switchAndMute buttons.
|
||||||
|
* If not provided it will use the normal mute Buttons
|
||||||
|
*/
|
||||||
|
audioDevice?: MediaDevice<
|
||||||
|
DeviceLabel | AudioOutputDeviceLabel,
|
||||||
|
SelectedDevice
|
||||||
|
>;
|
||||||
|
/**
|
||||||
|
* If provided the footer will use the switchAndMute buttons.
|
||||||
|
* If not provided it will use the normal mute Buttons
|
||||||
|
*/
|
||||||
|
videoDevice?: MediaDevice<DeviceLabel, SelectedDevice>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CallFooter: FC<FooterProps> = ({
|
export const CallFooter: FC<FooterProps> = ({
|
||||||
@@ -111,13 +136,25 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
debugTileLayout,
|
debugTileLayout,
|
||||||
tileStoreGeneration,
|
tileStoreGeneration,
|
||||||
|
|
||||||
audioOptions,
|
audioDevice,
|
||||||
videoOptions,
|
videoDevice,
|
||||||
selectedAudio,
|
|
||||||
selectedVideo,
|
|
||||||
selectAudioDevice,
|
|
||||||
selectVideoDevice,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const videoOptions = useObservableEagerState(
|
||||||
|
videoDevice?.available$ ?? of(new Map()),
|
||||||
|
);
|
||||||
|
const selectedVideo = useObservableEagerState(
|
||||||
|
videoDevice?.selected$ ?? of(undefined),
|
||||||
|
);
|
||||||
|
const audioOptions = useObservableEagerState(
|
||||||
|
audioDevice?.available$ ?? of(new Map()),
|
||||||
|
);
|
||||||
|
const selectedAudio = useObservableEagerState(
|
||||||
|
audioDevice?.selected$ ?? of(undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { supported: blurSupported } = useTrackProcessor();
|
||||||
|
const [blurActive, setBlurActive] = useSetting(backgroundBlurSettings);
|
||||||
|
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
const buttonSize = asPip ? "md" : "lg";
|
const buttonSize = asPip ? "md" : "lg";
|
||||||
const showSettingsButton =
|
const showSettingsButton =
|
||||||
@@ -138,18 +175,24 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((audioOptions?.length ?? 0) > 0) {
|
if ((audioOptions?.size ?? 0) > 0) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<MediaMuteAndSwitchButton
|
<MediaMuteAndSwitchButton
|
||||||
title={"Mic Source"}
|
title={"Mic Source"}
|
||||||
key="audio"
|
key="audio"
|
||||||
iconsAndLabels="video"
|
iconsAndLabels="audio"
|
||||||
enabled={audioEnabled ?? false}
|
enabled={audioEnabled ?? false}
|
||||||
onMuteClick={toggleAudio}
|
onMuteClick={toggleAudio}
|
||||||
data-testid="incall_mute"
|
data-testid="incall_mute"
|
||||||
options={audioOptions}
|
options={Array.from(audioOptions.entries()).map(([k, v]) => {
|
||||||
selectedOption={selectedAudio}
|
const label = mediaDeviceLabelToString(v, (n) => "Audio Device " + n);
|
||||||
onSelect={selectAudioDevice}
|
return {
|
||||||
|
id: k,
|
||||||
|
label: label,
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
selectedOption={selectedAudio?.id}
|
||||||
|
onSelect={audioDevice?.select}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -164,18 +207,40 @@ export const CallFooter: FC<FooterProps> = ({
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ((videoOptions?.length ?? 0) > 0) {
|
if ((videoOptions?.size ?? 0) > 0) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<MediaMuteAndSwitchButton
|
<MediaMuteAndSwitchButton
|
||||||
title={"Camera Source"}
|
title={"Camera Source"}
|
||||||
key="audio"
|
key="video"
|
||||||
iconsAndLabels="audio"
|
iconsAndLabels="video"
|
||||||
enabled={videoEnabled ?? false}
|
enabled={videoEnabled ?? false}
|
||||||
onMuteClick={toggleVideo}
|
onMuteClick={toggleVideo}
|
||||||
data-testid="incall_mute"
|
data-testid="incall_mute"
|
||||||
options={videoOptions}
|
options={Array.from(videoOptions.entries()).map(([k, v]) => ({
|
||||||
selectedOption={selectedVideo}
|
id: k,
|
||||||
onSelect={selectVideoDevice}
|
label: v.type === "name" ? v.name : "Camera " + v.number,
|
||||||
|
}))}
|
||||||
|
toggles={
|
||||||
|
blurSupported
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "blur",
|
||||||
|
enabled: blurActive,
|
||||||
|
label: "Blur Background",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
selectedOption={selectedVideo?.id}
|
||||||
|
onSelect={(option) => {
|
||||||
|
switch (option) {
|
||||||
|
case "blur":
|
||||||
|
setBlurActive(!blurActive);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
videoDevice?.select(option);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
muted: muteAllAudio,
|
muted: muteAllAudio,
|
||||||
});
|
});
|
||||||
const latestPickupPhaseAudio = useLatest(pickupPhaseAudio);
|
const latestPickupPhaseAudio = useLatest(pickupPhaseAudio);
|
||||||
|
const mediaDevices = useMediaDevices();
|
||||||
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
||||||
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
||||||
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
||||||
@@ -597,6 +597,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
//Debug props
|
//Debug props
|
||||||
debugTileLayout={debugTileLayout}
|
debugTileLayout={debugTileLayout}
|
||||||
tileStoreGeneration={tileStoreGeneration}
|
tileStoreGeneration={tileStoreGeneration}
|
||||||
|
audioDevice={mediaDevices.audioInput}
|
||||||
|
videoDevice={mediaDevices.videoInput}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const allConnections = useBehavior(vm.allConnections$);
|
const allConnections = useBehavior(vm.allConnections$);
|
||||||
|
|||||||
@@ -236,6 +236,8 @@ export const LobbyView: FC<Props> = ({
|
|||||||
hangup={!confineToRoom ? onLeaveClick : undefined}
|
hangup={!confineToRoom ? onLeaveClick : undefined}
|
||||||
// Logo and header are connected. We will only show the logo in SPA with header.
|
// Logo and header are connected. We will only show the logo in SPA with header.
|
||||||
hideLogo={hideHeader}
|
hideLogo={hideHeader}
|
||||||
|
audioDevice={devices.audioInput}
|
||||||
|
videoDevice={devices.videoInput}
|
||||||
>
|
>
|
||||||
{recentsButtonInFooter && recentsButton}
|
{recentsButtonInFooter && recentsButton}
|
||||||
</CallFooter>
|
</CallFooter>
|
||||||
|
|||||||
@@ -5,14 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type ChangeEvent, type FC, useCallback, useId } from "react";
|
||||||
type ChangeEvent,
|
|
||||||
type FC,
|
|
||||||
type ReactElement,
|
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useId,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
Heading,
|
Heading,
|
||||||
InlineField,
|
InlineField,
|
||||||
@@ -20,8 +13,8 @@ import {
|
|||||||
RadioControl,
|
RadioControl,
|
||||||
Separator,
|
Separator,
|
||||||
} from "@vector-im/compound-web";
|
} from "@vector-im/compound-web";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type AudioOutputDeviceLabel,
|
type AudioOutputDeviceLabel,
|
||||||
@@ -37,12 +30,39 @@ interface Props {
|
|||||||
numberedLabel: (number: number) => string;
|
numberedLabel: (number: number) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mediaDeviceLabelToString(
|
||||||
|
label: DeviceLabel | AudioOutputDeviceLabel,
|
||||||
|
numberedLabel: (number: number) => string,
|
||||||
|
): string {
|
||||||
|
let labelText = "";
|
||||||
|
switch (label.type) {
|
||||||
|
case "name":
|
||||||
|
labelText = label.name;
|
||||||
|
break;
|
||||||
|
case "number":
|
||||||
|
labelText = numberedLabel(label.number);
|
||||||
|
break;
|
||||||
|
case "default":
|
||||||
|
labelText =
|
||||||
|
label.name === null
|
||||||
|
? t("settings.devices.default")
|
||||||
|
: t("settings.devices.default") + " (" + label.name + ")";
|
||||||
|
break;
|
||||||
|
case "speaker":
|
||||||
|
labelText = t("settings.devices.loudspeaker");
|
||||||
|
break;
|
||||||
|
case "earpiece":
|
||||||
|
labelText = t("settings.devices.handset");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return labelText;
|
||||||
|
}
|
||||||
|
|
||||||
export const DeviceSelection: FC<Props> = ({
|
export const DeviceSelection: FC<Props> = ({
|
||||||
device,
|
device,
|
||||||
title,
|
title,
|
||||||
numberedLabel,
|
numberedLabel,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const groupId = useId();
|
const groupId = useId();
|
||||||
const available = useObservableEagerState(device.available$);
|
const available = useObservableEagerState(device.available$);
|
||||||
const selectedId = useObservableEagerState(device.selected$)?.id;
|
const selectedId = useObservableEagerState(device.selected$)?.id;
|
||||||
@@ -70,38 +90,7 @@ export const DeviceSelection: FC<Props> = ({
|
|||||||
<Separator className={styles.separator} />
|
<Separator className={styles.separator} />
|
||||||
<div className={styles.options}>
|
<div className={styles.options}>
|
||||||
{[...available].map(([id, label]) => {
|
{[...available].map(([id, label]) => {
|
||||||
let labelText: ReactNode;
|
const labelText = mediaDeviceLabelToString(label, numberedLabel);
|
||||||
switch (label.type) {
|
|
||||||
case "name":
|
|
||||||
labelText = label.name;
|
|
||||||
break;
|
|
||||||
case "number":
|
|
||||||
labelText = numberedLabel(label.number);
|
|
||||||
break;
|
|
||||||
case "default":
|
|
||||||
labelText =
|
|
||||||
label.name === null ? (
|
|
||||||
t("settings.devices.default")
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
i18nKey="settings.devices.default_named"
|
|
||||||
name={label.name}
|
|
||||||
>
|
|
||||||
Default{" "}
|
|
||||||
<span className={styles.secondary}>
|
|
||||||
({{ name: label.name } as unknown as ReactElement})
|
|
||||||
</span>
|
|
||||||
</Trans>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "speaker":
|
|
||||||
labelText = t("settings.devices.loudspeaker");
|
|
||||||
break;
|
|
||||||
case "earpiece":
|
|
||||||
labelText = t("settings.devices.handset");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InlineField
|
<InlineField
|
||||||
key={id}
|
key={id}
|
||||||
|
|||||||
Reference in New Issue
Block a user