mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-24 00:38:31 +08:00
Refactor settings to use observables
Also removing some unused settings along the way.
This commit is contained in:
parent
20602c122b
commit
41083c0f9e
@ -66,6 +66,7 @@
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#238eea0ef5c82d0a11b8d5cc5c04104d6c94c4c1",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
"pako": "^2.0.4",
|
||||
"postcss-preset-env": "^9.0.0",
|
||||
"posthog-js": "^1.29.0",
|
||||
|
@ -140,7 +140,6 @@
|
||||
"feedback_tab_title": "Feedback",
|
||||
"more_tab_title": "More",
|
||||
"opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
|
||||
"show_connection_stats_label": "Show connection stats",
|
||||
"speaker_device_selection_label": "Speaker"
|
||||
},
|
||||
"star_rating_input_label_one": "{{count}} stars",
|
||||
|
@ -20,7 +20,6 @@ import { MatrixClient } from "matrix-js-sdk";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
import { widget } from "../widget";
|
||||
import { getSetting, setSetting, getSettingKey } from "../settings/useSetting";
|
||||
import {
|
||||
CallEndedTracker,
|
||||
CallStartedTracker,
|
||||
@ -35,7 +34,7 @@ import {
|
||||
} from "./PosthogEvents";
|
||||
import { Config } from "../config/Config";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { localStorageBus } from "../useLocalStorage";
|
||||
import { optInAnalytics } from "../settings/settings";
|
||||
|
||||
/* Posthog analytics tracking.
|
||||
*
|
||||
@ -131,7 +130,7 @@ export class PosthogAnalytics {
|
||||
const { analyticsID } = getUrlParams();
|
||||
// if the embedding platform (element web) already got approval to communicating with posthog
|
||||
// element call can also send events to posthog
|
||||
setSetting("opt-in-analytics", Boolean(analyticsID));
|
||||
optInAnalytics.setValue(Boolean(analyticsID));
|
||||
}
|
||||
|
||||
this.posthog.init(posthogConfig.project_api_key, {
|
||||
@ -151,9 +150,7 @@ export class PosthogAnalytics {
|
||||
);
|
||||
this.enabled = false;
|
||||
}
|
||||
this.startListeningToSettingsChanges();
|
||||
const optInAnalytics = getSetting("opt-in-analytics", false);
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
this.startListeningToSettingsChanges(); // Triggers maybeIdentifyUser
|
||||
}
|
||||
|
||||
private sanitizeProperties = (
|
||||
@ -336,8 +333,7 @@ export class PosthogAnalytics {
|
||||
}
|
||||
|
||||
public onLoginStatusChanged(): void {
|
||||
const optInAnalytics = getSetting("opt-in-analytics", false);
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
this.maybeIdentifyUser();
|
||||
}
|
||||
|
||||
private updateSuperProperties(): void {
|
||||
@ -360,20 +356,12 @@ export class PosthogAnalytics {
|
||||
return this.eventSignup.getSignupEndTime() > new Date(0);
|
||||
}
|
||||
|
||||
private async updateAnonymityAndIdentifyUser(
|
||||
pseudonymousOptIn: boolean,
|
||||
): Promise<void> {
|
||||
// Update this.anonymity based on the user's analytics opt-in settings
|
||||
const anonymity = pseudonymousOptIn
|
||||
? Anonymity.Pseudonymous
|
||||
: Anonymity.Disabled;
|
||||
this.setAnonymity(anonymity);
|
||||
|
||||
private async maybeIdentifyUser(): Promise<void> {
|
||||
// We may not yet have a Matrix client at this point, if not, bail. This should get
|
||||
// triggered again by onLoginStatusChanged once we do have a client.
|
||||
if (!window.matrixclient) return;
|
||||
|
||||
if (anonymity === Anonymity.Pseudonymous) {
|
||||
if (this.anonymity === Anonymity.Pseudonymous) {
|
||||
this.setRegistrationType(
|
||||
window.matrixclient.isGuest() || window.passwordlessUser
|
||||
? RegistrationType.Guest
|
||||
@ -389,7 +377,7 @@ export class PosthogAnalytics {
|
||||
}
|
||||
}
|
||||
|
||||
if (anonymity !== Anonymity.Disabled) {
|
||||
if (this.anonymity !== Anonymity.Disabled) {
|
||||
this.updateSuperProperties();
|
||||
}
|
||||
}
|
||||
@ -419,8 +407,9 @@ export class PosthogAnalytics {
|
||||
// * When the user changes their preferences on this device
|
||||
// Note that for new accounts, pseudonymousAnalyticsOptIn won't be set, so updateAnonymityFromSettings
|
||||
// won't be called (i.e. this.anonymity will be left as the default, until the setting changes)
|
||||
localStorageBus.on(getSettingKey("opt-in-analytics"), (optInAnalytics) => {
|
||||
this.updateAnonymityAndIdentifyUser(optInAnalytics);
|
||||
optInAnalytics.value.subscribe((optIn) => {
|
||||
this.setAnonymity(optIn ? Anonymity.Pseudonymous : Anonymity.Disabled);
|
||||
this.maybeIdentifyUser();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -38,9 +38,12 @@ import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { JoinExistingCallModal } from "./JoinExistingCallModal";
|
||||
import { Caption } from "../typography/Typography";
|
||||
import { Form } from "../form/Form";
|
||||
import { useOptInAnalytics } from "../settings/useSetting";
|
||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import {
|
||||
useSetting,
|
||||
optInAnalytics as optInAnalyticsSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@ -49,7 +52,7 @@ interface Props {
|
||||
export const RegisteredView: FC<Props> = ({ client }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [optInAnalytics] = useOptInAnalytics();
|
||||
const [optInAnalytics] = useSetting(optInAnalyticsSetting);
|
||||
const history = useHistory();
|
||||
const { t } = useTranslation();
|
||||
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
|
||||
|
@ -41,15 +41,18 @@ import styles from "./UnauthenticatedView.module.css";
|
||||
import commonStyles from "./common.module.css";
|
||||
import { generateRandomName } from "../auth/generateRandomName";
|
||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||
import { useOptInAnalytics } from "../settings/useSetting";
|
||||
import { Config } from "../config/Config";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import {
|
||||
useSetting,
|
||||
optInAnalytics as optInAnalyticsSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
export const UnauthenticatedView: FC = () => {
|
||||
const { setClient } = useClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [optInAnalytics] = useOptInAnalytics();
|
||||
const [optInAnalytics] = useSetting(optInAnalyticsSetting);
|
||||
const { recaptchaKey, register } = useInteractiveRegistration();
|
||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||
|
||||
|
@ -29,11 +29,12 @@ import { Observable } from "rxjs";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import {
|
||||
useSetting,
|
||||
audioInput as audioInputSetting,
|
||||
audioOutput as audioOutputSetting,
|
||||
videoInput as videoInputSetting,
|
||||
isFirefox,
|
||||
useAudioInput,
|
||||
useAudioOutput,
|
||||
useVideoInput,
|
||||
} from "../settings/useSetting";
|
||||
} from "../settings/settings";
|
||||
|
||||
export interface MediaDevice {
|
||||
available: MediaDeviceInfo[];
|
||||
@ -145,43 +146,36 @@ export const MediaDevicesProvider: FC<Props> = ({ children }) => {
|
||||
// for ouput devices because the selector wont be shown on FF.
|
||||
const useOutputNames = usingNames && !isFirefox();
|
||||
|
||||
const [audioInputSetting, setAudioInputSetting] = useAudioInput();
|
||||
const [audioOutputSetting, setAudioOutputSetting] = useAudioOutput();
|
||||
const [videoInputSetting, setVideoInputSetting] = useVideoInput();
|
||||
const [storedAudioInput, setStoredAudioInput] = useSetting(audioInputSetting);
|
||||
const [storedAudioOutput, setStoredAudioOutput] =
|
||||
useSetting(audioOutputSetting);
|
||||
const [storedVideoInput, setStoredVideoInput] = useSetting(videoInputSetting);
|
||||
|
||||
const audioInput = useMediaDevice(
|
||||
"audioinput",
|
||||
audioInputSetting,
|
||||
usingNames,
|
||||
);
|
||||
const audioInput = useMediaDevice("audioinput", storedAudioInput, usingNames);
|
||||
const audioOutput = useMediaDevice(
|
||||
"audiooutput",
|
||||
audioOutputSetting,
|
||||
storedAudioOutput,
|
||||
useOutputNames,
|
||||
alwaysUseDefaultAudio,
|
||||
);
|
||||
const videoInput = useMediaDevice(
|
||||
"videoinput",
|
||||
videoInputSetting,
|
||||
usingNames,
|
||||
);
|
||||
const videoInput = useMediaDevice("videoinput", storedVideoInput, usingNames);
|
||||
|
||||
useEffect(() => {
|
||||
if (audioInput.selectedId !== undefined)
|
||||
setAudioInputSetting(audioInput.selectedId);
|
||||
}, [setAudioInputSetting, audioInput.selectedId]);
|
||||
setStoredAudioInput(audioInput.selectedId);
|
||||
}, [setStoredAudioInput, audioInput.selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip setting state for ff output. Redundent since it is set to always return 'undefined'
|
||||
// but makes it clear while debugging that this is not happening on FF. + perf ;)
|
||||
if (audioOutput.selectedId !== undefined && !isFirefox())
|
||||
setAudioOutputSetting(audioOutput.selectedId);
|
||||
}, [setAudioOutputSetting, audioOutput.selectedId]);
|
||||
setStoredAudioOutput(audioOutput.selectedId);
|
||||
}, [setStoredAudioOutput, audioOutput.selectedId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoInput.selectedId !== undefined)
|
||||
setVideoInputSetting(videoInput.selectedId);
|
||||
}, [setVideoInputSetting, videoInput.selectedId]);
|
||||
setStoredVideoInput(videoInput.selectedId);
|
||||
}, [setStoredVideoInput, videoInput.selectedId]);
|
||||
|
||||
const startUsingDeviceNames = useCallback(
|
||||
() => setNumCallersUsingNames((n) => n + 1),
|
||||
|
@ -26,7 +26,6 @@ import { GroupCallLoader } from "./GroupCallLoader";
|
||||
import { GroupCallView } from "./GroupCallView";
|
||||
import { useRoomIdentifier, useUrlParams } from "../UrlParams";
|
||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||
import { useOptInAnalytics } from "../settings/useSetting";
|
||||
import { HomePage } from "../home/HomePage";
|
||||
import { platform } from "../Platform";
|
||||
import { AppSelectionModal } from "./AppSelectionModal";
|
||||
@ -36,6 +35,10 @@ import { LobbyView } from "./LobbyView";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { useMuteStates } from "./MuteStates";
|
||||
import {
|
||||
useSetting,
|
||||
optInAnalytics as optInAnalyticsSetting,
|
||||
} from "../settings/settings";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const {
|
||||
@ -80,7 +83,7 @@ export const RoomPage: FC = () => {
|
||||
registerPasswordlessUser,
|
||||
]);
|
||||
|
||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||
const [optInAnalytics, setOptInAnalytics] = useSetting(optInAnalyticsSetting);
|
||||
useEffect(() => {
|
||||
// During the beta, opt into analytics by default
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
|
@ -29,12 +29,6 @@ import OverflowIcon from "../icons/Overflow.svg?react";
|
||||
import UserIcon from "../icons/User.svg?react";
|
||||
import FeedbackIcon from "../icons/Feedback.svg?react";
|
||||
import { SelectInput } from "../input/SelectInput";
|
||||
import {
|
||||
useOptInAnalytics,
|
||||
useDeveloperSettingsTab,
|
||||
useShowConnectionStats,
|
||||
isFirefox,
|
||||
} from "./useSetting";
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Body, Caption } from "../typography/Typography";
|
||||
import { AnalyticsNotice } from "../analytics/AnalyticsNotice";
|
||||
@ -46,6 +40,12 @@ import {
|
||||
useMediaDeviceNames,
|
||||
} from "../livekit/MediaDevicesContext";
|
||||
import { widget } from "../widget";
|
||||
import {
|
||||
useSetting,
|
||||
optInAnalytics as optInAnalyticsSetting,
|
||||
developerSettingsTab as developerSettingsTabSetting,
|
||||
isFirefox,
|
||||
} from "./settings";
|
||||
|
||||
type SettingsTab =
|
||||
| "audio"
|
||||
@ -76,11 +76,10 @@ export const SettingsModal: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [optInAnalytics, setOptInAnalytics] = useOptInAnalytics();
|
||||
const [developerSettingsTab, setDeveloperSettingsTab] =
|
||||
useDeveloperSettingsTab();
|
||||
const [showConnectionStats, setShowConnectionStats] =
|
||||
useShowConnectionStats();
|
||||
const [optInAnalytics, setOptInAnalytics] = useSetting(optInAnalyticsSetting);
|
||||
const [developerSettingsTab, setDeveloperSettingsTab] = useSetting(
|
||||
developerSettingsTabSetting,
|
||||
);
|
||||
|
||||
// Generate a `SelectInput` with a list of devices for a given device kind.
|
||||
const generateDeviceSelection = (
|
||||
@ -245,18 +244,6 @@ export const SettingsModal: FC<Props> = ({
|
||||
})}
|
||||
</Body>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="showConnectionStats"
|
||||
name="connection-stats"
|
||||
label={t("settings.show_connection_stats_label")}
|
||||
type="checkbox"
|
||||
checked={showConnectionStats}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void =>
|
||||
setShowConnectionStats(e.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
</TabItem>
|
||||
);
|
||||
|
||||
|
98
src/settings/settings.ts
Normal file
98
src/settings/settings.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
|
||||
export class Setting<T> {
|
||||
public constructor(key: string, defaultValue: T) {
|
||||
this.key = `matrix-setting-${key}`;
|
||||
|
||||
const storedValue = localStorage.getItem(this.key);
|
||||
let initialValue = defaultValue;
|
||||
if (storedValue !== null) {
|
||||
try {
|
||||
initialValue = JSON.parse(storedValue);
|
||||
} catch (e) {
|
||||
logger.warn(`Invalid value stored for setting ${key}: ${storedValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._value = new BehaviorSubject(initialValue);
|
||||
this.value = this._value;
|
||||
}
|
||||
|
||||
private readonly key: string;
|
||||
|
||||
private readonly _value: BehaviorSubject<T>;
|
||||
public readonly value: Observable<T>;
|
||||
|
||||
public readonly setValue = (value: T): void => {
|
||||
this._value.next(value);
|
||||
localStorage.setItem(this.key, JSON.stringify(value));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that returns a settings's current value and a setter.
|
||||
*/
|
||||
export function useSetting<T>(setting: Setting<T>): [T, (value: T) => void] {
|
||||
return [useObservableEagerState(setting.value), setting.setValue];
|
||||
}
|
||||
|
||||
// TODO: This doesn't belong here
|
||||
export const isFirefox = (): boolean => {
|
||||
const { userAgent } = navigator;
|
||||
return userAgent.includes("Firefox");
|
||||
};
|
||||
|
||||
// null = undecided
|
||||
export const optInAnalytics = new Setting<boolean | null>(
|
||||
"opt-in-analytics",
|
||||
null,
|
||||
);
|
||||
// TODO: This setting can be disabled. Work out an approach to disableable
|
||||
// settings thats works for Observables in addition to React.
|
||||
export const useOptInAnalytics = (): [
|
||||
boolean | null,
|
||||
((value: boolean | null) => void) | null,
|
||||
] => {
|
||||
const setting = useSetting(optInAnalytics);
|
||||
if (PosthogAnalytics.instance.isEnabled()) return setting;
|
||||
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
export const developerSettingsTab = new Setting(
|
||||
"developer-settings-tab",
|
||||
false,
|
||||
);
|
||||
|
||||
export const audioInput = new Setting<string | undefined>(
|
||||
"audio-input",
|
||||
undefined,
|
||||
);
|
||||
export const audioOutput = new Setting<string | undefined>(
|
||||
"audio-output",
|
||||
undefined,
|
||||
);
|
||||
export const videoInput = new Setting<string | undefined>(
|
||||
"video-input",
|
||||
undefined,
|
||||
);
|
@ -1,104 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 - 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import {
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
useLocalStorage,
|
||||
} from "../useLocalStorage";
|
||||
|
||||
type Setting<T> = [T, (value: T) => void];
|
||||
type DisableableSetting<T> = [T, ((value: T) => void) | null];
|
||||
|
||||
export const getSettingKey = (name: string): string => {
|
||||
return `matrix-setting-${name}`;
|
||||
};
|
||||
// Like useState, but reads from and persists the value to localStorage
|
||||
export const useSetting = <T>(name: string, defaultValue: T): Setting<T> => {
|
||||
const key = useMemo(() => getSettingKey(name), [name]);
|
||||
|
||||
const [item, setItem] = useLocalStorage(key);
|
||||
|
||||
const value = useMemo(
|
||||
() => (item == null ? defaultValue : JSON.parse(item)),
|
||||
[item, defaultValue],
|
||||
);
|
||||
const setValue = useCallback(
|
||||
(value: T) => {
|
||||
setItem(JSON.stringify(value));
|
||||
},
|
||||
[setItem],
|
||||
);
|
||||
|
||||
return [value, setValue];
|
||||
};
|
||||
|
||||
export const getSetting = <T>(name: string, defaultValue: T): T => {
|
||||
const item = getLocalStorageItem(getSettingKey(name));
|
||||
return item === null ? defaultValue : JSON.parse(item);
|
||||
};
|
||||
|
||||
export const setSetting = <T>(name: string, newValue: T): void =>
|
||||
setLocalStorageItem(getSettingKey(name), JSON.stringify(newValue));
|
||||
|
||||
export const isFirefox = (): boolean => {
|
||||
const { userAgent } = navigator;
|
||||
return userAgent.includes("Firefox");
|
||||
};
|
||||
|
||||
const canEnableSpatialAudio = (): boolean => {
|
||||
// Spatial audio means routing audio through audio contexts. On Chrome,
|
||||
// this bypasses the AEC processor and so breaks echo cancellation.
|
||||
// We only allow spatial audio to be enabled on Firefox which we know
|
||||
// passes audio context audio through the AEC algorithm.
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=687574 is the
|
||||
// chrome bug for this: once this is fixed and the updated version is deployed
|
||||
// widely enough, we can allow spatial audio everywhere. It's currently in a
|
||||
// chrome flag, so we could enable this in Electron if we enabled the chrome flag
|
||||
// in the Electron wrapper.
|
||||
return isFirefox();
|
||||
};
|
||||
|
||||
export const useSpatialAudio = (): DisableableSetting<boolean> => {
|
||||
const settingVal = useSetting("spatial-audio", false);
|
||||
if (canEnableSpatialAudio()) return settingVal;
|
||||
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
// null = undecided
|
||||
export const useOptInAnalytics = (): DisableableSetting<boolean | null> => {
|
||||
const settingVal = useSetting<boolean | null>("opt-in-analytics", null);
|
||||
if (PosthogAnalytics.instance.isEnabled()) return settingVal;
|
||||
|
||||
return [false, null];
|
||||
};
|
||||
|
||||
export const useDeveloperSettingsTab = (): Setting<boolean> =>
|
||||
useSetting("developer-settings-tab", false);
|
||||
|
||||
export const useShowConnectionStats = (): Setting<boolean> =>
|
||||
useSetting("show-connection-stats", false);
|
||||
|
||||
export const useAudioInput = (): Setting<string | undefined> =>
|
||||
useSetting<string | undefined>("audio-input", undefined);
|
||||
export const useAudioOutput = (): Setting<string | undefined> =>
|
||||
useSetting<string | undefined>("audio-output", undefined);
|
||||
export const useVideoInput = (): Setting<string | undefined> =>
|
||||
useSetting<string | undefined>("video-input", undefined);
|
@ -7104,6 +7104,11 @@ object.values@^1.1.7:
|
||||
define-properties "^1.2.0"
|
||||
es-abstract "^1.22.1"
|
||||
|
||||
observable-hooks@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/observable-hooks/-/observable-hooks-4.2.3.tgz#69e3353caafd7887ad9030bd440b053304e8d2d1"
|
||||
integrity sha512-d6fYTIU+9sg1V+CT0GhgoE/ntjIqcy9DGaYGE6ELGVP4ojaWIEsaLvL/05hLOM+AL7aySN4DCTLvj6dDF9T8XA==
|
||||
|
||||
oidc-client-ts@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7"
|
||||
|
Loading…
Reference in New Issue
Block a user