Harden Settings using mapped types (#28775)

* Harden Settings using mapped types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix issues found during hardening

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove oidc native flow stale key

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/28813/head
Michael Telatynski 2024-12-23 20:25:15 +00:00 committed by GitHub
parent 4e1bd69e4d
commit 1e42f28a69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 576 additions and 274 deletions

View File

@ -19,6 +19,10 @@ ignore.push("/OpenSpotlightPayload.ts");
ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.ts");
ignore.push("/json.ts");
ignore.push("/ReleaseAnnouncementStore.ts");
ignore.push("/WidgetLayoutStore.ts");
ignore.push("/common.ts");
// We ignore js-sdk by default as it may export for other non element-web projects
if (!includeJSSDK) ignore.push("matrix-js-sdk");

View File

@ -44,3 +44,11 @@ type DeepReadonlyObject<T> = {
};
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
/**
* Returns a union type of the keys of the input Object type whose values are assignable to the given Item type.
* Based on https://stackoverflow.com/a/57862073
*/
export type Assignable<Object, Item> = {
[Key in keyof Object]: Object[Key] extends Item ? Key : never;
}[keyof Object];

13
src/@types/json.ts Normal file
View File

@ -0,0 +1,13 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
export type JsonValue = null | string | number | boolean;
export type JsonArray = Array<JsonValue | JsonObject | JsonArray>;
export interface JsonObject {
[key: string]: JsonObject | JsonArray | JsonValue;
}
export type Json = JsonArray | JsonObject;

View File

@ -176,7 +176,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
url: string;
name: string;
type: string;
size: string;
size: number;
} | null {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.

View File

@ -246,7 +246,7 @@ class LoggedInView extends React.Component<IProps, IState> {
} else {
backgroundImage = OwnProfileStore.instance.getHttpAvatarUrl();
}
this.setState({ backgroundImage });
this.setState({ backgroundImage: backgroundImage ?? undefined });
};
public canResetTimelineInRoom = (roomId: string): boolean => {

View File

@ -20,12 +20,13 @@ import SettingsFlag from "../elements/SettingsFlag";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import InlineSpinner from "../elements/InlineSpinner";
import { shouldShowFeedback } from "../../../utils/Feedback";
import { FeatureSettingKey } from "../../../settings/Settings.tsx";
// XXX: Keep this around for re-use in future Betas
interface IProps {
title?: string;
featureId: string;
featureId: FeatureSettingKey;
}
interface IBetaPillProps {

View File

@ -282,7 +282,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
}
})();
const developerModeEnabled = useSettingValue<boolean>("developerMode");
const developerModeEnabled = useSettingValue("developerMode");
const developerToolsOption = developerModeEnabled ? (
<DeveloperToolsOption onFinished={onFinished} roomId={room.roomId} />
) : null;

View File

@ -71,7 +71,7 @@ const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() =>
const showSnapshotButton = (widgetMessaging: ClientWidgetApi | undefined): boolean => {
return (
SettingsStore.getValue<boolean>("enableWidgetScreenshots") &&
SettingsStore.getValue("enableWidgetScreenshots") &&
!!widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)
);
};

View File

@ -131,7 +131,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
const msc3946ProcessDynamicPredecessor = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors");
const visibleRooms = useMemo(
() =>
cli

View File

@ -15,11 +15,12 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
import { SettingKey } from "../../../settings/Settings.tsx";
// XXX: Keep this around for re-use in future Betas
interface IProps {
featureId: string;
featureId: SettingKey;
onFinished(sendFeedback?: boolean): void;
}
@ -35,7 +36,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
rageshakeLabel={info.feedbackLabel}
rageshakeData={Object.fromEntries(
(SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map((k) => {
return SettingsStore.getValue(k);
return [k, SettingsStore.getValue(k)];
}),
)}
>

View File

@ -253,8 +253,8 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
const previewLayout = useSettingValue<Layout>("layout");
const msc3946DynamicRoomPredecessors = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const previewLayout = useSettingValue("layout");
const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors");
let rooms = useMemo(
() =>

View File

@ -100,8 +100,8 @@ type ShareDialogProps = XOR<Props, EventProps>;
* A dialog to share a link to a room, user, room member or a matrix event.
*/
export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element {
const showQrCode = useSettingValue<boolean>(UIFeature.ShareQRCode);
const showSocials = useSettingValue<boolean>(UIFeature.ShareSocial);
const showQrCode = useSettingValue(UIFeature.ShareQRCode);
const showSocials = useSettingValue(UIFeature.ShareSocial);
const timeoutIdRef = useRef<number>();
const [isCopied, setIsCopied] = useState(false);

View File

@ -85,8 +85,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
}
export default function UserSettingsDialog(props: IProps): JSX.Element {
const voipEnabled = useSettingValue<boolean>(UIFeature.Voip);
const mjolnirEnabled = useSettingValue<boolean>("feature_mjolnir");
const voipEnabled = useSettingValue(UIFeature.Voip);
const mjolnirEnabled = useSettingValue("feature_mjolnir");
// store this prop in state as changing tabs back and forth should clear it
const [showMsc4108QrCode, setShowMsc4108QrCode] = useState(props.showMsc4108QrCode);

View File

@ -15,11 +15,11 @@ import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsStore, { LEVEL_ORDER } from "../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../settings/SettingLevel";
import { SETTINGS } from "../../../../settings/Settings";
import { SettingKey, SETTINGS, SettingValueType } from "../../../../settings/Settings";
import Field from "../../elements/Field";
const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
const [setting, setSetting] = useState<string | null>(null);
const [setting, setSetting] = useState<SettingKey | null>(null);
const [editing, setEditing] = useState(false);
if (setting && editing) {
@ -36,10 +36,10 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
};
return <ViewSetting setting={setting} onBack={onBack} onEdit={onEdit} />;
} else {
const onView = (setting: string): void => {
const onView = (setting: SettingKey): void => {
setSetting(setting);
};
const onEdit = (setting: string): void => {
const onEdit = (setting: SettingKey): void => {
setSetting(setting);
setEditing(true);
};
@ -50,7 +50,7 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
export default SettingExplorer;
interface ICanEditLevelFieldProps {
setting: string;
setting: SettingKey;
level: SettingLevel;
roomId?: string;
}
@ -65,8 +65,8 @@ const CanEditLevelField: React.FC<ICanEditLevelFieldProps> = ({ setting, roomId,
);
};
function renderExplicitSettingValues(setting: string, roomId?: string): string {
const vals: Record<string, number | null> = {};
function renderExplicitSettingValues(setting: SettingKey, roomId?: string): string {
const vals: Record<string, SettingValueType> = {};
for (const level of LEVEL_ORDER) {
try {
vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
@ -81,7 +81,7 @@ function renderExplicitSettingValues(setting: string, roomId?: string): string {
}
interface IEditSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
}
const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
@ -191,7 +191,7 @@ const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
};
interface IViewSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
onEdit(): Promise<void>;
}
@ -258,7 +258,7 @@ const SettingsList: React.FC<ISettingsListProps> = ({ onBack, onView, onEdit })
const [query, setQuery] = useState("");
const allSettings = useMemo(() => {
let allSettings = Object.keys(SETTINGS);
let allSettings = Object.keys(SETTINGS) as SettingKey[];
if (query) {
const lcQuery = query.toLowerCase();
allSettings = allSettings.filter((setting) => setting.toLowerCase().includes(lcQuery));

View File

@ -26,6 +26,7 @@ import {
import TextInputDialog from "../dialogs/TextInputDialog";
import AccessibleButton from "../elements/AccessibleButton";
import withValidation from "../elements/Validation";
import { SettingKey, Settings } from "../../../settings/Settings.tsx";
const SETTING_NAME = "room_directory_servers";
@ -67,15 +68,32 @@ const validServer = withValidation<undefined, { error?: unknown }>({
memoize: true,
});
function useSettingsValueWithSetter<T>(
settingName: string,
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null,
excludeDefault: true,
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId?: string | null,
excludeDefault?: false,
): [Settings[S]["default"], (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null = null,
excludeDefault = false,
): [T, (value: T) => Promise<void>] {
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId ?? undefined, excludeDefault));
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>] {
const [value, setValue] = useState(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
const setter = useCallback(
async (value: T): Promise<void> => {
async (value: Settings[S]["default"]): Promise<void> => {
setValue(value);
SettingsStore.setValue(settingName, roomId, level, value);
},
@ -84,7 +102,12 @@ function useSettingsValueWithSetter<T>(
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
setValue(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
});
// clean-up
return () => {
@ -109,10 +132,7 @@ function removeAll<T>(target: Set<T>, ...toRemove: T[]): void {
}
function useServers(): ServerList {
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter<string[]>(
SETTING_NAME,
SettingLevel.ACCOUNT,
);
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter(SETTING_NAME, SettingLevel.ACCOUNT);
const homeServer = MatrixClientPeg.safeGet().getDomain()!;
const configServers = new Set<string>(SdkConfig.getObject("room_directory")?.get("servers") ?? []);

View File

@ -105,7 +105,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;

View File

@ -15,11 +15,11 @@ import { _t } from "../../../languageHandler";
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
import { defaultWatchManager } from "../../../settings/Settings";
import { BooleanSettingKey, defaultWatchManager } from "../../../settings/Settings";
interface IProps {
// The setting must be a boolean
name: string;
name: BooleanSettingKey;
level: SettingLevel;
roomId?: string; // for per-room settings
label?: string;

View File

@ -107,7 +107,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<
// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;

View File

@ -36,9 +36,9 @@ const ExpandCollapseButton: React.FC<{
};
const CodeBlock: React.FC<Props> = ({ children, onHeightChanged }) => {
const enableSyntaxHighlightLanguageDetection = useSettingValue<boolean>("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue<boolean>("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue<boolean>("expandCodeByDefault");
const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue("expandCodeByDefault");
const [expanded, setExpanded] = useState(expandCodeByDefault);
let expandCollapseButton: JSX.Element | undefined;

View File

@ -426,7 +426,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
const htmlOpts = {
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
};

View File

@ -128,7 +128,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above
const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
let isRichTextEnabled = true;
let initialComposerContent = "";
if (isWysiwygLabEnabled) {

View File

@ -54,7 +54,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient = useContext(MatrixClientContext);
const { room, narrow } = useScopedRoomContext("room", "narrow");
const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer");
if (!matrixClient || !room || props.haveRecording) {
return null;

View File

@ -45,7 +45,7 @@ export function PlainTextComposer({
rightComponent,
eventRelation,
}: PlainTextComposerProps): JSX.Element {
const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const {
ref: editorRef,
autocompleteRef,

View File

@ -61,7 +61,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({
const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);
const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const emojiSuggestions = useMemo(() => getEmojiSuggestions(isAutoReplaceEmojiEnabled), [isAutoReplaceEmojiEnabled]);
const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({

View File

@ -36,7 +36,7 @@ export function useInputEventProcessor(
const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType");
const composerContext = useComposerContext();
const mxClient = useMatrixClientContext();
const isCtrlEnterToSend = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");
return useCallback(
(event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => {

View File

@ -128,7 +128,7 @@ export function usePlainTextListeners(
[eventRelation, mxClient, onInput, roomContext],
);
const enterShouldSend = !useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend");
const onKeyDown = useCallback(
(event: KeyboardEvent<HTMLDivElement>) => {
// we need autocomplete to take priority when it is open for using enter to select

View File

@ -66,7 +66,7 @@ export async function createMessageContent(
// TODO markdown support
const isMarkdownEnabled = SettingsStore.getValue<boolean>("MessageComposerInput.useMarkdown");
const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown");
const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message, true) : null;
if (formattedBody) {

View File

@ -47,7 +47,7 @@ export default class FontScalingPanel extends React.Component<IProps, IState> {
super(props);
this.state = {
fontSizeDelta: SettingsStore.getValue<number>("fontSizeDelta", null),
fontSizeDelta: SettingsStore.getValue("fontSizeDelta", null),
browserFontSize: FontWatcher.getBrowserDefaultFontSize(),
useCustomFontSize: SettingsStore.getValue("useCustomFontSize"),
layout: SettingsStore.getValue("layout"),

View File

@ -70,7 +70,7 @@ interface LayoutRadioProps {
* @param label
*/
function LayoutRadio({ layout, label }: LayoutRadioProps): JSX.Element {
const currentLayout = useSettingValue<Layout>("layout");
const currentLayout = useSettingValue("layout");
const eventTileInfo = useEventTileInfo();
return (
@ -134,8 +134,8 @@ function useEventTileInfo(): EventTileInfo {
* A toggleable setting to enable or disable the compact layout.
*/
function ToggleCompactLayout(): JSX.Element {
const compactLayoutEnabled = useSettingValue<boolean>("useCompactLayout");
const layout = useSettingValue<Layout>("layout");
const compactLayoutEnabled = useSettingValue("useCompactLayout");
const layout = useSettingValue("layout");
return (
<Root

View File

@ -40,7 +40,7 @@ import { useSettingValue } from "../../../hooks/useSettings";
export function ThemeChoicePanel(): JSX.Element {
const themeState = useTheme();
const themeWatcher = useRef(new ThemeWatcher());
const customThemeEnabled = useSettingValue<boolean>("feature_custom_themes");
const customThemeEnabled = useSettingValue("feature_custom_themes");
return (
<SettingsSubsection heading={_t("common|theme")} legacy={false} data-testid="themePanel">
@ -159,7 +159,7 @@ function ThemeSelectors({ theme, disabled }: ThemeSelectorProps): JSX.Element {
* Return all the available themes
*/
function useThemes(): Array<ITheme & { isDark: boolean }> {
const customThemes = useSettingValue<CustomThemeType[] | undefined>("custom_themes");
const customThemes = useSettingValue("custom_themes");
return useMemo(() => {
// Put the custom theme into a map
// To easily find the theme by name when going through the themes list
@ -239,8 +239,7 @@ function CustomTheme({ theme }: CustomThemeProps): JSX.Element {
// Get the custom themes and do a cheap clone
// To avoid to mutate the original array in the settings
const currentThemes =
SettingsStore.getValue<CustomThemeType[]>("custom_themes").map((t) => t) || [];
const currentThemes = SettingsStore.getValue("custom_themes").map((t) => t) || [];
try {
const r = await fetch(customTheme);
@ -294,7 +293,7 @@ interface CustomThemeListProps {
* List of the custom themes
*/
function CustomThemeList({ theme: currentTheme }: CustomThemeListProps): JSX.Element {
const customThemes = useSettingValue<CustomThemeType[]>("custom_themes") || [];
const customThemes = useSettingValue("custom_themes") || [];
return (
<ul className="mx_ThemeChoicePanel_CustomThemeList">
@ -309,8 +308,7 @@ function CustomThemeList({ theme: currentTheme }: CustomThemeListProps): JSX.Ele
onClick={async () => {
// Get the custom themes and do a cheap clone
// To avoid to mutate the original array in the settings
const currentThemes =
SettingsStore.getValue<CustomThemeType[]>("custom_themes").map((t) => t) || [];
const currentThemes = SettingsStore.getValue("custom_themes").map((t) => t) || [];
// Remove the theme from the list
const newThemes = currentThemes.filter((t) => t.name !== theme.name);

View File

@ -70,9 +70,9 @@ function useHasUnreadNotifications(): boolean {
export default function NotificationSettings2(): JSX.Element {
const cli = useMatrixClientContext();
const desktopNotifications = useSettingValue<boolean>("notificationsEnabled");
const desktopShowBody = useSettingValue<boolean>("notificationBodyEnabled");
const audioNotifications = useSettingValue<boolean>("audioNotificationsEnabled");
const desktopNotifications = useSettingValue("notificationsEnabled");
const desktopShowBody = useSettingValue("notificationBodyEnabled");
const audioNotifications = useSettingValue("audioNotificationsEnabled");
const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli);

View File

@ -14,7 +14,7 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
import SdkConfig from "../../../../../SdkConfig";
import BetaCard from "../../../beta/BetaCard";
import SettingsFlag from "../../../elements/SettingsFlag";
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { FeatureSettingKey, LabGroup, labGroupNames } from "../../../../../settings/Settings";
import { EnhancedMap } from "../../../../../utils/maps";
import { SettingsSection } from "../../shared/SettingsSection";
import { SettingsSubsection, SettingsSubsectionText } from "../../shared/SettingsSubsection";
@ -25,8 +25,8 @@ export const showLabsFlags = (): boolean => {
};
export default class LabsUserSettingsTab extends React.Component<{}> {
private readonly labs: string[];
private readonly betas: string[];
private readonly labs: FeatureSettingKey[];
private readonly betas: FeatureSettingKey[];
public constructor(props: {}) {
super(props);
@ -34,10 +34,10 @@ export default class LabsUserSettingsTab extends React.Component<{}> {
const features = SettingsStore.getFeatureSettingNames();
const [labs, betas] = features.reduce(
(arr, f) => {
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f as FeatureSettingKey);
return arr;
},
[[], []] as [string[], string[]],
[[], []] as [FeatureSettingKey[], FeatureSettingKey[]],
);
this.labs = labs;

View File

@ -11,7 +11,6 @@ import React, { ReactElement, useCallback, useEffect, useState } from "react";
import { NonEmptyArray } from "../../../../../@types/common";
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
import { UseCase } from "../../../../../settings/enums/UseCase";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import Dropdown from "../../../elements/Dropdown";
@ -33,6 +32,7 @@ import { IS_MAC } from "../../../../../Keyboard";
import SpellCheckSettings from "../../SpellCheckSettings";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import * as TimezoneHandler from "../../../../../TimezoneHandler";
import { BooleanSettingKey } from "../../../../../settings/Settings.tsx";
interface IProps {
closeSettingsFn(success: boolean): void;
@ -117,15 +117,15 @@ const SpellCheckSection: React.FC = () => {
};
export default class PreferencesUserSettingsTab extends React.Component<IProps, IState> {
private static ROOM_LIST_SETTINGS = ["breadcrumbs", "FTUE.userOnboardingButton"];
private static ROOM_LIST_SETTINGS: BooleanSettingKey[] = ["breadcrumbs", "FTUE.userOnboardingButton"];
private static SPACES_SETTINGS = ["Spaces.allRoomsInHome"];
private static SPACES_SETTINGS: BooleanSettingKey[] = ["Spaces.allRoomsInHome"];
private static KEYBINDINGS_SETTINGS = ["ctrlFForSearch"];
private static KEYBINDINGS_SETTINGS: BooleanSettingKey[] = ["ctrlFForSearch"];
private static PRESENCE_SETTINGS = ["sendReadReceipts", "sendTypingNotifications"];
private static PRESENCE_SETTINGS: BooleanSettingKey[] = ["sendReadReceipts", "sendTypingNotifications"];
private static COMPOSER_SETTINGS = [
private static COMPOSER_SETTINGS: BooleanSettingKey[] = [
"MessageComposerInput.autoReplaceEmoji",
"MessageComposerInput.useMarkdown",
"MessageComposerInput.suggestEmoji",
@ -135,17 +135,22 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
"MessageComposerInput.insertTrailingColon",
];
private static TIME_SETTINGS = ["showTwelveHourTimestamps", "alwaysShowTimestamps"];
private static TIME_SETTINGS: BooleanSettingKey[] = ["showTwelveHourTimestamps", "alwaysShowTimestamps"];
private static CODE_BLOCKS_SETTINGS = [
private static CODE_BLOCKS_SETTINGS: BooleanSettingKey[] = [
"enableSyntaxHighlightLanguageDetection",
"expandCodeByDefault",
"showCodeLineNumbers",
];
private static IMAGES_AND_VIDEOS_SETTINGS = ["urlPreviewsEnabled", "autoplayGifs", "autoplayVideo", "showImages"];
private static IMAGES_AND_VIDEOS_SETTINGS: BooleanSettingKey[] = [
"urlPreviewsEnabled",
"autoplayGifs",
"autoplayVideo",
"showImages",
];
private static TIMELINE_SETTINGS = [
private static TIMELINE_SETTINGS: BooleanSettingKey[] = [
"showTypingNotifications",
"showRedactions",
"showReadReceipts",
@ -159,9 +164,9 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
"useOnlyCurrentProfiles",
];
private static ROOM_DIRECTORY_SETTINGS = ["SpotlightSearch.showNsfwPublicRooms"];
private static ROOM_DIRECTORY_SETTINGS: BooleanSettingKey[] = ["SpotlightSearch.showNsfwPublicRooms"];
private static GENERAL_SETTINGS = [
private static GENERAL_SETTINGS: BooleanSettingKey[] = [
"promptBeforeInviteUnknownUsers",
// Start automatically after startup (electron-only)
// Autocomplete delay (niche text box)
@ -220,7 +225,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value);
};
private renderGroup(settingIds: string[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
private renderGroup(settingIds: BooleanSettingKey[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
return settingIds.map((i) => <SettingsFlag key={i} name={i} level={level} />);
}
@ -232,7 +237,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
public render(): React.ReactNode {
const useCase = SettingsStore.getValue<UseCase | null>("FTUE.useCaseSelection");
const useCase = SettingsStore.getValue("FTUE.useCaseSelection");
const roomListSettings = PreferencesUserSettingsTab.ROOM_LIST_SETTINGS
// Only show the user onboarding setting if the user should see the user onboarding page
.filter((it) => it !== "FTUE.userOnboardingButton" || showUserOnboardingPage(useCase));

View File

@ -58,8 +58,8 @@ const SidebarUserSettingsTab: React.FC = () => {
[MetaSpace.People]: peopleEnabled,
[MetaSpace.Orphans]: orphansEnabled,
[MetaSpace.VideoRooms]: videoRoomsEnabled,
} = useSettingValue<Record<MetaSpace, boolean>>("Spaces.enabledMetaSpaces");
const allRoomsInHome = useSettingValue<boolean>("Spaces.allRoomsInHome");
} = useSettingValue("Spaces.enabledMetaSpaces");
const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome");
const guestSpaUrl = useMemo(() => {
return SdkConfig.get("element_call").guest_spa_url;
}, []);

View File

@ -36,10 +36,10 @@ const QuickSettingsButton: React.FC<{
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
const { [MetaSpace.Favourites]: favouritesEnabled, [MetaSpace.People]: peopleEnabled } =
useSettingValue<Record<MetaSpace, boolean>>("Spaces.enabledMetaSpaces");
useSettingValue("Spaces.enabledMetaSpaces");
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
const developerModeEnabled = useSettingValue<boolean>("developerMode");
const developerModeEnabled = useSettingValue("developerMode");
let contextMenu: JSX.Element | undefined;
if (menuDisplayed && handle.current) {

View File

@ -88,7 +88,7 @@ export const HomeButtonContextMenu: React.FC<ComponentProps<typeof SpaceContextM
hideHeader,
...props
}) => {
const allRoomsInHome = useSettingValue<boolean>("Spaces.allRoomsInHome");
const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome");
return (
<IconizedContextMenu {...props} onFinished={onFinished} className="mx_SpacePanel_contextMenu" compact>

View File

@ -44,7 +44,7 @@ export function ThreadsActivityCentre({ displayButtonLabel }: ThreadsActivityCen
const [open, setOpen] = useState(false);
const roomsAndNotifications = useUnreadThreadRooms(open);
const isReleaseAnnouncementOpen = useIsReleaseAnnouncementOpen("threadsActivityCentre");
const settingTACOnlyNotifs = useSettingValue<boolean>("Notifications.tac_only_notifications");
const settingTACOnlyNotifs = useSettingValue("Notifications.tac_only_notifications");
const emptyCaption = settingTACOnlyNotifs
? _t("threads_activity_centre|no_rooms_with_threads_notifs")

View File

@ -32,8 +32,8 @@ type Result = {
* @returns {Result}
*/
export function useUnreadThreadRooms(forceComputation: boolean): Result {
const msc3946ProcessDynamicPredecessor = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const settingTACOnlyNotifs = useSettingValue<boolean>("Notifications.tac_only_notifications");
const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors");
const settingTACOnlyNotifs = useSettingValue("Notifications.tac_only_notifications");
const mxClient = useMatrixClientContext();
const [result, setResult] = useState<Result>({ greatestNotificationLevel: NotificationLevel.None, rooms: [] });

View File

@ -14,7 +14,6 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { useSettingValue } from "../../../hooks/useSettings";
import { _t } from "../../../languageHandler";
import PosthogTrackers from "../../../PosthogTrackers";
import { UseCase } from "../../../settings/enums/UseCase";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
@ -27,8 +26,8 @@ interface Props {
}
export function UserOnboardingButton({ selected, minimized }: Props): JSX.Element {
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
const visible = useSettingValue<boolean>("FTUE.userOnboardingButton");
const useCase = useSettingValue("FTUE.useCaseSelection");
const visible = useSettingValue("FTUE.userOnboardingButton");
if (!visible || minimized || !showUserOnboardingPage(useCase)) {
return <></>;

View File

@ -41,7 +41,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props): JSX.Eleme
const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config, cli);
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection");
const useCase = useSettingValue("FTUE.useCaseSelection");
const context = useUserOnboardingContext();
const tasks = useUserOnboardingTasks(context);

View File

@ -17,7 +17,7 @@ interface ILegacyFormat {
}
// New format tries to be more space efficient for synchronization. Ordered by Date descending.
type Format = [string, number][]; // [emoji, count]
export type RecentEmojiData = [emoji: string, count: number][];
const SETTING_NAME = "recent_emoji";
@ -33,7 +33,7 @@ function migrate(): void {
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, newFormat.slice(0, STORAGE_LIMIT));
}
function getRecentEmoji(): Format {
function getRecentEmoji(): RecentEmojiData {
return SettingsStore.getValue(SETTING_NAME) || [];
}

View File

@ -17,7 +17,7 @@ import { filterBoolean } from "../../utils/arrays";
export const useRecentSearches = (): [Room[], () => void] => {
const [rooms, setRooms] = useState(() => {
const cli = MatrixClientPeg.safeGet();
const recents = SettingsStore.getValue<string[]>("SpotlightSearch.recentSearches", null);
const recents = SettingsStore.getValue("SpotlightSearch.recentSearches", null);
return filterBoolean(recents.map((r) => cli.getRoom(r)));
});

View File

@ -55,7 +55,7 @@ export const usePublicRoomDirectory = (): {
const [updateQuery, updateResult] = useLatestResult<IRoomDirectoryOptions, IPublicRoomsChunkRoom[]>(setPublicRooms);
const showNsfwPublicRooms = useSettingValue<boolean>("SpotlightSearch.showNsfwPublicRooms");
const showNsfwPublicRooms = useSettingValue("SpotlightSearch.showNsfwPublicRooms");
async function initProtocols(): Promise<void> {
if (!MatrixClientPeg.get()) {

View File

@ -10,14 +10,39 @@ import { useEffect, useState } from "react";
import SettingsStore from "../settings/SettingsStore";
import { SettingLevel } from "../settings/SettingLevel";
import { FeatureSettingKey, SettingKey, Settings } from "../settings/Settings.tsx";
// Hook to fetch the value of a setting and dynamically update when it changes
export const useSettingValue = <T>(settingName: string, roomId: string | null = null, excludeDefault = false): T => {
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
export function useSettingValue<S extends SettingKey>(
settingName: S,
roomId: string | null,
excludeDefault: true,
): Settings[S]["default"] | undefined;
export function useSettingValue<S extends SettingKey>(
settingName: S,
roomId?: string | null,
excludeDefault?: false,
): Settings[S]["default"];
export function useSettingValue<S extends SettingKey>(
settingName: S,
roomId: string | null = null,
excludeDefault = false,
): Settings[S]["default"] | undefined {
const [value, setValue] = useState(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
setValue(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
});
// clean-up
return () => {
@ -26,7 +51,7 @@ export const useSettingValue = <T>(settingName: string, roomId: string | null =
}, [settingName, roomId, excludeDefault]);
return value;
};
}
/**
* Hook to fetch the value of a setting at a specific level and dynamically update when it changes
@ -37,20 +62,18 @@ export const useSettingValue = <T>(settingName: string, roomId: string | null =
* @param explicit
* @param excludeDefault
*/
export const useSettingValueAt = <T>(
export const useSettingValueAt = <S extends SettingKey>(
level: SettingLevel,
settingName: string,
settingName: S,
roomId: string | null = null,
explicit = false,
excludeDefault = false,
): T => {
const [value, setValue] = useState(
SettingsStore.getValueAt<T>(level, settingName, roomId, explicit, excludeDefault),
);
): Settings[S]["default"] => {
const [value, setValue] = useState(SettingsStore.getValueAt(level, settingName, roomId, explicit, excludeDefault));
useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValueAt<T>(level, settingName, roomId, explicit, excludeDefault));
setValue(SettingsStore.getValueAt(level, settingName, roomId, explicit, excludeDefault));
});
// clean-up
return () => {
@ -62,8 +85,8 @@ export const useSettingValueAt = <T>(
};
// Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string | null = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue<boolean>(featureName, roomId));
export const useFeatureEnabled = (featureName: FeatureSettingKey, roomId: string | null = null): boolean => {
const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => {

View File

@ -16,10 +16,10 @@ export function useTheme(): { theme: string; systemThemeActivated: boolean } {
// We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we
// show the right values for things.
const themeChoice = useSettingValue<string>("theme");
const systemThemeExplicit = useSettingValueAt<string>(SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = useSettingValueAt<string>(SettingLevel.DEVICE, "theme", null, false, true);
const systemThemeActivated = useSettingValue<boolean>("use_system_theme");
const themeChoice = useSettingValue("theme");
const systemThemeExplicit = useSettingValueAt(SettingLevel.DEVICE, "use_system_theme", null, false, true);
const themeExplicit = useSettingValueAt(SettingLevel.DEVICE, "theme", null, false, true);
const systemThemeActivated = useSettingValue("use_system_theme");
// If the user has enabled system theme matching, use that.
if (systemThemeExplicit) {

View File

@ -145,7 +145,7 @@ const tasks: UserOnboardingTask[] = [
];
export function useUserOnboardingTasks(context: UserOnboardingContext): UserOnboardingTaskWithResolvedCompletion[] {
const useCase = useSettingValue<UseCase | null>("FTUE.useCaseSelection") ?? UseCase.Skip;
const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip;
return useMemo<UserOnboardingTaskWithResolvedCompletion[]>(() => {
return tasks

View File

@ -688,7 +688,7 @@ export class ElementCall extends Call {
// Set custom fonts
if (SettingsStore.getValue("useSystemFont")) {
SettingsStore.getValue<string>("systemFont")
SettingsStore.getValue("systemFont")
.split(",")
.map((font) => {
// Strip whitespace and quotes

View File

@ -38,6 +38,13 @@ import { WatchManager } from "./WatchManager";
import { CustomTheme } from "../theme";
import AnalyticsController from "./controllers/AnalyticsController";
import FallbackIceServerController from "./controllers/FallbackIceServerController";
import { UseCase } from "./enums/UseCase.tsx";
import { IRightPanelForRoomStored } from "../stores/right-panel/RightPanelStoreIPanelState.ts";
import { ILayoutSettings } from "../stores/widgets/WidgetLayoutStore.ts";
import { ReleaseAnnouncementData } from "../stores/ReleaseAnnouncementStore.ts";
import { Json, JsonValue } from "../@types/json.ts";
import { RecentEmojiData } from "../emojipicker/recent.ts";
import { Assignable } from "../@types/common.ts";
export const defaultWatchManager = new WatchManager();
@ -106,15 +113,7 @@ export const labGroupNames: Record<LabGroup, TranslationKey> = {
[LabGroup.Ui]: _td("labs|group_ui"),
};
export type SettingValueType =
| boolean
| number
| string
| number[]
| string[]
| Record<string, unknown>
| Record<string, unknown>[]
| null;
export type SettingValueType = Json | JsonValue | Record<string, unknown> | Record<string, unknown>[];
export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
isFeature?: false | undefined;
@ -164,7 +163,7 @@ export interface IBaseSetting<T extends SettingValueType = SettingValueType> {
image?: string; // require(...)
feedbackSubheading?: TranslationKey;
feedbackLabel?: string;
extraSettings?: string[];
extraSettings?: BooleanSettingKey[];
requiresRefresh?: boolean;
};
@ -181,7 +180,179 @@ export interface IFeature extends Omit<IBaseSetting<boolean>, "isFeature"> {
// Type using I-identifier for backwards compatibility from before it became a discriminated union
export type ISetting = IBaseSetting | IFeature;
export const SETTINGS: { [setting: string]: ISetting } = {
export interface Settings {
[settingName: `UIFeature.${string}`]: IBaseSetting<boolean>;
// We can't use the following type because of `feature_sliding_sync_proxy_url` & `feature_hidebold` being in the namespace incorrectly
// [settingName: `feature_${string}`]: IFeature;
"feature_video_rooms": IFeature;
[Features.NotificationSettings2]: IFeature;
[Features.ReleaseAnnouncement]: IFeature;
"feature_msc3531_hide_messages_pending_moderation": IFeature;
"feature_report_to_moderators": IFeature;
"feature_latex_maths": IFeature;
"feature_wysiwyg_composer": IFeature;
"feature_mjolnir": IFeature;
"feature_custom_themes": IFeature;
"feature_exclude_insecure_devices": IFeature;
"feature_html_topic": IFeature;
"feature_bridge_state": IFeature;
"feature_jump_to_date": IFeature;
"feature_sliding_sync": IFeature;
"feature_element_call_video_rooms": IFeature;
"feature_group_calls": IFeature;
"feature_disable_call_per_sender_encryption": IFeature;
"feature_allow_screen_share_only_mode": IFeature;
"feature_location_share_live": IFeature;
"feature_dynamic_room_predecessors": IFeature;
"feature_render_reaction_images": IFeature;
"feature_ask_to_join": IFeature;
"feature_notifications": IFeature;
// These are in the feature namespace but aren't actually features
"feature_sliding_sync_proxy_url": IBaseSetting<string>;
"feature_hidebold": IBaseSetting<boolean>;
"useOnlyCurrentProfiles": IBaseSetting<boolean>;
"mjolnirRooms": IBaseSetting<string[]>;
"mjolnirPersonalRoom": IBaseSetting<string | null>;
"RoomList.backgroundImage": IBaseSetting<string | null>;
"sendReadReceipts": IBaseSetting<boolean>;
"baseFontSize": IBaseSetting<"" | number>;
"baseFontSizeV2": IBaseSetting<"" | number>;
"fontSizeDelta": IBaseSetting<number>;
"useCustomFontSize": IBaseSetting<boolean>;
"MessageComposerInput.suggestEmoji": IBaseSetting<boolean>;
"MessageComposerInput.showStickersButton": IBaseSetting<boolean>;
"MessageComposerInput.showPollsButton": IBaseSetting<boolean>;
"MessageComposerInput.insertTrailingColon": IBaseSetting<boolean>;
"Notifications.alwaysShowBadgeCounts": IBaseSetting<boolean>;
"Notifications.showbold": IBaseSetting<boolean>;
"Notifications.tac_only_notifications": IBaseSetting<boolean>;
"useCompactLayout": IBaseSetting<boolean>;
"showRedactions": IBaseSetting<boolean>;
"showJoinLeaves": IBaseSetting<boolean>;
"showAvatarChanges": IBaseSetting<boolean>;
"showDisplaynameChanges": IBaseSetting<boolean>;
"showReadReceipts": IBaseSetting<boolean>;
"showTwelveHourTimestamps": IBaseSetting<boolean>;
"alwaysShowTimestamps": IBaseSetting<boolean>;
"userTimezone": IBaseSetting<string>;
"userTimezonePublish": IBaseSetting<boolean>;
"autoplayGifs": IBaseSetting<boolean>;
"autoplayVideo": IBaseSetting<boolean>;
"enableSyntaxHighlightLanguageDetection": IBaseSetting<boolean>;
"expandCodeByDefault": IBaseSetting<boolean>;
"showCodeLineNumbers": IBaseSetting<boolean>;
"scrollToBottomOnMessageSent": IBaseSetting<boolean>;
"Pill.shouldShowPillAvatar": IBaseSetting<boolean>;
"TextualBody.enableBigEmoji": IBaseSetting<boolean>;
"MessageComposerInput.isRichTextEnabled": IBaseSetting<boolean>;
"MessageComposer.showFormatting": IBaseSetting<boolean>;
"sendTypingNotifications": IBaseSetting<boolean>;
"showTypingNotifications": IBaseSetting<boolean>;
"ctrlFForSearch": IBaseSetting<boolean>;
"MessageComposerInput.ctrlEnterToSend": IBaseSetting<boolean>;
"MessageComposerInput.surroundWith": IBaseSetting<boolean>;
"MessageComposerInput.autoReplaceEmoji": IBaseSetting<boolean>;
"MessageComposerInput.useMarkdown": IBaseSetting<boolean>;
"VideoView.flipVideoHorizontally": IBaseSetting<boolean>;
"theme": IBaseSetting<string>;
"custom_themes": IBaseSetting<CustomTheme[]>;
"use_system_theme": IBaseSetting<boolean>;
"useBundledEmojiFont": IBaseSetting<boolean>;
"useSystemFont": IBaseSetting<boolean>;
"systemFont": IBaseSetting<string>;
"webRtcAllowPeerToPeer": IBaseSetting<boolean>;
"webrtc_audiooutput": IBaseSetting<string>;
"webrtc_audioinput": IBaseSetting<string>;
"webrtc_videoinput": IBaseSetting<string>;
"webrtc_audio_autoGainControl": IBaseSetting<boolean>;
"webrtc_audio_echoCancellation": IBaseSetting<boolean>;
"webrtc_audio_noiseSuppression": IBaseSetting<boolean>;
"language": IBaseSetting<string>;
"breadcrumb_rooms": IBaseSetting<string[]>;
"recent_emoji": IBaseSetting<RecentEmojiData>;
"SpotlightSearch.recentSearches": IBaseSetting<string[]>;
"SpotlightSearch.showNsfwPublicRooms": IBaseSetting<boolean>;
"room_directory_servers": IBaseSetting<string[]>;
"integrationProvisioning": IBaseSetting<boolean>;
"allowedWidgets": IBaseSetting<{ [eventId: string]: boolean }>;
"analyticsOptIn": IBaseSetting<boolean>;
"pseudonymousAnalyticsOptIn": IBaseSetting<boolean | null>;
"deviceClientInformationOptIn": IBaseSetting<boolean>;
"FTUE.useCaseSelection": IBaseSetting<UseCase | null>;
"Registration.mobileRegistrationHelper": IBaseSetting<boolean>;
"autocompleteDelay": IBaseSetting<number>;
"readMarkerInViewThresholdMs": IBaseSetting<number>;
"readMarkerOutOfViewThresholdMs": IBaseSetting<number>;
"blacklistUnverifiedDevices": IBaseSetting<boolean>;
"urlPreviewsEnabled": IBaseSetting<boolean>;
"urlPreviewsEnabled_e2ee": IBaseSetting<boolean>;
"notificationsEnabled": IBaseSetting<boolean>;
"deviceNotificationsEnabled": IBaseSetting<boolean>;
"notificationSound": IBaseSetting<
| {
name: string;
type: string;
size: number;
url: string;
}
| false
>;
"notificationBodyEnabled": IBaseSetting<boolean>;
"audioNotificationsEnabled": IBaseSetting<boolean>;
"enableWidgetScreenshots": IBaseSetting<boolean>;
"promptBeforeInviteUnknownUsers": IBaseSetting<boolean>;
"widgetOpenIDPermissions": IBaseSetting<{
allow?: string[];
deny?: string[];
}>;
"breadcrumbs": IBaseSetting<boolean>;
"FTUE.userOnboardingButton": IBaseSetting<boolean>;
"showHiddenEventsInTimeline": IBaseSetting<boolean>;
"lowBandwidth": IBaseSetting<boolean>;
"fallbackICEServerAllowed": IBaseSetting<boolean | null>;
"showImages": IBaseSetting<boolean>;
"RightPanel.phasesGlobal": IBaseSetting<IRightPanelForRoomStored | null>;
"RightPanel.phases": IBaseSetting<IRightPanelForRoomStored | null>;
"enableEventIndexing": IBaseSetting<boolean>;
"crawlerSleepTime": IBaseSetting<number>;
"showCallButtonsInComposer": IBaseSetting<boolean>;
"ircDisplayNameWidth": IBaseSetting<number>;
"layout": IBaseSetting<Layout>;
"Images.size": IBaseSetting<ImageSize>;
"showChatEffects": IBaseSetting<boolean>;
"Performance.addSendMessageTimingMetadata": IBaseSetting<boolean>;
"Widgets.pinned": IBaseSetting<{ [widgetId: string]: boolean }>;
"Widgets.layout": IBaseSetting<ILayoutSettings | null>;
"Spaces.allRoomsInHome": IBaseSetting<boolean>;
"Spaces.enabledMetaSpaces": IBaseSetting<Partial<Record<MetaSpace, boolean>>>;
"Spaces.showPeopleInSpace": IBaseSetting<boolean>;
"developerMode": IBaseSetting<boolean>;
"automaticErrorReporting": IBaseSetting<boolean>;
"automaticDecryptionErrorReporting": IBaseSetting<boolean>;
"automaticKeyBackNotEnabledReporting": IBaseSetting<boolean>;
"debug_scroll_panel": IBaseSetting<boolean>;
"debug_timeline_panel": IBaseSetting<boolean>;
"debug_registration": IBaseSetting<boolean>;
"debug_animation": IBaseSetting<boolean>;
"debug_legacy_call_handler": IBaseSetting<boolean>;
"audioInputMuted": IBaseSetting<boolean>;
"videoInputMuted": IBaseSetting<boolean>;
"activeCallRoomIds": IBaseSetting<string[]>;
"releaseAnnouncementData": IBaseSetting<ReleaseAnnouncementData>;
"Electron.autoLaunch": IBaseSetting<boolean>;
"Electron.warnBeforeExit": IBaseSetting<boolean>;
"Electron.alwaysShowMenuBar": IBaseSetting<boolean>;
"Electron.showTrayIcon": IBaseSetting<boolean>;
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
}
export type SettingKey = keyof Settings;
export type FeatureSettingKey = Assignable<Settings, IFeature>;
export type BooleanSettingKey = Assignable<Settings, IBaseSetting<boolean>> | FeatureSettingKey;
export const SETTINGS: Settings = {
"feature_video_rooms": {
isFeature: true,
labsGroup: LabGroup.VoiceAndVideo,
@ -710,7 +881,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
},
"custom_themes": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: [] as CustomTheme[],
default: [],
},
"use_system_theme": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,

View File

@ -20,7 +20,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher";
import { IFeature, ISetting, LabGroup, SETTINGS, defaultWatchManager } from "./Settings";
import { IFeature, ISetting, LabGroup, SETTINGS, defaultWatchManager, SettingKey, Settings } from "./Settings";
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
import { CallbackFn as WatchCallbackFn } from "./WatchManager";
import { SettingLevel } from "./SettingLevel";
@ -34,11 +34,11 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
// Convert the settings to easier to manage objects for the handlers
const defaultSettings: Record<string, any> = {};
const invertedDefaultSettings: Record<string, boolean> = {};
const featureNames: string[] = [];
const featureNames: SettingKey[] = [];
for (const key in SETTINGS) {
const setting = SETTINGS[key];
const setting = SETTINGS[key as SettingKey];
defaultSettings[key] = setting.default;
if (setting.isFeature) featureNames.push(key);
if (setting.isFeature) featureNames.push(key as SettingKey);
if (setting.invertedSettingName) {
// Invert now so that the rest of the system will invert it back to what was intended.
invertedDefaultSettings[setting.invertedSettingName] = !setting.default;
@ -80,7 +80,7 @@ function getLevelOrder(setting: ISetting): SettingLevel[] {
}
export type CallbackFn = (
settingName: string,
settingName: SettingKey,
roomId: string | null,
atLevel: SettingLevel,
newValAtLevel: any,
@ -138,8 +138,8 @@ export default class SettingsStore {
* Gets all the feature-style setting names.
* @returns {string[]} The names of the feature settings.
*/
public static getFeatureSettingNames(): string[] {
return Object.keys(SETTINGS).filter((n) => SettingsStore.isFeature(n));
public static getFeatureSettingNames(): SettingKey[] {
return (Object.keys(SETTINGS) as SettingKey[]).filter((n) => SettingsStore.isFeature(n));
}
/**
@ -158,33 +158,30 @@ export default class SettingsStore {
* if the change in value is worthwhile enough to react upon.
* @returns {string} A reference to the watcher that was employed.
*/
public static watchSetting(settingName: string, roomId: string | null, callbackFn: CallbackFn): string {
public static watchSetting(settingName: SettingKey, roomId: string | null, callbackFn: CallbackFn): string {
const setting = SETTINGS[settingName];
const originalSettingName = settingName;
if (!setting) throw new Error(`${settingName} is not a setting`);
if (setting.invertedSettingName) {
settingName = setting.invertedSettingName;
}
const finalSettingName: string = setting.invertedSettingName ?? settingName;
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${settingName}_${roomId}`;
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${finalSettingName}_${roomId}`;
const localizedCallback = (changedInRoomId: string | null, atLevel: SettingLevel, newValAtLevel: any): void => {
if (!SettingsStore.doesSettingSupportLevel(originalSettingName, atLevel)) {
if (!SettingsStore.doesSettingSupportLevel(settingName, atLevel)) {
logger.warn(
`Setting handler notified for an update of an invalid setting level: ` +
`${originalSettingName}@${atLevel} - this likely means a weird setting value ` +
`${settingName}@${atLevel} - this likely means a weird setting value ` +
`made it into the level's storage. The notification will be ignored.`,
);
return;
}
const newValue = SettingsStore.getValue(originalSettingName);
const newValueAtLevel = SettingsStore.getValueAt(atLevel, originalSettingName) ?? newValAtLevel;
callbackFn(originalSettingName, changedInRoomId, atLevel, newValueAtLevel, newValue);
const newValue = SettingsStore.getValue(settingName);
const newValueAtLevel = SettingsStore.getValueAt(atLevel, settingName) ?? newValAtLevel;
callbackFn(settingName, changedInRoomId, atLevel, newValueAtLevel, newValue);
};
SettingsStore.watchers.set(watcherId, localizedCallback);
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
defaultWatchManager.watchSetting(finalSettingName, roomId, localizedCallback);
return watcherId;
}
@ -214,7 +211,7 @@ export default class SettingsStore {
* @param {string} settingName The setting name to monitor.
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
*/
public static monitorSetting(settingName: string, roomId: string | null): void {
public static monitorSetting(settingName: SettingKey, roomId: string | null): void {
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
if (!this.monitors.has(settingName)) this.monitors.set(settingName, new Map());
@ -262,7 +259,7 @@ export default class SettingsStore {
* The level to get the display name for; Defaults to 'default'.
* @return {String} The display name for the setting, or null if not found.
*/
public static getDisplayName(settingName: string, atLevel = SettingLevel.DEFAULT): string | null {
public static getDisplayName(settingName: SettingKey, atLevel = SettingLevel.DEFAULT): string | null {
if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
const displayName = SETTINGS[settingName].displayName;
@ -285,7 +282,7 @@ export default class SettingsStore {
* @param {string} settingName The setting to look up.
* @return {String} The description for the setting, or null if not found.
*/
public static getDescription(settingName: string): string | ReactNode {
public static getDescription(settingName: SettingKey): string | ReactNode {
const description = SETTINGS[settingName]?.description;
if (!description) return null;
if (typeof description !== "string") return description();
@ -297,7 +294,7 @@ export default class SettingsStore {
* @param {string} settingName The setting to look up.
* @return {boolean} True if the setting is a feature.
*/
public static isFeature(settingName: string): boolean {
public static isFeature(settingName: SettingKey): boolean {
if (!SETTINGS[settingName]) return false;
return !!SETTINGS[settingName].isFeature;
}
@ -307,12 +304,12 @@ export default class SettingsStore {
* @param {string} settingName The setting to look up.
* @return {boolean} True if the setting should have a warning sign.
*/
public static shouldHaveWarning(settingName: string): boolean {
public static shouldHaveWarning(settingName: SettingKey): boolean {
if (!SETTINGS[settingName]) return false;
return SETTINGS[settingName].shouldWarn ?? false;
}
public static getBetaInfo(settingName: string): ISetting["betaInfo"] {
public static getBetaInfo(settingName: SettingKey): ISetting["betaInfo"] {
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
if (
SettingsStore.isFeature(settingName) &&
@ -327,7 +324,7 @@ export default class SettingsStore {
}
}
public static getLabGroup(settingName: string): LabGroup | undefined {
public static getLabGroup(settingName: SettingKey): LabGroup | undefined {
if (SettingsStore.isFeature(settingName)) {
return (<IFeature>SETTINGS[settingName]).labsGroup;
}
@ -340,7 +337,7 @@ export default class SettingsStore {
* @param {string} settingName The setting to look up.
* @return {string} The reason the setting is disabled.
*/
public static disabledMessage(settingName: string): string | undefined {
public static disabledMessage(settingName: SettingKey): string | undefined {
const disabled = SETTINGS[settingName].controller?.settingDisabled;
return typeof disabled === "string" ? disabled : undefined;
}
@ -353,7 +350,21 @@ export default class SettingsStore {
* @param {boolean} excludeDefault True to disable using the default value.
* @return {*} The value, or null if not found
*/
public static getValue<T = any>(settingName: string, roomId: string | null = null, excludeDefault = false): T {
public static getValue<S extends SettingKey>(
settingName: S,
roomId: string | null,
excludeDefault: true,
): Settings[S]["default"] | undefined;
public static getValue<S extends SettingKey>(
settingName: S,
roomId?: string | null,
excludeDefault?: false,
): Settings[S]["default"];
public static getValue<S extends SettingKey>(
settingName: S,
roomId: string | null = null,
excludeDefault = false,
): Settings[S]["default"] | undefined {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
@ -362,7 +373,7 @@ export default class SettingsStore {
const setting = SETTINGS[settingName];
const levelOrder = getLevelOrder(setting);
return SettingsStore.getValueAt<T>(levelOrder[0], settingName, roomId, false, excludeDefault);
return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
}
/**
@ -376,13 +387,13 @@ export default class SettingsStore {
* @param {boolean} excludeDefault True to disable using the default value.
* @return {*} The value, or null if not found.
*/
public static getValueAt<T = any>(
public static getValueAt<S extends SettingKey>(
level: SettingLevel,
settingName: string,
settingName: S,
roomId: string | null = null,
explicit = false,
excludeDefault = false,
): T {
): Settings[S]["default"] {
// Verify that the setting is actually a setting
const setting = SETTINGS[settingName];
if (!setting) {
@ -399,9 +410,10 @@ export default class SettingsStore {
// Check if we need to invert the setting at all. Do this after we get the setting
// handlers though, otherwise we'll fail to read the value.
let finalSettingName: string = settingName;
if (setting.invertedSettingName) {
//console.warn(`Inverting ${settingName} to be ${setting.invertedSettingName} - legacy setting`);
settingName = setting.invertedSettingName;
finalSettingName = setting.invertedSettingName;
}
if (explicit) {
@ -409,7 +421,7 @@ export default class SettingsStore {
if (!handler) {
return SettingsStore.getFinalValue(setting, level, roomId, null, null);
}
const value = handler.getValue(settingName, roomId);
const value = handler.getValue(finalSettingName, roomId);
return SettingsStore.getFinalValue(setting, level, roomId, value, level);
}
@ -418,7 +430,7 @@ export default class SettingsStore {
if (!handler) continue;
if (excludeDefault && levelOrder[i] === "default") continue;
const value = handler.getValue(settingName, roomId);
const value = handler.getValue(finalSettingName, roomId);
if (value === null || value === undefined) continue;
return SettingsStore.getFinalValue(setting, level, roomId, value, levelOrder[i]);
}
@ -432,7 +444,7 @@ export default class SettingsStore {
* @param {String} roomId The room ID to read the setting value in, may be null.
* @return {*} The default value
*/
public static getDefaultValue(settingName: string): any {
public static getDefaultValue(settingName: SettingKey): any {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
@ -474,7 +486,7 @@ export default class SettingsStore {
/* eslint-enable valid-jsdoc */
public static async setValue(
settingName: string,
settingName: SettingKey,
roomId: string | null,
level: SettingLevel,
value: any,
@ -490,24 +502,25 @@ export default class SettingsStore {
throw new Error("Setting " + settingName + " does not have a handler for " + level);
}
let finalSettingName: string = settingName;
if (setting.invertedSettingName) {
// Note: We can't do this when the `level` is "default", however we also
// know that the user can't possible change the default value through this
// function so we don't bother checking it.
//console.warn(`Inverting ${settingName} to be ${setting.invertedSettingName} - legacy setting`);
settingName = setting.invertedSettingName;
finalSettingName = setting.invertedSettingName;
value = !value;
}
if (!handler.canSetValue(settingName, roomId)) {
throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
if (!handler.canSetValue(finalSettingName, roomId)) {
throw new Error("User cannot set " + finalSettingName + " at " + level + " in " + roomId);
}
if (setting.controller && !(await setting.controller.beforeChange(level, roomId, value))) {
return; // controller says no
}
await handler.setValue(settingName, roomId, value);
await handler.setValue(finalSettingName, roomId, value);
setting.controller?.onChange(level, roomId, value);
}
@ -530,7 +543,7 @@ export default class SettingsStore {
* @param {SettingLevel} level The level to check at.
* @return {boolean} True if the user may set the setting, false otherwise.
*/
public static canSetValue(settingName: string, roomId: string | null, level: SettingLevel): boolean {
public static canSetValue(settingName: SettingKey, roomId: string | null, level: SettingLevel): boolean {
const setting = SETTINGS[settingName];
// Verify that the setting is actually a setting
if (!setting) {
@ -563,7 +576,7 @@ export default class SettingsStore {
* @returns
*/
public static settingIsOveriddenAtConfigLevel(
settingName: string,
settingName: SettingKey,
roomId: string | null,
level: SettingLevel,
): boolean {
@ -597,7 +610,7 @@ export default class SettingsStore {
* the level itself can be supported by the runtime (ie: you will need to call #isLevelSupported()
* on your own).
*/
public static doesSettingSupportLevel(settingName: string, level: SettingLevel): boolean {
public static doesSettingSupportLevel(settingName: SettingKey, level: SettingLevel): boolean {
const setting = SETTINGS[settingName];
if (!setting) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
@ -612,7 +625,7 @@ export default class SettingsStore {
* @param {string} settingName The setting name.
* @return {SettingLevel}
*/
public static firstSupportedLevel(settingName: string): SettingLevel | null {
public static firstSupportedLevel(settingName: SettingKey): SettingLevel | null {
// Verify that the setting is actually a setting
const setting = SETTINGS[settingName];
if (!setting) {
@ -699,7 +712,7 @@ export default class SettingsStore {
* @param {string} realSettingName The setting name to try and read.
* @param {string} roomId Optional room ID to test the setting in.
*/
public static debugSetting(realSettingName: string, roomId: string): void {
public static debugSetting(realSettingName: SettingKey, roomId: string): void {
logger.log(`--- DEBUG ${realSettingName}`);
// Note: we intentionally use JSON.stringify here to avoid the console masking the
@ -711,7 +724,7 @@ export default class SettingsStore {
logger.log(`--- default level order: ${JSON.stringify(LEVEL_ORDER)}`);
logger.log(`--- registered handlers: ${JSON.stringify(Object.keys(LEVEL_HANDLERS))}`);
const doChecks = (settingName: string): void => {
const doChecks = (settingName: SettingKey): void => {
for (const handlerName of Object.keys(LEVEL_HANDLERS)) {
const handler = LEVEL_HANDLERS[handlerName as SettingLevel];
@ -803,19 +816,19 @@ export default class SettingsStore {
if (def.invertedSettingName) {
logger.log(`--- TESTING INVERTED SETTING NAME`);
logger.log(`--- inverted: ${def.invertedSettingName}`);
doChecks(def.invertedSettingName);
doChecks(def.invertedSettingName as SettingKey);
}
logger.log(`--- END DEBUG`);
}
private static getHandler(settingName: string, level: SettingLevel): SettingsHandler | null {
private static getHandler(settingName: SettingKey, level: SettingLevel): SettingsHandler | null {
const handlers = SettingsStore.getHandlers(settingName);
if (!handlers[level]) return null;
return handlers[level]!;
}
private static getHandlers(settingName: string): HandlerMap {
private static getHandlers(settingName: SettingKey): HandlerMap {
if (!SETTINGS[settingName]) return {};
const handlers: Partial<Record<SettingLevel, SettingsHandler>> = {};

View File

@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import SettingController from "./SettingController";
import { SettingLevel } from "../SettingLevel";
import SettingsStore from "../SettingsStore";
import { BooleanSettingKey } from "../Settings.tsx";
/**
* Enforces that a boolean setting cannot be enabled if the incompatible setting
@ -17,7 +18,7 @@ import SettingsStore from "../SettingsStore";
*/
export default class IncompatibleController extends SettingController {
public constructor(
private settingName: string,
private settingName: BooleanSettingKey,
private forcedValue: any = false,
private incompatibleValue: any | ((v: any) => boolean) = true,
) {

View File

@ -10,6 +10,7 @@ import { SettingLevel } from "../SettingLevel";
import MatrixClientBackedController from "./MatrixClientBackedController";
import { WatchManager } from "../WatchManager";
import SettingsStore from "../SettingsStore";
import { SettingKey } from "../Settings.tsx";
/**
* Disables a given setting if the server unstable feature it requires is not supported
@ -28,7 +29,7 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
* the features in the group are supported (all features in a group are required).
*/
public constructor(
private readonly settingName: string,
private readonly settingName: SettingKey,
private readonly watchers: WatchManager,
private readonly unstableFeatureGroups: string[][],
private readonly stableVersion?: string,

View File

@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import SettingController from "./SettingController";
import { SettingLevel } from "../SettingLevel";
import SettingsStore from "../SettingsStore";
import { SettingKey } from "../Settings.tsx";
/**
* Enforces that a boolean setting cannot be enabled if the corresponding
@ -19,7 +20,7 @@ import SettingsStore from "../SettingsStore";
*/
export default class UIFeatureController extends SettingController {
public constructor(
private uiFeatureName: string,
private uiFeatureName: SettingKey,
private forcedValue = false,
) {
super();

View File

@ -57,7 +57,7 @@ export class FontWatcher implements IWatcher {
* @private
*/
private async migrateBaseFontV1toFontSizeDelta(): Promise<void> {
const legacyBaseFontSize = SettingsStore.getValue<number>("baseFontSize");
const legacyBaseFontSize = SettingsStore.getValue("baseFontSize");
// No baseFontV1 found, nothing to migrate
if (!legacyBaseFontSize) return;
@ -82,7 +82,7 @@ export class FontWatcher implements IWatcher {
* @private
*/
private async migrateBaseFontV2toFontSizeDelta(): Promise<void> {
const legacyBaseFontV2Size = SettingsStore.getValue<number>("baseFontSizeV2");
const legacyBaseFontV2Size = SettingsStore.getValue("baseFontSizeV2");
// No baseFontV2 found, nothing to migrate
if (!legacyBaseFontV2Size) return;
@ -140,7 +140,7 @@ export class FontWatcher implements IWatcher {
* @returns {number} the default font size of the browser
*/
public static getBrowserDefaultFontSize(): number {
return this.getRootFontSize() - SettingsStore.getValue<number>("fontSizeDelta");
return this.getRootFontSize() - SettingsStore.getValue("fontSizeDelta");
}
public stop(): void {
@ -148,7 +148,7 @@ export class FontWatcher implements IWatcher {
}
private updateFont(): void {
this.setRootFontSize(SettingsStore.getValue<number>("fontSizeDelta"));
this.setRootFontSize(SettingsStore.getValue("fontSizeDelta"));
this.setSystemFont({
useBundledEmojiFont: SettingsStore.getValue("useBundledEmojiFont"),
useSystemFont: SettingsStore.getValue("useSystemFont"),

View File

@ -11,6 +11,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import SettingsStore from "./settings/SettingsStore";
import { IRoomState } from "./components/structures/RoomView";
import { SettingKey } from "./settings/Settings.tsx";
interface IDiff {
isMemberEvent: boolean;
@ -53,7 +54,7 @@ export default function shouldHideEvent(ev: MatrixEvent, ctx?: IRoomState): bool
// so we should prefer using cached values if a RoomContext is available
const isEnabled = ctx
? (name: keyof IRoomState) => ctx[name]
: (name: string) => SettingsStore.getValue(name, ev.getRoomId());
: (name: SettingKey) => SettingsStore.getValue(name, ev.getRoomId());
// Hide redacted events
// Deleted events with a thread are always shown regardless of user preference

View File

@ -127,7 +127,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
};
private async updateRooms(): Promise<void> {
let roomIds = SettingsStore.getValue<string[]>("breadcrumb_rooms");
let roomIds = SettingsStore.getValue("breadcrumb_rooms");
if (!roomIds || roomIds.length === 0) roomIds = [];
const rooms = filterBoolean(roomIds.map((r) => this.matrixClient?.getRoom(r)));

View File

@ -61,7 +61,7 @@ export class CallStore extends AsyncStoreWithClient<{}> {
// If the room ID of a previously connected call is still in settings at
// this time, that's a sign that we failed to disconnect from it
// properly, and need to clean up after ourselves
const uncleanlyDisconnectedRoomIds = SettingsStore.getValue<string[]>("activeCallRoomIds");
const uncleanlyDisconnectedRoomIds = SettingsStore.getValue("activeCallRoomIds");
if (uncleanlyDisconnectedRoomIds.length) {
await Promise.all([
...uncleanlyDisconnectedRoomIds.map(async (uncleanlyDisconnectedRoomId): Promise<void> => {

View File

@ -26,7 +26,9 @@ export type Feature = (typeof FEATURES)[number];
* The stored settings for the release announcements.
* The boolean is at true when the user has viewed the feature
*/
type StoredSettings = Record<Feature, boolean>;
type StoredSettings = Partial<Record<Feature, boolean>>;
export type ReleaseAnnouncementData = StoredSettings;
/**
* The events emitted by the ReleaseAnnouncementStore.
@ -82,7 +84,7 @@ export class ReleaseAnnouncementStore extends TypedEventEmitter<ReleaseAnnouncem
*/
private getViewedReleaseAnnouncements(): StoredSettings {
// Clone the settings to avoid to mutate the internal stored value in the SettingsStore
return cloneDeep(SettingsStore.getValue<StoredSettings>("releaseAnnouncementData"));
return cloneDeep(SettingsStore.getValue("releaseAnnouncementData"));
}
/**
@ -90,7 +92,7 @@ export class ReleaseAnnouncementStore extends TypedEventEmitter<ReleaseAnnouncem
* @private
*/
private isReleaseAnnouncementEnabled(): boolean {
return SettingsStore.getValue<boolean>(Features.ReleaseAnnouncement);
return SettingsStore.getValue(Features.ReleaseAnnouncement);
}
/**

View File

@ -252,10 +252,13 @@ export default class RightPanelStore extends ReadyWatchingStore {
const room = this.mxClient?.getRoom(this.viewedRoomId);
if (!!room) {
this.global =
this.global ?? convertToStatePanel(SettingsStore.getValue("RightPanel.phasesGlobal"), room);
this.global ??
convertToStatePanel(SettingsStore.getValue("RightPanel.phasesGlobal"), room) ??
undefined;
this.byRoom[this.viewedRoomId] =
this.byRoom[this.viewedRoomId] ??
convertToStatePanel(SettingsStore.getValue("RightPanel.phases", this.viewedRoomId), room);
convertToStatePanel(SettingsStore.getValue("RightPanel.phases", this.viewedRoomId), room) ??
undefined;
} else {
logger.warn(
"Could not restore the right panel after load because there was no associated room object.",

View File

@ -57,10 +57,10 @@ export interface IRightPanelForRoom {
history: Array<IRightPanelCard>;
}
interface IRightPanelForRoomStored {
export type IRightPanelForRoomStored = {
isOpen: boolean;
history: Array<IRightPanelCardStored>;
}
};
export function convertToStorePanel(cacheRoom?: IRightPanelForRoom): IRightPanelForRoomStored | undefined {
if (!cacheRoom) return undefined;
@ -68,7 +68,7 @@ export function convertToStorePanel(cacheRoom?: IRightPanelForRoom): IRightPanel
return { isOpen: cacheRoom.isOpen, history: storeHistory };
}
export function convertToStatePanel(storeRoom: IRightPanelForRoomStored, room: Room): IRightPanelForRoom {
export function convertToStatePanel(storeRoom: IRightPanelForRoomStored | null, room: Room): IRightPanelForRoom | null {
if (!storeRoom) return storeRoom;
const stateHistory = [...storeRoom.history].map((panelStateStore) => convertStoreToCard(panelStateStore, room));
return { history: stateHistory, isOpen: storeRoom.isOpen };

View File

@ -239,7 +239,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
if (!isMetaSpace(space)) {
cliSpace = this.matrixClient.getRoom(space);
if (!cliSpace?.isSpaceRoom()) return;
} else if (!this.enabledMetaSpaces.includes(space as MetaSpace)) {
} else if (!this.enabledMetaSpaces.includes(space)) {
return;
}
@ -1178,7 +1178,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
}
// restore selected state from last session if any and still valid
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY);
const lastSpaceId = window.localStorage.getItem(ACTIVE_SPACE_LS_KEY) as MetaSpace;
const valid =
lastSpaceId &&
(!isMetaSpace(lastSpaceId) ? this.matrixClient.getRoom(lastSpaceId) : enabledMetaSpaces[lastSpaceId]);

View File

@ -48,7 +48,7 @@ export interface ISuggestedRoom extends HierarchyRoom {
viaServers: string[];
}
export function isMetaSpace(spaceKey?: SpaceKey): boolean {
export function isMetaSpace(spaceKey?: SpaceKey): spaceKey is MetaSpace {
return (
spaceKey === MetaSpace.Home ||
spaceKey === MetaSpace.Favourites ||

View File

@ -25,9 +25,9 @@ import { Container, IStoredLayout, ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE,
export type { IStoredLayout, ILayoutStateEvent };
export { Container, WIDGET_LAYOUT_EVENT_TYPE };
interface ILayoutSettings extends ILayoutStateEvent {
export type ILayoutSettings = Partial<ILayoutStateEvent> & {
overrides?: string; // event ID for layout state event, if present
}
};
// Dev note: "Pinned" widgets are ones in the top container.
export const MAX_PINNED = 3;
@ -149,7 +149,7 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
const layoutEv = room.currentState.getStateEvents(WIDGET_LAYOUT_EVENT_TYPE, "");
const legacyPinned = SettingsStore.getValue("Widgets.pinned", room.roomId);
let userLayout = SettingsStore.getValue<ILayoutSettings | null>("Widgets.layout", room.roomId);
let userLayout = SettingsStore.getValue("Widgets.layout", room.roomId);
if (layoutEv && userLayout && userLayout.overrides !== layoutEv.getId()) {
// For some other layout that we don't really care about. The user can reset this

View File

@ -53,10 +53,7 @@ export class WidgetPermissionStore {
public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string | undefined, newState: OIDCState): void {
const settingsKey = this.packSettingKey(widget, kind, roomId);
let currentValues = SettingsStore.getValue<{
allow?: string[];
deny?: string[];
}>("widgetOpenIDPermissions");
let currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
if (!currentValues) {
currentValues = {};
}

View File

@ -263,9 +263,9 @@ export function getCustomTheme(themeName: string): CustomTheme {
if (!customThemes) {
throw new Error(`No custom themes set, can't set custom theme "${themeName}"`);
}
const customTheme = customThemes.find((t: ITheme) => t.name === themeName);
const customTheme = customThemes.find((t: CustomTheme) => t.name === themeName);
if (!customTheme) {
const knownNames = customThemes.map((t: ITheme) => t.name).join(", ");
const knownNames = customThemes.map((t: CustomTheme) => t.name).join(", ");
throw new Error(`Can't find custom theme "${themeName}", only know ${knownNames}`);
}
return customTheme;

View File

@ -20,6 +20,7 @@ import { IndicatorIcon } from "@vector-im/compound-web";
import SettingsStore from "../settings/SettingsStore";
import { NotificationLevel } from "../stores/notifications/NotificationLevel";
import { doesRoomHaveUnreadMessages } from "../Unread";
import { SettingKey } from "../settings/Settings.tsx";
// MSC2867 is not yet spec at time of writing. We read from both stable
// and unstable prefixes and accept the risk that the format may change,
@ -34,7 +35,7 @@ export const MARKED_UNREAD_TYPE_UNSTABLE = "com.famedly.marked_unread";
*/
export const MARKED_UNREAD_TYPE_STABLE = "m.marked_unread";
export const deviceNotificationSettingsKeys = [
export const deviceNotificationSettingsKeys: SettingKey[] = [
"notificationsEnabled",
"notificationBodyEnabled",
"audioNotificationsEnabled",

View File

@ -343,7 +343,7 @@ describe("Notifier", () => {
describe("getSoundForRoom", () => {
it("should not explode if given invalid url", () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
return { url: { content_uri: "foobar" } };
});
expect(Notifier.getSoundForRoom("!roomId:server")).toBeNull();

View File

@ -309,7 +309,7 @@ describe("SlidingSyncManager", () => {
});
it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => {
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy";
});
await manager.setup(client);

View File

@ -1493,7 +1493,7 @@ describe("<MatrixChat />", () => {
};
const enabledMobileRegistration = (): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => {
if (settingName === "Registration.mobileRegistrationHelper") return true;
if (settingName === UIFeature.Registration) return true;
});

View File

@ -303,8 +303,8 @@ describe("TimelinePanel", () => {
client.isVersionSupported.mockResolvedValue(true);
client.doesServerSupportUnstableFeature.mockResolvedValue(true);
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => {
if (setting === "sendReadReceipt") return false;
jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string): any => {
if (setting === "sendReadReceipts") return false;
return undefined;
});

View File

@ -14,13 +14,14 @@ import { shouldShowFeedback } from "../../../../../src/utils/Feedback";
import BetaCard from "../../../../../src/components/views/beta/BetaCard";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { TranslationKey } from "../../../../../src/languageHandler";
import { FeatureSettingKey } from "../../../../../src/settings/Settings.tsx";
jest.mock("../../../../../src/utils/Feedback");
jest.mock("../../../../../src/settings/SettingsStore");
describe("<BetaCard />", () => {
describe("Feedback prompt", () => {
const featureId = "featureId";
const featureId = "featureId" as FeatureSettingKey;
beforeEach(() => {
mocked(SettingsStore).getBetaInfo.mockReturnValue({

View File

@ -27,6 +27,7 @@ import {
import { UIFeature } from "../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { FeatureSettingKey } from "../../../../../src/settings/Settings.tsx";
mockPlatformPeg({
supportsSpellCheckSettings: jest.fn().mockReturnValue(false),
@ -111,13 +112,13 @@ describe("<UserSettingsDialog />", () => {
});
it("renders ignored users tab when feature_mjolnir is enabled", () => {
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === "feature_mjolnir");
mockSettingsStore.getValue.mockImplementation((settingName) => settingName === "feature_mjolnir");
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Mjolnir}`)).toBeTruthy();
});
it("renders voip tab when voip is enabled", () => {
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
mockSettingsStore.getValue.mockImplementation((settingName: any): any => settingName === UIFeature.Voip);
const { getByTestId } = render(getComponent());
expect(getByTestId(`settings-tab-${UserTab.Voice}`)).toBeTruthy();
});
@ -165,7 +166,7 @@ describe("<UserSettingsDialog />", () => {
it("renders with voip tab selected", () => {
useMockMediaDevices();
mockSettingsStore.getValue.mockImplementation((settingName): any => settingName === UIFeature.Voip);
mockSettingsStore.getValue.mockImplementation((settingName: any): any => settingName === UIFeature.Voip);
const { container } = render(getComponent({ initialTabId: UserTab.Voice }));
expect(getActiveTabLabel(container)).toEqual("Voice & Video");
@ -212,8 +213,11 @@ describe("<UserSettingsDialog />", () => {
});
it("renders labs tab when some feature is in beta", () => {
mockSettingsStore.getFeatureSettingNames.mockReturnValue(["feature_beta_setting", "feature_just_normal_labs"]);
mockSettingsStore.getBetaInfo.mockImplementation((settingName) =>
mockSettingsStore.getFeatureSettingNames.mockReturnValue([
"feature_beta_setting",
"feature_just_normal_labs",
] as unknown[] as FeatureSettingKey[]);
mockSettingsStore.getBetaInfo.mockImplementation((settingName: any) =>
settingName === "feature_beta_setting" ? ({} as any) : undefined,
);
const { getByTestId } = render(getComponent());

View File

@ -66,11 +66,13 @@ describe("MessageComposer", () => {
// restore settings
act(() => {
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
"feature_wysiwyg_composer",
].forEach((setting: string): void => {
(
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
"feature_wysiwyg_composer",
] as const
).forEach((setting): void => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
});
});
@ -188,11 +190,11 @@ describe("MessageComposer", () => {
// test button display depending on settings
[
{
setting: "MessageComposerInput.showStickersButton",
setting: "MessageComposerInput.showStickersButton" as const,
buttonLabel: "Sticker",
},
{
setting: "MessageComposerInput.showPollsButton",
setting: "MessageComposerInput.showPollsButton" as const,
buttonLabel: "Poll",
},
].forEach(({ setting, buttonLabel }) => {

View File

@ -176,7 +176,7 @@ describe("RoomHeader", () => {
});
it("opens the notifications panel", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "feature_notifications") return true;
});

View File

@ -44,7 +44,7 @@ describe("RoomPreviewCard", () => {
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
enabledFeatures = [];
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) =>
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any =>
enabledFeatures.includes(settingName) ? true : undefined,
);
});

View File

@ -419,7 +419,7 @@ describe("WysiwygComposer", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "MessageComposerInput.autoReplaceEmoji") return true;
});
customRender(onChange, onSend);
@ -455,7 +455,7 @@ describe("WysiwygComposer", () => {
const onChange = jest.fn();
const onSend = jest.fn();
beforeEach(async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name): any => {
if (name === "MessageComposerInput.ctrlEnterToSend") return true;
});
customRender(onChange, onSend);

View File

@ -57,7 +57,7 @@ describe("<LayoutSwitcher />", () => {
act(() => screen.getByRole("radio", { name: "Message bubbles" }).click());
expect(screen.getByRole("radio", { name: "Message bubbles" })).toBeChecked();
await waitFor(() => expect(SettingsStore.getValue<boolean>("layout")).toBe(Layout.Bubble));
await waitFor(() => expect(SettingsStore.getValue("layout")).toBe(Layout.Bubble));
});
});
@ -77,7 +77,7 @@ describe("<LayoutSwitcher />", () => {
await renderLayoutSwitcher();
act(() => screen.getByRole("checkbox", { name: "Show compact text and messages" }).click());
await waitFor(() => expect(SettingsStore.getValue<boolean>("useCompactLayout")).toBe(true));
await waitFor(() => expect(SettingsStore.getValue("useCompactLayout")).toBe(true));
});
it("should be disabled when the modern layout is not enabled", async () => {

View File

@ -40,7 +40,7 @@ describe("DiscoverySettings", () => {
const DiscoveryWrapper = (props = {}) => <MatrixClientContext.Provider value={client} {...props} />;
it("is empty if 3pid features are disabled", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((key) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((key: any): any => {
if (key === UIFeature.ThirdPartyID) return false;
});

View File

@ -69,7 +69,7 @@ describe("RolesRoomSettingsTab", () => {
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "feature_group_calls") return val;
});
};

View File

@ -17,6 +17,7 @@ import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
import MatrixClientBackedController from "../../../../../../../src/settings/controllers/MatrixClientBackedController";
import PlatformPeg from "../../../../../../../src/PlatformPeg";
import { SettingKey } from "../../../../../../../src/settings/Settings.tsx";
describe("PreferencesUserSettingsTab", () => {
beforeEach(() => {
@ -121,13 +122,13 @@ describe("PreferencesUserSettingsTab", () => {
const mockGetValue = (val: boolean) => {
const copyOfGetValueAt = SettingsStore.getValueAt;
SettingsStore.getValueAt = <T,>(
SettingsStore.getValueAt = (
level: SettingLevel,
name: string,
name: SettingKey,
roomId?: string,
isExplicit?: boolean,
): T => {
if (name === "sendReadReceipts") return val as T;
) => {
if (name === "sendReadReceipts") return val;
return copyOfGetValueAt(level, name, roomId, isExplicit);
};
};

View File

@ -19,6 +19,7 @@ import { populateThread } from "../../../../test-utils/threads";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import { Features } from "../../../../../src/settings/Settings.tsx";
describe("ThreadsActivityCentre", () => {
const getTACButton = () => {
@ -92,7 +93,7 @@ describe("ThreadsActivityCentre", () => {
});
beforeEach(async () => {
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, false);
await SettingsStore.setValue(Features.ReleaseAnnouncement, null, SettingLevel.DEVICE, false);
});
it("should render the threads activity centre button", async () => {
@ -102,7 +103,7 @@ describe("ThreadsActivityCentre", () => {
it("should render the release announcement", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
await SettingsStore.setValue(Features.ReleaseAnnouncement, null, SettingLevel.DEVICE, true);
renderTAC();
expect(document.body).toMatchSnapshot();
@ -110,7 +111,7 @@ describe("ThreadsActivityCentre", () => {
it("should render not display the tooltip when the release announcement is displayed", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
await SettingsStore.setValue(Features.ReleaseAnnouncement, null, SettingLevel.DEVICE, true);
renderTAC();
@ -121,7 +122,7 @@ describe("ThreadsActivityCentre", () => {
it("should close the release announcement when the TAC button is clicked", async () => {
// Enable release announcement
await SettingsStore.setValue("feature_release_announcement", null, SettingLevel.DEVICE, true);
await SettingsStore.setValue(Features.ReleaseAnnouncement, null, SettingLevel.DEVICE, true);
renderTAC();
await userEvent.click(getTACButton());

View File

@ -105,7 +105,7 @@ describe("createRoom", () => {
});
it("correctly sets up MSC3401 power levels", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
if (name === "feature_group_calls") return true;
});

View File

@ -48,6 +48,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../../src/stores/A
import { ElementWidgetActions } from "../../../src/stores/widgets/ElementWidgetActions";
import SettingsStore from "../../../src/settings/SettingsStore";
import { PosthogAnalytics } from "../../../src/PosthogAnalytics";
import { SettingKey } from "../../../src/settings/Settings.tsx";
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
[MediaDeviceKindEnum.AudioInput]: [
@ -63,7 +64,7 @@ jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");
const enabledSettings = new Set(["feature_group_calls", "feature_video_rooms", "feature_element_call_video_rooms"]);
jest.spyOn(SettingsStore, "getValue").mockImplementation(
(settingName) => enabledSettings.has(settingName) || undefined,
(settingName): any => enabledSettings.has(settingName) || undefined,
);
const setUpClientRoomAndStores = (): {
@ -709,16 +710,18 @@ describe("ElementCall", () => {
it("passes font settings through widget URL", async () => {
const originalGetValue = SettingsStore.getValue;
SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => {
SettingsStore.getValue = (name: SettingKey, roomId: string | null = null, excludeDefault = false): any => {
switch (name) {
case "fontSizeDelta":
return 4 as T;
return 4;
case "useSystemFont":
return true as T;
return true;
case "systemFont":
return "OpenDyslexic, DejaVu Sans" as T;
return "OpenDyslexic, DejaVu Sans";
default:
return originalGetValue<T>(name, roomId, excludeDefault);
return excludeDefault
? originalGetValue(name, roomId, excludeDefault)
: originalGetValue(name, roomId, excludeDefault);
}
};
document.documentElement.style.fontSize = "12px";
@ -746,12 +749,14 @@ describe("ElementCall", () => {
// Now test with the preference set to true
const originalGetValue = SettingsStore.getValue;
SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => {
SettingsStore.getValue = (name: SettingKey, roomId: string | null = null, excludeDefault = false): any => {
switch (name) {
case "fallbackICEServerAllowed":
return true as T;
return true;
default:
return originalGetValue<T>(name, roomId, excludeDefault);
return excludeDefault
? originalGetValue(name, roomId, excludeDefault)
: originalGetValue(name, roomId, excludeDefault);
}
};
@ -803,12 +808,14 @@ describe("ElementCall", () => {
it("passes feature_allow_screen_share_only_mode setting to allowVoipWithNoMedia url param", async () => {
// Now test with the preference set to true
const originalGetValue = SettingsStore.getValue;
SettingsStore.getValue = <T>(name: string, roomId?: string, excludeDefault?: boolean) => {
SettingsStore.getValue = (name: SettingKey, roomId: string | null = null, excludeDefault = false): any => {
switch (name) {
case "feature_allow_screen_share_only_mode":
return true as T;
return true;
default:
return originalGetValue<T>(name, roomId, excludeDefault);
return excludeDefault
? originalGetValue(name, roomId, excludeDefault)
: originalGetValue(name, roomId, excludeDefault);
}
};
await ElementCall.create(room);

View File

@ -13,10 +13,11 @@ import SdkConfig from "../../../src/SdkConfig";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import SettingsStore from "../../../src/settings/SettingsStore";
import { mkStubRoom, mockPlatformPeg, stubClient } from "../../test-utils";
import { SettingKey } from "../../../src/settings/Settings.tsx";
const TEST_DATA = [
{
name: "Electron.showTrayIcon",
name: "Electron.showTrayIcon" as SettingKey,
level: SettingLevel.PLATFORM,
value: true,
},

View File

@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import IncompatibleController from "../../../../src/settings/controllers/IncompatibleController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { FeatureSettingKey } from "../../../../src/settings/Settings.tsx";
describe("IncompatibleController", () => {
const settingsGetValueSpy = jest.spyOn(SettingsStore, "getValue");
@ -20,7 +21,7 @@ describe("IncompatibleController", () => {
describe("when incompatibleValue is not set", () => {
it("returns true when setting value is true", () => {
// no incompatible value set, defaulted to true
const controller = new IncompatibleController("feature_spotlight", { key: null });
const controller = new IncompatibleController("feature_spotlight" as FeatureSettingKey, { key: null });
settingsGetValueSpy.mockReturnValue(true);
// true === true
expect(controller.incompatibleSetting).toBe(true);
@ -30,7 +31,7 @@ describe("IncompatibleController", () => {
it("returns false when setting value is not true", () => {
// no incompatible value set, defaulted to true
const controller = new IncompatibleController("feature_spotlight", { key: null });
const controller = new IncompatibleController("feature_spotlight" as FeatureSettingKey, { key: null });
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(false);
});
@ -38,13 +39,21 @@ describe("IncompatibleController", () => {
describe("when incompatibleValue is set to a value", () => {
it("returns true when setting value matches incompatible value", () => {
const controller = new IncompatibleController("feature_spotlight", { key: null }, "test");
const controller = new IncompatibleController(
"feature_spotlight" as FeatureSettingKey,
{ key: null },
"test",
);
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(true);
});
it("returns false when setting value is not true", () => {
const controller = new IncompatibleController("feature_spotlight", { key: null }, "test");
const controller = new IncompatibleController(
"feature_spotlight" as FeatureSettingKey,
{ key: null },
"test",
);
settingsGetValueSpy.mockReturnValue("not test");
expect(controller.incompatibleSetting).toBe(false);
});
@ -53,7 +62,11 @@ describe("IncompatibleController", () => {
describe("when incompatibleValue is set to a function", () => {
it("returns result from incompatibleValue function", () => {
const incompatibleValueFn = jest.fn().mockReturnValue(false);
const controller = new IncompatibleController("feature_spotlight", { key: null }, incompatibleValueFn);
const controller = new IncompatibleController(
"feature_spotlight" as FeatureSettingKey,
{ key: null },
incompatibleValueFn,
);
settingsGetValueSpy.mockReturnValue("test");
expect(controller.incompatibleSetting).toBe(false);
expect(incompatibleValueFn).toHaveBeenCalledWith("test");
@ -64,7 +77,7 @@ describe("IncompatibleController", () => {
describe("getValueOverride()", () => {
it("returns forced value when setting is incompatible", () => {
settingsGetValueSpy.mockReturnValue(true);
const controller = new IncompatibleController("feature_spotlight", { key: null });
const controller = new IncompatibleController("feature_spotlight" as FeatureSettingKey, { key: null });
expect(
controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", true, SettingLevel.ACCOUNT),
).toEqual({ key: null });
@ -72,7 +85,7 @@ describe("IncompatibleController", () => {
it("returns null when setting is not incompatible", () => {
settingsGetValueSpy.mockReturnValue(false);
const controller = new IncompatibleController("feature_spotlight", { key: null });
const controller = new IncompatibleController("feature_spotlight" as FeatureSettingKey, { key: null });
expect(
controller.getValueOverride(SettingLevel.ACCOUNT, "$room:server", true, SettingLevel.ACCOUNT),
).toEqual(null);

View File

@ -11,7 +11,7 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import ServerSupportUnstableFeatureController from "../../../../src/settings/controllers/ServerSupportUnstableFeatureController";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { LabGroup, SETTINGS } from "../../../../src/settings/Settings";
import { FeatureSettingKey, LabGroup, SETTINGS } from "../../../../src/settings/Settings";
import { stubClient } from "../../../test-utils";
import { WatchManager } from "../../../../src/settings/WatchManager";
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
@ -19,7 +19,7 @@ import { TranslationKey } from "../../../../src/languageHandler";
describe("ServerSupportUnstableFeatureController", () => {
const watchers = new WatchManager();
const setting = "setting_name";
const setting = "setting_name" as FeatureSettingKey;
async function prepareSetting(
cli: MatrixClient,

View File

@ -17,7 +17,7 @@ describe("SystemFontController", () => {
it("dispatches a system font update action on change", () => {
const controller = new SystemFontController();
const getValueSpy = jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
const getValueSpy = jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName): any => {
if (settingName === "useBundledEmojiFont") return false;
if (settingName === "useSystemFont") return true;
if (settingName === "systemFont") return "Comic Sans MS";

View File

@ -9,6 +9,7 @@ Please see LICENSE files in the repository root for full details.
import SettingsStore from "../../../../src/settings/SettingsStore";
import ThemeWatcher from "../../../../src/settings/watchers/ThemeWatcher";
import { SettingLevel } from "../../../../src/settings/SettingLevel";
import { SettingKey, Settings } from "../../../../src/settings/Settings.tsx";
function makeMatchMedia(values: any) {
class FakeMediaQueryList {
@ -33,8 +34,12 @@ function makeMatchMedia(values: any) {
};
}
function makeGetValue(values: any) {
return function getValue<T = any>(settingName: string, _roomId: string | null = null, _excludeDefault = false): T {
function makeGetValue(values: any): any {
return function getValue<S extends SettingKey>(
settingName: S,
_roomId: string | null = null,
_excludeDefault = false,
): Settings[S] {
return values[settingName];
};
}

View File

@ -21,6 +21,7 @@ import { getMockClientWithEventEmitter, mockClientMethodsCrypto, mockPlatformPeg
import { collectBugReport } from "../../src/rageshake/submit-rageshake";
import SettingsStore from "../../src/settings/SettingsStore";
import { ConsoleLogger } from "../../src/rageshake/rageshake";
import { FeatureSettingKey, SettingKey } from "../../src/settings/Settings.tsx";
describe("Rageshakes", () => {
const RUST_CRYPTO_VERSION = "Rust SDK 0.7.0 (691ec63), Vodozemac 0.5.0";
@ -376,8 +377,11 @@ describe("Rageshakes", () => {
const mockSettingsStore = mocked(SettingsStore);
it("should collect labs from settings store", async () => {
const someFeatures: string[] = ["feature_video_rooms", "feature_notification_settings2"];
const enabledFeatures: string[] = ["feature_video_rooms"];
const someFeatures = [
"feature_video_rooms",
"feature_notification_settings2",
] as unknown[] as FeatureSettingKey[];
const enabledFeatures: SettingKey[] = ["feature_video_rooms"];
jest.spyOn(mockSettingsStore, "getFeatureSettingNames").mockReturnValue(someFeatures);
jest.spyOn(mockSettingsStore, "getValue").mockImplementation((settingName): any => {
return enabledFeatures.includes(settingName);

View File

@ -149,7 +149,7 @@ describe("theme", () => {
});
it("should be robust to malformed custom_themes values", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue([23]);
jest.spyOn(SettingsStore, "getValue").mockReturnValue([23] as any);
expect(enumerateThemes()).toEqual({
"light": "Light",
"light-high-contrast": "Light high contrast",

View File

@ -44,7 +44,7 @@ describe("PlainTextExport", () => {
[24, false, "Fri, Apr 16, 2021, 17:20:00 - @alice:example.com: Hello, world!\n"],
[12, true, "Fri, Apr 16, 2021, 5:20:00 PM - @alice:example.com: Hello, world!\n"],
])("should return text with %i hr time format", async (hour: number, setting: boolean, expectedMessage: string) => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string) =>
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName: string): any =>
settingName === "showTwelveHourTimestamps" ? setting : undefined,
);
const events: MatrixEvent[] = [