mirror of
https://github.com/vector-im/element-call.git
synced 2026-05-07 10:14:36 +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}}",
|
||||
"change_device_button": "Change audio device",
|
||||
"default": "Default",
|
||||
"default_named": "Default <2>({{name}})</2>",
|
||||
"handset": "Handset",
|
||||
"loudspeaker": "Loudspeaker",
|
||||
"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 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 LogoType from "../icons/LogoType.svg?react";
|
||||
@@ -29,7 +30,18 @@ import {
|
||||
MediaMuteAndSwitchButton,
|
||||
type MenuOptions,
|
||||
} 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 {
|
||||
targetOutput: string;
|
||||
switch: () => void;
|
||||
@@ -85,6 +97,19 @@ export interface FooterProps {
|
||||
selectedVideo?: string;
|
||||
selectAudioDevice?: (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> = ({
|
||||
@@ -111,13 +136,25 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
debugTileLayout,
|
||||
tileStoreGeneration,
|
||||
|
||||
audioOptions,
|
||||
videoOptions,
|
||||
selectedAudio,
|
||||
selectedVideo,
|
||||
selectAudioDevice,
|
||||
selectVideoDevice,
|
||||
audioDevice,
|
||||
videoDevice,
|
||||
}) => {
|
||||
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 buttonSize = asPip ? "md" : "lg";
|
||||
const showSettingsButton =
|
||||
@@ -138,18 +175,24 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if ((audioOptions?.length ?? 0) > 0) {
|
||||
if ((audioOptions?.size ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Mic Source"}
|
||||
key="audio"
|
||||
iconsAndLabels="video"
|
||||
iconsAndLabels="audio"
|
||||
enabled={audioEnabled ?? false}
|
||||
onMuteClick={toggleAudio}
|
||||
data-testid="incall_mute"
|
||||
options={audioOptions}
|
||||
selectedOption={selectedAudio}
|
||||
onSelect={selectAudioDevice}
|
||||
options={Array.from(audioOptions.entries()).map(([k, v]) => {
|
||||
const label = mediaDeviceLabelToString(v, (n) => "Audio Device " + n);
|
||||
return {
|
||||
id: k,
|
||||
label: label,
|
||||
};
|
||||
})}
|
||||
selectedOption={selectedAudio?.id}
|
||||
onSelect={audioDevice?.select}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
@@ -164,18 +207,40 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
/>,
|
||||
);
|
||||
}
|
||||
if ((videoOptions?.length ?? 0) > 0) {
|
||||
if ((videoOptions?.size ?? 0) > 0) {
|
||||
buttons.push(
|
||||
<MediaMuteAndSwitchButton
|
||||
title={"Camera Source"}
|
||||
key="audio"
|
||||
iconsAndLabels="audio"
|
||||
key="video"
|
||||
iconsAndLabels="video"
|
||||
enabled={videoEnabled ?? false}
|
||||
onMuteClick={toggleVideo}
|
||||
data-testid="incall_mute"
|
||||
options={videoOptions}
|
||||
selectedOption={selectedVideo}
|
||||
onSelect={selectVideoDevice}
|
||||
options={Array.from(videoOptions.entries()).map(([k, v]) => ({
|
||||
id: k,
|
||||
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 {
|
||||
|
||||
@@ -220,7 +220,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
muted: muteAllAudio,
|
||||
});
|
||||
const latestPickupPhaseAudio = useLatest(pickupPhaseAudio);
|
||||
|
||||
const mediaDevices = useMediaDevices();
|
||||
const audioEnabled = useBehavior(muteStates.audio.enabled$);
|
||||
const videoEnabled = useBehavior(muteStates.video.enabled$);
|
||||
const toggleAudio = useBehavior(muteStates.audio.toggle$);
|
||||
@@ -597,6 +597,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
//Debug props
|
||||
debugTileLayout={debugTileLayout}
|
||||
tileStoreGeneration={tileStoreGeneration}
|
||||
audioDevice={mediaDevices.audioInput}
|
||||
videoDevice={mediaDevices.videoInput}
|
||||
/>
|
||||
);
|
||||
const allConnections = useBehavior(vm.allConnections$);
|
||||
|
||||
@@ -236,6 +236,8 @@ export const LobbyView: FC<Props> = ({
|
||||
hangup={!confineToRoom ? onLeaveClick : undefined}
|
||||
// Logo and header are connected. We will only show the logo in SPA with header.
|
||||
hideLogo={hideHeader}
|
||||
audioDevice={devices.audioInput}
|
||||
videoDevice={devices.videoInput}
|
||||
>
|
||||
{recentsButtonInFooter && recentsButton}
|
||||
</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.
|
||||
*/
|
||||
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type FC,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useId,
|
||||
} from "react";
|
||||
import { type ChangeEvent, type FC, useCallback, useId } from "react";
|
||||
import {
|
||||
Heading,
|
||||
InlineField,
|
||||
@@ -20,8 +13,8 @@ import {
|
||||
RadioControl,
|
||||
Separator,
|
||||
} from "@vector-im/compound-web";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import { t } from "i18next";
|
||||
|
||||
import {
|
||||
type AudioOutputDeviceLabel,
|
||||
@@ -37,12 +30,39 @@ interface Props {
|
||||
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> = ({
|
||||
device,
|
||||
title,
|
||||
numberedLabel,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const groupId = useId();
|
||||
const available = useObservableEagerState(device.available$);
|
||||
const selectedId = useObservableEagerState(device.selected$)?.id;
|
||||
@@ -70,38 +90,7 @@ export const DeviceSelection: FC<Props> = ({
|
||||
<Separator className={styles.separator} />
|
||||
<div className={styles.options}>
|
||||
{[...available].map(([id, label]) => {
|
||||
let labelText: ReactNode;
|
||||
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;
|
||||
}
|
||||
|
||||
const labelText = mediaDeviceLabelToString(label, numberedLabel);
|
||||
return (
|
||||
<InlineField
|
||||
key={id}
|
||||
|
||||
Reference in New Issue
Block a user