Refactor settings to use observables
Also removing some unused settings along the way.
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user