/* Copyright 2022-2024 New Vector Ltd. 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 ReactNode, useEffect, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk"; import { Button, Heading, Root as Form, Separator } from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; import { type Tab, TabContainer } from "../tabs/Tabs"; import { ProfileSettingsTab } from "./ProfileSettingsTab"; import { FeedbackSettingsTab } from "./FeedbackSettingsTab"; import { iosDeviceMenu$ } from "../state/MediaDevices"; import { useMediaDevices } from "../MediaDevicesContext"; import { widget } from "../widget"; import { useSetting, soundEffectVolume as soundEffectVolumeSetting, backgroundBlur as backgroundBlurSetting, developerMode, noiseGateEnabled as noiseGateEnabledSetting, noiseGateThreshold as noiseGateThresholdSetting, noiseGateAttack as noiseGateAttackSetting, noiseGateHold as noiseGateHoldSetting, noiseGateRelease as noiseGateReleaseSetting, transientSuppressorEnabled as transientSuppressorEnabledSetting, transientThreshold as transientThresholdSetting, transientRelease as transientReleaseSetting, } from "./settings"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; import { NoiseLevelSlider } from "./NoiseLevelSlider"; import { DeviceSelection } from "./DeviceSelection"; import { useTrackProcessor } from "../livekit/TrackProcessorContext"; import { DeveloperSettingsTab } from "./DeveloperSettingsTab"; import { FieldRow, InputField } from "../input/Input"; import { useSubmitRageshake } from "./submit-rageshake"; import { useUrlParams } from "../UrlParams"; import { useBehavior } from "../useBehavior"; type SettingsTab = | "audio" | "video" | "profile" | "preferences" | "feedback" | "more" | "developer"; interface Props { open: boolean; onDismiss: () => void; tab: SettingsTab; onTabChange: (tab: SettingsTab) => void; client: MatrixClient; roomId?: string; livekitRooms?: { room: LivekitRoom; url: string; isLocal?: boolean; }[]; } export const defaultSettingsTab: SettingsTab = "audio"; export const SettingsModal: FC = ({ open, onDismiss, tab, onTabChange, client, roomId, livekitRooms, }) => { const { t } = useTranslation(); // Generate a `Checkbox` input to turn blur on or off. const BlurCheckbox: React.FC = (): ReactNode => { const { supported } = useTrackProcessor(); const [blurActive, setBlurActive] = useSetting(backgroundBlurSetting); return ( <>

{t("settings.background_blur_header")}

setBlurActive(b.target.checked)} disabled={!supported} /> ); }; const devices = useMediaDevices(); useEffect(() => { if (open) devices.requestDeviceNames(); }, [open, devices]); const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting); const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume); const [showDeveloperSettingsTab] = useSetting(developerMode); // Noise gate settings const [noiseGateEnabled, setNoiseGateEnabled] = useSetting(noiseGateEnabledSetting); const [noiseGateThreshold, setNoiseGateThreshold] = useSetting(noiseGateThresholdSetting); const [noiseGateThresholdRaw, setNoiseGateThresholdRaw] = useState(noiseGateThreshold); const [noiseGateAttack, setNoiseGateAttack] = useSetting(noiseGateAttackSetting); const [noiseGateAttackRaw, setNoiseGateAttackRaw] = useState(noiseGateAttack); const [noiseGateHold, setNoiseGateHold] = useSetting(noiseGateHoldSetting); const [noiseGateHoldRaw, setNoiseGateHoldRaw] = useState(noiseGateHold); const [noiseGateRelease, setNoiseGateRelease] = useSetting(noiseGateReleaseSetting); const [noiseGateReleaseRaw, setNoiseGateReleaseRaw] = useState(noiseGateRelease); const [showAdvancedGate, setShowAdvancedGate] = useState(false); // Transient suppressor settings const [transientEnabled, setTransientEnabled] = useSetting(transientSuppressorEnabledSetting); const [transientThreshold, setTransientThreshold] = useSetting(transientThresholdSetting); const [transientThresholdRaw, setTransientThresholdRaw] = useState(transientThreshold); const [transientRelease, setTransientRelease] = useSetting(transientReleaseSetting); const [transientReleaseRaw, setTransientReleaseRaw] = useState(transientRelease); const resetGateDefaults = useCallback((): void => { const a = noiseGateAttackSetting.defaultValue; const h = noiseGateHoldSetting.defaultValue; const r = noiseGateReleaseSetting.defaultValue; setNoiseGateAttack(a); setNoiseGateAttackRaw(a); setNoiseGateHold(h); setNoiseGateHoldRaw(h); setNoiseGateRelease(r); setNoiseGateReleaseRaw(r); }, [setNoiseGateAttack, setNoiseGateHold, setNoiseGateRelease]); const { available: isRageshakeAvailable } = useSubmitRageshake(); // For controlled devices, we will not show the input section: // Controlled media devices are used on mobile platforms, where input and output are grouped into // a single device. These are called "headset" or "speaker" (or similar) but contain both input and output. // On EC, we decided that it is less confusing for the user if they see those options in the output section // rather than the input section. const { controlledAudioDevices } = useUrlParams(); // If we are on iOS we will show a button to open the native audio device picker. const iosDeviceMenu = useBehavior(iosDeviceMenu$); const audioTab: Tab = { key: "audio", name: t("common.audio"), content: ( <>
{!controlledAudioDevices && ( t("settings.devices.microphone_numbered", { n }) } /> )} {iosDeviceMenu && controlledAudioDevices && ( )} t("settings.devices.speaker_numbered", { n })} />

{t("settings.audio_tab.effect_volume_description")}

Noise Gate ): void => setNoiseGateEnabled(e.target.checked) } /> {noiseGateEnabled && ( <>
Threshold

Gate opens above this level, closes below it.

{showAdvancedGate && ( <>

How quickly the gate opens when signal exceeds threshold.

How long the gate stays open after signal drops below threshold.

How quickly the gate closes after hold expires.

)}
)}
Transient Suppressor ): void => setTransientEnabled(e.target.checked) } /> {transientEnabled && ( <>
Sensitivity: {transientThresholdRaw} dB above background

Lower values catch more impacts; higher values only catch the loudest ones.

Release: {transientReleaseRaw} ms

How quickly audio returns after suppression.

)}
), }; const videoTab: Tab = { key: "video", name: t("common.video"), content: ( <>
t("settings.devices.camera_numbered", { n })} /> ), }; const preferencesTab: Tab = { key: "preferences", name: t("common.preferences"), content: , }; const profileTab: Tab = { key: "profile", name: t("common.profile"), content: , }; const feedbackTab: Tab = { key: "feedback", name: t("settings.feedback_tab_title"), content: , }; const developerTab: Tab = { key: "developer", name: t("settings.developer_tab_title"), content: ( ), }; const tabs = [audioTab, videoTab]; if (widget === null) tabs.push(profileTab); tabs.push(preferencesTab); if (isRageshakeAvailable || import.meta.env.VITE_PACKAGE === "full") { // for full package we want to show the analytics consent checkbox // even if rageshake is not available tabs.push(feedbackTab); } if (showDeveloperSettingsTab) tabs.push(developerTab); return ( ); };