diff --git a/scripts/analyse_unused_exports.ts b/scripts/analyse_unused_exports.ts index 46e594d2b74..b20e6b285a0 100755 --- a/scripts/analyse_unused_exports.ts +++ b/scripts/analyse_unused_exports.ts @@ -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"); diff --git a/src/@types/common.ts b/src/@types/common.ts index f80b66a6322..b0fc694493d 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -44,3 +44,11 @@ type DeepReadonlyObject = { }; export type AtLeastOne }> = Partial & 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 = { + [Key in keyof Object]: Object[Key] extends Item ? Key : never; +}[keyof Object]; diff --git a/src/@types/json.ts b/src/@types/json.ts new file mode 100644 index 00000000000..ae88680fc83 --- /dev/null +++ b/src/@types/json.ts @@ -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; +export interface JsonObject { + [key: string]: JsonObject | JsonArray | JsonValue; +} +export type Json = JsonArray | JsonObject; diff --git a/src/Notifier.ts b/src/Notifier.ts index c724c4780cd..a582c2eb6fa 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -176,7 +176,7 @@ class NotifierClass extends TypedEventEmitter { } else { backgroundImage = OwnProfileStore.instance.getHttpAvatarUrl(); } - this.setState({ backgroundImage }); + this.setState({ backgroundImage: backgroundImage ?? undefined }); }; public canResetTimelineInRoom = (roomId: string): boolean => { diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 8cd9122e909..9b55d17a100 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -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 { diff --git a/src/components/views/context_menus/RoomGeneralContextMenu.tsx b/src/components/views/context_menus/RoomGeneralContextMenu.tsx index 714f97ed988..b2abac19853 100644 --- a/src/components/views/context_menus/RoomGeneralContextMenu.tsx +++ b/src/components/views/context_menus/RoomGeneralContextMenu.tsx @@ -282,7 +282,7 @@ export const RoomGeneralContextMenu: React.FC = ({ } })(); - const developerModeEnabled = useSettingValue("developerMode"); + const developerModeEnabled = useSettingValue("developerMode"); const developerToolsOption = developerModeEnabled ? ( ) : null; diff --git a/src/components/views/context_menus/WidgetContextMenu.tsx b/src/components/views/context_menus/WidgetContextMenu.tsx index 46052de9ff2..cf1fa756e72 100644 --- a/src/components/views/context_menus/WidgetContextMenu.tsx +++ b/src/components/views/context_menus/WidgetContextMenu.tsx @@ -71,7 +71,7 @@ const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() => const showSnapshotButton = (widgetMessaging: ClientWidgetApi | undefined): boolean => { return ( - SettingsStore.getValue("enableWidgetScreenshots") && + SettingsStore.getValue("enableWidgetScreenshots") && !!widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots) ); }; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 31162b45f41..fd562583dbc 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -131,7 +131,7 @@ export const AddExistingToSpace: React.FC = ({ onFinished, }) => { const cli = useContext(MatrixClientContext); - const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors"); + const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors"); const visibleRooms = useMemo( () => cli diff --git a/src/components/views/dialogs/BetaFeedbackDialog.tsx b/src/components/views/dialogs/BetaFeedbackDialog.tsx index 9f4852cf2ac..c479e1dfe0d 100644 --- a/src/components/views/dialogs/BetaFeedbackDialog.tsx +++ b/src/components/views/dialogs/BetaFeedbackDialog.tsx @@ -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 = ({ featureId, onFinished }) => { rageshakeLabel={info.feedbackLabel} rageshakeData={Object.fromEntries( (SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map((k) => { - return SettingsStore.getValue(k); + return [k, SettingsStore.getValue(k)]; }), )} > diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 0a0a70d1b5b..687e0f9eedd 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -253,8 +253,8 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const previewLayout = useSettingValue("layout"); - const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors"); + const previewLayout = useSettingValue("layout"); + const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors"); let rooms = useMemo( () => diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index 1796b79239e..2d3a2771883 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -100,8 +100,8 @@ type ShareDialogProps = XOR; * 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(UIFeature.ShareQRCode); - const showSocials = useSettingValue(UIFeature.ShareSocial); + const showQrCode = useSettingValue(UIFeature.ShareQRCode); + const showSocials = useSettingValue(UIFeature.ShareSocial); const timeoutIdRef = useRef(); const [isCopied, setIsCopied] = useState(false); diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 8ae7a302ac4..473ed2efe30 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -85,8 +85,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode { } export default function UserSettingsDialog(props: IProps): JSX.Element { - const voipEnabled = useSettingValue(UIFeature.Voip); - const mjolnirEnabled = useSettingValue("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); diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index ae37fa3e1c9..e7c0efaa95e 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -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 = ({ onBack }) => { - const [setting, setSetting] = useState(null); + const [setting, setSetting] = useState(null); const [editing, setEditing] = useState(false); if (setting && editing) { @@ -36,10 +36,10 @@ const SettingExplorer: React.FC = ({ onBack }) => { }; return ; } 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 = ({ onBack }) => { export default SettingExplorer; interface ICanEditLevelFieldProps { - setting: string; + setting: SettingKey; level: SettingLevel; roomId?: string; } @@ -65,8 +65,8 @@ const CanEditLevelField: React.FC = ({ setting, roomId, ); }; -function renderExplicitSettingValues(setting: string, roomId?: string): string { - const vals: Record = {}; +function renderExplicitSettingValues(setting: SettingKey, roomId?: string): string { + const vals: Record = {}; 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 { - setting: string; + setting: SettingKey; } const EditSetting: React.FC = ({ setting, onBack }) => { @@ -191,7 +191,7 @@ const EditSetting: React.FC = ({ setting, onBack }) => { }; interface IViewSettingProps extends Pick { - setting: string; + setting: SettingKey; onEdit(): Promise; } @@ -258,7 +258,7 @@ const SettingsList: React.FC = ({ 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)); diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index a1b1986f471..d753ec1bdd5 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -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({ memoize: true, }); -function useSettingsValueWithSetter( - settingName: string, +function useSettingsValueWithSetter( + settingName: S, + level: SettingLevel, + roomId: string | null, + excludeDefault: true, +): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise]; +function useSettingsValueWithSetter( + settingName: S, + level: SettingLevel, + roomId?: string | null, + excludeDefault?: false, +): [Settings[S]["default"], (value: Settings[S]["default"]) => Promise]; +function useSettingsValueWithSetter( + settingName: S, level: SettingLevel, roomId: string | null = null, excludeDefault = false, -): [T, (value: T) => Promise] { - const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId ?? undefined, excludeDefault)); +): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise] { + 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 => { + async (value: Settings[S]["default"]): Promise => { setValue(value); SettingsStore.setValue(settingName, roomId, level, value); }, @@ -84,7 +102,12 @@ function useSettingsValueWithSetter( useEffect(() => { const ref = SettingsStore.watchSetting(settingName, roomId, () => { - setValue(SettingsStore.getValue(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(target: Set, ...toRemove: T[]): void { } function useServers(): ServerList { - const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter( - SETTING_NAME, - SettingLevel.ACCOUNT, - ); + const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter(SETTING_NAME, SettingLevel.ACCOUNT); const homeServer = MatrixClientPeg.safeGet().getDomain()!; const configServers = new Set(SdkConfig.getObject("room_directory")?.get("servers") ?? []); diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 27b4b2b31ce..753cf9e1450 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -105,7 +105,7 @@ export default class LanguageDropdown 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("language", null, /*excludeDefault:*/ true); + let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); let value: string | undefined; if (language) { value = this.props.value || language; diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 07fa3fd7fd2..2af9b9aa712 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -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; diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index ce521c959a1..3014ee959b8 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -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("language", null, /*excludeDefault:*/ true); + let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); let value: string | undefined; if (language) { value = this.props.value || language; diff --git a/src/components/views/messages/CodeBlock.tsx b/src/components/views/messages/CodeBlock.tsx index 80619914058..2fd89417659 100644 --- a/src/components/views/messages/CodeBlock.tsx +++ b/src/components/views/messages/CodeBlock.tsx @@ -36,9 +36,9 @@ const ExpandCollapseButton: React.FC<{ }; const CodeBlock: React.FC = ({ children, onHeightChanged }) => { - const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection"); - const showCodeLineNumbers = useSettingValue("showCodeLineNumbers"); - const expandCodeByDefault = useSettingValue("expandCodeByDefault"); + const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection"); + const showCodeLineNumbers = useSettingValue("showCodeLineNumbers"); + const expandCodeByDefault = useSettingValue("expandCodeByDefault"); const [expanded, setExpanded] = useState(expandCodeByDefault); let expandCollapseButton: JSX.Element | undefined; diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index 242feff6d4b..9c10d6ca0ca 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -426,7 +426,7 @@ export default class TextualBody extends React.Component { const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent); const htmlOpts = { - disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"), + disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"), // Part of Replies fallback support stripReplyFallback: stripReply, }; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index f5716d728b6..508b64eec2c 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -128,7 +128,7 @@ export class MessageComposer extends React.Component { super(props, context); this.context = context; // otherwise React will only set it prior to render due to type def above - const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); + const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer"); let isRichTextEnabled = true; let initialComposerContent = ""; if (isWysiwygLabEnabled) { diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index 19b86834dd7..e54306caff3 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -54,7 +54,7 @@ const MessageComposerButtons: React.FC = (props: IProps) => { const matrixClient = useContext(MatrixClientContext); const { room, narrow } = useScopedRoomContext("room", "narrow"); - const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); + const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer"); if (!matrixClient || !room || props.haveRecording) { return null; diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index 787f0dd889d..3430994e015 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -45,7 +45,7 @@ export function PlainTextComposer({ rightComponent, eventRelation, }: PlainTextComposerProps): JSX.Element { - const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); + const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); const { ref: editorRef, autocompleteRef, diff --git a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx index f1e42ce091f..f72fa44a0ed 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer.tsx @@ -61,7 +61,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({ const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation); - const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); + const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji"); const emojiSuggestions = useMemo(() => getEmojiSuggestions(isAutoReplaceEmojiEnabled), [isAutoReplaceEmojiEnabled]); const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({ diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts index cab3bdefb85..33314ef4180 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useInputEventProcessor.ts @@ -36,7 +36,7 @@ export function useInputEventProcessor( const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType"); const composerContext = useComposerContext(); const mxClient = useMatrixClientContext(); - const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend"); + const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend"); return useCallback( (event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => { diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index 1dc23cc274a..2e9dd7e32d3 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -128,7 +128,7 @@ export function usePlainTextListeners( [eventRelation, mxClient, onInput, roomContext], ); - const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend"); + const enterShouldSend = !useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onKeyDown = useCallback( (event: KeyboardEvent) => { // we need autocomplete to take priority when it is open for using enter to select diff --git a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts index 58d09b3d128..c73f6332ee7 100644 --- a/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts +++ b/src/components/views/rooms/wysiwyg_composer/utils/createMessageContent.ts @@ -66,7 +66,7 @@ export async function createMessageContent( // TODO markdown support - const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown"); + const isMarkdownEnabled = SettingsStore.getValue("MessageComposerInput.useMarkdown"); const formattedBody = isHTML ? message : isMarkdownEnabled ? await plainToRich(message, true) : null; if (formattedBody) { diff --git a/src/components/views/settings/FontScalingPanel.tsx b/src/components/views/settings/FontScalingPanel.tsx index edc6c66645e..034fbc0631f 100644 --- a/src/components/views/settings/FontScalingPanel.tsx +++ b/src/components/views/settings/FontScalingPanel.tsx @@ -47,7 +47,7 @@ export default class FontScalingPanel extends React.Component { super(props); this.state = { - fontSizeDelta: SettingsStore.getValue("fontSizeDelta", null), + fontSizeDelta: SettingsStore.getValue("fontSizeDelta", null), browserFontSize: FontWatcher.getBrowserDefaultFontSize(), useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), layout: SettingsStore.getValue("layout"), diff --git a/src/components/views/settings/LayoutSwitcher.tsx b/src/components/views/settings/LayoutSwitcher.tsx index bbf090aa383..4eaf8e66193 100644 --- a/src/components/views/settings/LayoutSwitcher.tsx +++ b/src/components/views/settings/LayoutSwitcher.tsx @@ -70,7 +70,7 @@ interface LayoutRadioProps { * @param label */ function LayoutRadio({ layout, label }: LayoutRadioProps): JSX.Element { - const currentLayout = useSettingValue("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("useCompactLayout"); - const layout = useSettingValue("layout"); + const compactLayoutEnabled = useSettingValue("useCompactLayout"); + const layout = useSettingValue("layout"); return ( ("feature_custom_themes"); + const customThemeEnabled = useSettingValue("feature_custom_themes"); return ( @@ -159,7 +159,7 @@ function ThemeSelectors({ theme, disabled }: ThemeSelectorProps): JSX.Element { * Return all the available themes */ function useThemes(): Array { - const customThemes = useSettingValue("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("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("custom_themes") || []; + const customThemes = useSettingValue("custom_themes") || []; return (
    @@ -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("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); diff --git a/src/components/views/settings/notifications/NotificationSettings2.tsx b/src/components/views/settings/notifications/NotificationSettings2.tsx index 5f91c3874c7..38d4c3ce193 100644 --- a/src/components/views/settings/notifications/NotificationSettings2.tsx +++ b/src/components/views/settings/notifications/NotificationSettings2.tsx @@ -70,9 +70,9 @@ function useHasUnreadNotifications(): boolean { export default function NotificationSettings2(): JSX.Element { const cli = useMatrixClientContext(); - const desktopNotifications = useSettingValue("notificationsEnabled"); - const desktopShowBody = useSettingValue("notificationBodyEnabled"); - const audioNotifications = useSettingValue("audioNotificationsEnabled"); + const desktopNotifications = useSettingValue("notificationsEnabled"); + const desktopShowBody = useSettingValue("notificationBodyEnabled"); + const audioNotifications = useSettingValue("audioNotificationsEnabled"); const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli); diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx index 54995415e2e..0e1654bbf46 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx @@ -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; diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 8cb662a9f02..db214bd7edc 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -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 { - 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 ); } @@ -232,7 +237,7 @@ export default class PreferencesUserSettingsTab extends React.Component("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)); diff --git a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx index 0971ece699f..413551e9000 100644 --- a/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/SidebarUserSettingsTab.tsx @@ -58,8 +58,8 @@ const SidebarUserSettingsTab: React.FC = () => { [MetaSpace.People]: peopleEnabled, [MetaSpace.Orphans]: orphansEnabled, [MetaSpace.VideoRooms]: videoRoomsEnabled, - } = useSettingValue>("Spaces.enabledMetaSpaces"); - const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); + } = useSettingValue("Spaces.enabledMetaSpaces"); + const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); const guestSpaUrl = useMemo(() => { return SdkConfig.get("element_call").guest_spa_url; }, []); diff --git a/src/components/views/spaces/QuickSettingsButton.tsx b/src/components/views/spaces/QuickSettingsButton.tsx index 161290fca88..f7b2691fdda 100644 --- a/src/components/views/spaces/QuickSettingsButton.tsx +++ b/src/components/views/spaces/QuickSettingsButton.tsx @@ -36,10 +36,10 @@ const QuickSettingsButton: React.FC<{ const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); const { [MetaSpace.Favourites]: favouritesEnabled, [MetaSpace.People]: peopleEnabled } = - useSettingValue>("Spaces.enabledMetaSpaces"); + useSettingValue("Spaces.enabledMetaSpaces"); const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId(); - const developerModeEnabled = useSettingValue("developerMode"); + const developerModeEnabled = useSettingValue("developerMode"); let contextMenu: JSX.Element | undefined; if (menuDisplayed && handle.current) { diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 73bb66af380..7f888370556 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -88,7 +88,7 @@ export const HomeButtonContextMenu: React.FC { - const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); + const allRoomsInHome = useSettingValue("Spaces.allRoomsInHome"); return ( diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx index c8e5ef1424b..5d08744fc79 100644 --- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx +++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx @@ -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("Notifications.tac_only_notifications"); + const settingTACOnlyNotifs = useSettingValue("Notifications.tac_only_notifications"); const emptyCaption = settingTACOnlyNotifs ? _t("threads_activity_centre|no_rooms_with_threads_notifs") diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts index 110c9d51f83..574d7459eaf 100644 --- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts +++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts @@ -32,8 +32,8 @@ type Result = { * @returns {Result} */ export function useUnreadThreadRooms(forceComputation: boolean): Result { - const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors"); - const settingTACOnlyNotifs = useSettingValue("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({ greatestNotificationLevel: NotificationLevel.None, rooms: [] }); diff --git a/src/components/views/user-onboarding/UserOnboardingButton.tsx b/src/components/views/user-onboarding/UserOnboardingButton.tsx index d1e420d4ac7..b6002f1d0b6 100644 --- a/src/components/views/user-onboarding/UserOnboardingButton.tsx +++ b/src/components/views/user-onboarding/UserOnboardingButton.tsx @@ -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("FTUE.useCaseSelection"); - const visible = useSettingValue("FTUE.userOnboardingButton"); + const useCase = useSettingValue("FTUE.useCaseSelection"); + const visible = useSettingValue("FTUE.userOnboardingButton"); if (!visible || minimized || !showUserOnboardingPage(useCase)) { return <>; diff --git a/src/components/views/user-onboarding/UserOnboardingPage.tsx b/src/components/views/user-onboarding/UserOnboardingPage.tsx index ebb9b9565e3..d150c782c9e 100644 --- a/src/components/views/user-onboarding/UserOnboardingPage.tsx +++ b/src/components/views/user-onboarding/UserOnboardingPage.tsx @@ -41,7 +41,7 @@ export function UserOnboardingPage({ justRegistered = false }: Props): JSX.Eleme const config = SdkConfig.get(); const pageUrl = getHomePageUrl(config, cli); - const useCase = useSettingValue("FTUE.useCaseSelection"); + const useCase = useSettingValue("FTUE.useCaseSelection"); const context = useUserOnboardingContext(); const tasks = useUserOnboardingTasks(context); diff --git a/src/emojipicker/recent.ts b/src/emojipicker/recent.ts index f95e440cc7c..44b04778d7a 100644 --- a/src/emojipicker/recent.ts +++ b/src/emojipicker/recent.ts @@ -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) || []; } diff --git a/src/hooks/spotlight/useRecentSearches.ts b/src/hooks/spotlight/useRecentSearches.ts index 1ce092a45de..33942c64cc5 100644 --- a/src/hooks/spotlight/useRecentSearches.ts +++ b/src/hooks/spotlight/useRecentSearches.ts @@ -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("SpotlightSearch.recentSearches", null); + const recents = SettingsStore.getValue("SpotlightSearch.recentSearches", null); return filterBoolean(recents.map((r) => cli.getRoom(r))); }); diff --git a/src/hooks/usePublicRoomDirectory.ts b/src/hooks/usePublicRoomDirectory.ts index 339ee9ad6dc..be3f41b6633 100644 --- a/src/hooks/usePublicRoomDirectory.ts +++ b/src/hooks/usePublicRoomDirectory.ts @@ -55,7 +55,7 @@ export const usePublicRoomDirectory = (): { const [updateQuery, updateResult] = useLatestResult(setPublicRooms); - const showNsfwPublicRooms = useSettingValue("SpotlightSearch.showNsfwPublicRooms"); + const showNsfwPublicRooms = useSettingValue("SpotlightSearch.showNsfwPublicRooms"); async function initProtocols(): Promise { if (!MatrixClientPeg.get()) { diff --git a/src/hooks/useSettings.ts b/src/hooks/useSettings.ts index 169fccb7c0e..efb04e459a1 100644 --- a/src/hooks/useSettings.ts +++ b/src/hooks/useSettings.ts @@ -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 = (settingName: string, roomId: string | null = null, excludeDefault = false): T => { - const [value, setValue] = useState(SettingsStore.getValue(settingName, roomId, excludeDefault)); +export function useSettingValue( + settingName: S, + roomId: string | null, + excludeDefault: true, +): Settings[S]["default"] | undefined; +export function useSettingValue( + settingName: S, + roomId?: string | null, + excludeDefault?: false, +): Settings[S]["default"]; +export function useSettingValue( + 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(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 = (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 = (settingName: string, roomId: string | null = * @param explicit * @param excludeDefault */ -export const useSettingValueAt = ( +export const useSettingValueAt = ( level: SettingLevel, - settingName: string, + settingName: S, roomId: string | null = null, explicit = false, excludeDefault = false, -): T => { - const [value, setValue] = useState( - SettingsStore.getValueAt(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(level, settingName, roomId, explicit, excludeDefault)); + setValue(SettingsStore.getValueAt(level, settingName, roomId, explicit, excludeDefault)); }); // clean-up return () => { @@ -62,8 +85,8 @@ export const useSettingValueAt = ( }; // 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(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, () => { diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts index e459e68011a..5afcf9ea1da 100644 --- a/src/hooks/useTheme.ts +++ b/src/hooks/useTheme.ts @@ -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("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"); + 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) { diff --git a/src/hooks/useUserOnboardingTasks.ts b/src/hooks/useUserOnboardingTasks.ts index 2ede7076cbd..52463e1487d 100644 --- a/src/hooks/useUserOnboardingTasks.ts +++ b/src/hooks/useUserOnboardingTasks.ts @@ -145,7 +145,7 @@ const tasks: UserOnboardingTask[] = [ ]; export function useUserOnboardingTasks(context: UserOnboardingContext): UserOnboardingTaskWithResolvedCompletion[] { - const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; + const useCase = useSettingValue("FTUE.useCaseSelection") ?? UseCase.Skip; return useMemo(() => { return tasks diff --git a/src/models/Call.ts b/src/models/Call.ts index 4beb5fccc1e..fe92bed4c71 100644 --- a/src/models/Call.ts +++ b/src/models/Call.ts @@ -688,7 +688,7 @@ export class ElementCall extends Call { // Set custom fonts if (SettingsStore.getValue("useSystemFont")) { - SettingsStore.getValue("systemFont") + SettingsStore.getValue("systemFont") .split(",") .map((font) => { // Strip whitespace and quotes diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 81c4fb6ef28..649dca0e233 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -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.Ui]: _td("labs|group_ui"), }; -export type SettingValueType = - | boolean - | number - | string - | number[] - | string[] - | Record - | Record[] - | null; +export type SettingValueType = Json | JsonValue | Record | Record[]; export interface IBaseSetting { isFeature?: false | undefined; @@ -164,7 +163,7 @@ export interface IBaseSetting { image?: string; // require(...) feedbackSubheading?: TranslationKey; feedbackLabel?: string; - extraSettings?: string[]; + extraSettings?: BooleanSettingKey[]; requiresRefresh?: boolean; }; @@ -181,7 +180,179 @@ export interface IFeature extends Omit, "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; + + // 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; + "feature_hidebold": IBaseSetting; + + "useOnlyCurrentProfiles": IBaseSetting; + "mjolnirRooms": IBaseSetting; + "mjolnirPersonalRoom": IBaseSetting; + "RoomList.backgroundImage": IBaseSetting; + "sendReadReceipts": IBaseSetting; + "baseFontSize": IBaseSetting<"" | number>; + "baseFontSizeV2": IBaseSetting<"" | number>; + "fontSizeDelta": IBaseSetting; + "useCustomFontSize": IBaseSetting; + "MessageComposerInput.suggestEmoji": IBaseSetting; + "MessageComposerInput.showStickersButton": IBaseSetting; + "MessageComposerInput.showPollsButton": IBaseSetting; + "MessageComposerInput.insertTrailingColon": IBaseSetting; + "Notifications.alwaysShowBadgeCounts": IBaseSetting; + "Notifications.showbold": IBaseSetting; + "Notifications.tac_only_notifications": IBaseSetting; + "useCompactLayout": IBaseSetting; + "showRedactions": IBaseSetting; + "showJoinLeaves": IBaseSetting; + "showAvatarChanges": IBaseSetting; + "showDisplaynameChanges": IBaseSetting; + "showReadReceipts": IBaseSetting; + "showTwelveHourTimestamps": IBaseSetting; + "alwaysShowTimestamps": IBaseSetting; + "userTimezone": IBaseSetting; + "userTimezonePublish": IBaseSetting; + "autoplayGifs": IBaseSetting; + "autoplayVideo": IBaseSetting; + "enableSyntaxHighlightLanguageDetection": IBaseSetting; + "expandCodeByDefault": IBaseSetting; + "showCodeLineNumbers": IBaseSetting; + "scrollToBottomOnMessageSent": IBaseSetting; + "Pill.shouldShowPillAvatar": IBaseSetting; + "TextualBody.enableBigEmoji": IBaseSetting; + "MessageComposerInput.isRichTextEnabled": IBaseSetting; + "MessageComposer.showFormatting": IBaseSetting; + "sendTypingNotifications": IBaseSetting; + "showTypingNotifications": IBaseSetting; + "ctrlFForSearch": IBaseSetting; + "MessageComposerInput.ctrlEnterToSend": IBaseSetting; + "MessageComposerInput.surroundWith": IBaseSetting; + "MessageComposerInput.autoReplaceEmoji": IBaseSetting; + "MessageComposerInput.useMarkdown": IBaseSetting; + "VideoView.flipVideoHorizontally": IBaseSetting; + "theme": IBaseSetting; + "custom_themes": IBaseSetting; + "use_system_theme": IBaseSetting; + "useBundledEmojiFont": IBaseSetting; + "useSystemFont": IBaseSetting; + "systemFont": IBaseSetting; + "webRtcAllowPeerToPeer": IBaseSetting; + "webrtc_audiooutput": IBaseSetting; + "webrtc_audioinput": IBaseSetting; + "webrtc_videoinput": IBaseSetting; + "webrtc_audio_autoGainControl": IBaseSetting; + "webrtc_audio_echoCancellation": IBaseSetting; + "webrtc_audio_noiseSuppression": IBaseSetting; + "language": IBaseSetting; + "breadcrumb_rooms": IBaseSetting; + "recent_emoji": IBaseSetting; + "SpotlightSearch.recentSearches": IBaseSetting; + "SpotlightSearch.showNsfwPublicRooms": IBaseSetting; + "room_directory_servers": IBaseSetting; + "integrationProvisioning": IBaseSetting; + "allowedWidgets": IBaseSetting<{ [eventId: string]: boolean }>; + "analyticsOptIn": IBaseSetting; + "pseudonymousAnalyticsOptIn": IBaseSetting; + "deviceClientInformationOptIn": IBaseSetting; + "FTUE.useCaseSelection": IBaseSetting; + "Registration.mobileRegistrationHelper": IBaseSetting; + "autocompleteDelay": IBaseSetting; + "readMarkerInViewThresholdMs": IBaseSetting; + "readMarkerOutOfViewThresholdMs": IBaseSetting; + "blacklistUnverifiedDevices": IBaseSetting; + "urlPreviewsEnabled": IBaseSetting; + "urlPreviewsEnabled_e2ee": IBaseSetting; + "notificationsEnabled": IBaseSetting; + "deviceNotificationsEnabled": IBaseSetting; + "notificationSound": IBaseSetting< + | { + name: string; + type: string; + size: number; + url: string; + } + | false + >; + "notificationBodyEnabled": IBaseSetting; + "audioNotificationsEnabled": IBaseSetting; + "enableWidgetScreenshots": IBaseSetting; + "promptBeforeInviteUnknownUsers": IBaseSetting; + "widgetOpenIDPermissions": IBaseSetting<{ + allow?: string[]; + deny?: string[]; + }>; + "breadcrumbs": IBaseSetting; + "FTUE.userOnboardingButton": IBaseSetting; + "showHiddenEventsInTimeline": IBaseSetting; + "lowBandwidth": IBaseSetting; + "fallbackICEServerAllowed": IBaseSetting; + "showImages": IBaseSetting; + "RightPanel.phasesGlobal": IBaseSetting; + "RightPanel.phases": IBaseSetting; + "enableEventIndexing": IBaseSetting; + "crawlerSleepTime": IBaseSetting; + "showCallButtonsInComposer": IBaseSetting; + "ircDisplayNameWidth": IBaseSetting; + "layout": IBaseSetting; + "Images.size": IBaseSetting; + "showChatEffects": IBaseSetting; + "Performance.addSendMessageTimingMetadata": IBaseSetting; + "Widgets.pinned": IBaseSetting<{ [widgetId: string]: boolean }>; + "Widgets.layout": IBaseSetting; + "Spaces.allRoomsInHome": IBaseSetting; + "Spaces.enabledMetaSpaces": IBaseSetting>>; + "Spaces.showPeopleInSpace": IBaseSetting; + "developerMode": IBaseSetting; + "automaticErrorReporting": IBaseSetting; + "automaticDecryptionErrorReporting": IBaseSetting; + "automaticKeyBackNotEnabledReporting": IBaseSetting; + "debug_scroll_panel": IBaseSetting; + "debug_timeline_panel": IBaseSetting; + "debug_registration": IBaseSetting; + "debug_animation": IBaseSetting; + "debug_legacy_call_handler": IBaseSetting; + "audioInputMuted": IBaseSetting; + "videoInputMuted": IBaseSetting; + "activeCallRoomIds": IBaseSetting; + "releaseAnnouncementData": IBaseSetting; + "Electron.autoLaunch": IBaseSetting; + "Electron.warnBeforeExit": IBaseSetting; + "Electron.alwaysShowMenuBar": IBaseSetting; + "Electron.showTrayIcon": IBaseSetting; + "Electron.enableHardwareAcceleration": IBaseSetting; +} + +export type SettingKey = keyof Settings; +export type FeatureSettingKey = Assignable; +export type BooleanSettingKey = Assignable> | 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, diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index 98ae347a0ab..6e857c2618a 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -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 = {}; const invertedDefaultSettings: Record = {}; -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 (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(settingName: string, roomId: string | null = null, excludeDefault = false): T { + public static getValue( + settingName: S, + roomId: string | null, + excludeDefault: true, + ): Settings[S]["default"] | undefined; + public static getValue( + settingName: S, + roomId?: string | null, + excludeDefault?: false, + ): Settings[S]["default"]; + public static getValue( + 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(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( + public static getValueAt( 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> = {}; diff --git a/src/settings/controllers/IncompatibleController.ts b/src/settings/controllers/IncompatibleController.ts index 3dcce19c895..47d6e83c6d0 100644 --- a/src/settings/controllers/IncompatibleController.ts +++ b/src/settings/controllers/IncompatibleController.ts @@ -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, ) { diff --git a/src/settings/controllers/ServerSupportUnstableFeatureController.ts b/src/settings/controllers/ServerSupportUnstableFeatureController.ts index f2ca2c0b07c..f535969108f 100644 --- a/src/settings/controllers/ServerSupportUnstableFeatureController.ts +++ b/src/settings/controllers/ServerSupportUnstableFeatureController.ts @@ -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, diff --git a/src/settings/controllers/UIFeatureController.ts b/src/settings/controllers/UIFeatureController.ts index e6548909dea..74e6693e02b 100644 --- a/src/settings/controllers/UIFeatureController.ts +++ b/src/settings/controllers/UIFeatureController.ts @@ -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(); diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 64a6a27f581..3ee5320f544 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -57,7 +57,7 @@ export class FontWatcher implements IWatcher { * @private */ private async migrateBaseFontV1toFontSizeDelta(): Promise { - const legacyBaseFontSize = SettingsStore.getValue("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 { - const legacyBaseFontV2Size = SettingsStore.getValue("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("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("fontSizeDelta")); + this.setRootFontSize(SettingsStore.getValue("fontSizeDelta")); this.setSystemFont({ useBundledEmojiFont: SettingsStore.getValue("useBundledEmojiFont"), useSystemFont: SettingsStore.getValue("useSystemFont"), diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts index 7948bfb9f92..aaeff252931 100644 --- a/src/shouldHideEvent.ts +++ b/src/shouldHideEvent.ts @@ -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 diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 9859f240154..661dda7760e 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -127,7 +127,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { }; private async updateRooms(): Promise { - let roomIds = SettingsStore.getValue("breadcrumb_rooms"); + let roomIds = SettingsStore.getValue("breadcrumb_rooms"); if (!roomIds || roomIds.length === 0) roomIds = []; const rooms = filterBoolean(roomIds.map((r) => this.matrixClient?.getRoom(r))); diff --git a/src/stores/CallStore.ts b/src/stores/CallStore.ts index 115a56aced2..c21a81b145c 100644 --- a/src/stores/CallStore.ts +++ b/src/stores/CallStore.ts @@ -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("activeCallRoomIds"); + const uncleanlyDisconnectedRoomIds = SettingsStore.getValue("activeCallRoomIds"); if (uncleanlyDisconnectedRoomIds.length) { await Promise.all([ ...uncleanlyDisconnectedRoomIds.map(async (uncleanlyDisconnectedRoomId): Promise => { diff --git a/src/stores/ReleaseAnnouncementStore.ts b/src/stores/ReleaseAnnouncementStore.ts index ba6a79fec0f..37bfb474b9a 100644 --- a/src/stores/ReleaseAnnouncementStore.ts +++ b/src/stores/ReleaseAnnouncementStore.ts @@ -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; +type StoredSettings = Partial>; + +export type ReleaseAnnouncementData = StoredSettings; /** * The events emitted by the ReleaseAnnouncementStore. @@ -82,7 +84,7 @@ export class ReleaseAnnouncementStore extends TypedEventEmitter("releaseAnnouncementData")); + return cloneDeep(SettingsStore.getValue("releaseAnnouncementData")); } /** @@ -90,7 +92,7 @@ export class ReleaseAnnouncementStore extends TypedEventEmitter(Features.ReleaseAnnouncement); + return SettingsStore.getValue(Features.ReleaseAnnouncement); } /** diff --git a/src/stores/right-panel/RightPanelStore.ts b/src/stores/right-panel/RightPanelStore.ts index 99b2d7fe507..b4510e78f56 100644 --- a/src/stores/right-panel/RightPanelStore.ts +++ b/src/stores/right-panel/RightPanelStore.ts @@ -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.", diff --git a/src/stores/right-panel/RightPanelStoreIPanelState.ts b/src/stores/right-panel/RightPanelStoreIPanelState.ts index 0d205abd2fa..f750de6791a 100644 --- a/src/stores/right-panel/RightPanelStoreIPanelState.ts +++ b/src/stores/right-panel/RightPanelStoreIPanelState.ts @@ -57,10 +57,10 @@ export interface IRightPanelForRoom { history: Array; } -interface IRightPanelForRoomStored { +export type IRightPanelForRoomStored = { isOpen: boolean; history: Array; -} +}; 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 }; diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index 90358f3310d..8bed00e561d 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -239,7 +239,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { 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 { } // 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]); diff --git a/src/stores/spaces/index.ts b/src/stores/spaces/index.ts index 848fc008d3c..184e116cf96 100644 --- a/src/stores/spaces/index.ts +++ b/src/stores/spaces/index.ts @@ -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 || diff --git a/src/stores/widgets/WidgetLayoutStore.ts b/src/stores/widgets/WidgetLayoutStore.ts index f0d3cafc83c..6e2a33687a9 100644 --- a/src/stores/widgets/WidgetLayoutStore.ts +++ b/src/stores/widgets/WidgetLayoutStore.ts @@ -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 & { 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("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 diff --git a/src/stores/widgets/WidgetPermissionStore.ts b/src/stores/widgets/WidgetPermissionStore.ts index ecae4f9c009..6ee5f034c39 100644 --- a/src/stores/widgets/WidgetPermissionStore.ts +++ b/src/stores/widgets/WidgetPermissionStore.ts @@ -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 = {}; } diff --git a/src/theme.ts b/src/theme.ts index 94ae7cb6e40..d04e91ae699 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -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; diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 164b888d027..31bd3d8e535 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -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", diff --git a/test/unit-tests/Notifier-test.ts b/test/unit-tests/Notifier-test.ts index 4c4897dbc38..8ed3bdeeb3c 100644 --- a/test/unit-tests/Notifier-test.ts +++ b/test/unit-tests/Notifier-test.ts @@ -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(); diff --git a/test/unit-tests/SlidingSyncManager-test.ts b/test/unit-tests/SlidingSyncManager-test.ts index 9929102580b..18a914fc4da 100644 --- a/test/unit-tests/SlidingSyncManager-test.ts +++ b/test/unit-tests/SlidingSyncManager-test.ts @@ -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); diff --git a/test/unit-tests/components/structures/MatrixChat-test.tsx b/test/unit-tests/components/structures/MatrixChat-test.tsx index d5db4f190a2..006b315a9c5 100644 --- a/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -1493,7 +1493,7 @@ describe("", () => { }; 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; }); diff --git a/test/unit-tests/components/structures/TimelinePanel-test.tsx b/test/unit-tests/components/structures/TimelinePanel-test.tsx index 442ed1c1d28..46ccfd1b85f 100644 --- a/test/unit-tests/components/structures/TimelinePanel-test.tsx +++ b/test/unit-tests/components/structures/TimelinePanel-test.tsx @@ -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; }); diff --git a/test/unit-tests/components/views/beta/BetaCard-test.tsx b/test/unit-tests/components/views/beta/BetaCard-test.tsx index 7d721d14877..5fe2025d786 100644 --- a/test/unit-tests/components/views/beta/BetaCard-test.tsx +++ b/test/unit-tests/components/views/beta/BetaCard-test.tsx @@ -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("", () => { describe("Feedback prompt", () => { - const featureId = "featureId"; + const featureId = "featureId" as FeatureSettingKey; beforeEach(() => { mocked(SettingsStore).getBetaInfo.mockReturnValue({ diff --git a/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx b/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx index 2b71256f0d2..5dae500a892 100644 --- a/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx +++ b/test/unit-tests/components/views/dialogs/UserSettingsDialog-test.tsx @@ -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("", () => { }); 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("", () => { 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("", () => { }); 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()); diff --git a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx index 6849612fee5..953294d1912 100644 --- a/test/unit-tests/components/views/rooms/MessageComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/MessageComposer-test.tsx @@ -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 }) => { diff --git a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx index 8b9b0d98481..eb449b6891c 100644 --- a/test/unit-tests/components/views/rooms/RoomHeader-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomHeader-test.tsx @@ -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; }); diff --git a/test/unit-tests/components/views/rooms/RoomPreviewCard-test.tsx b/test/unit-tests/components/views/rooms/RoomPreviewCard-test.tsx index 274c79002c7..4780a4652aa 100644 --- a/test/unit-tests/components/views/rooms/RoomPreviewCard-test.tsx +++ b/test/unit-tests/components/views/rooms/RoomPreviewCard-test.tsx @@ -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, ); }); diff --git a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx index c3c480eb828..40757889014 100644 --- a/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx +++ b/test/unit-tests/components/views/rooms/wysiwyg_composer/components/WysiwygComposer-test.tsx @@ -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); diff --git a/test/unit-tests/components/views/settings/LayoutSwitcher-test.tsx b/test/unit-tests/components/views/settings/LayoutSwitcher-test.tsx index 9148f7ceb1e..aafc7fe8a49 100644 --- a/test/unit-tests/components/views/settings/LayoutSwitcher-test.tsx +++ b/test/unit-tests/components/views/settings/LayoutSwitcher-test.tsx @@ -57,7 +57,7 @@ describe("", () => { act(() => screen.getByRole("radio", { name: "Message bubbles" }).click()); expect(screen.getByRole("radio", { name: "Message bubbles" })).toBeChecked(); - await waitFor(() => expect(SettingsStore.getValue("layout")).toBe(Layout.Bubble)); + await waitFor(() => expect(SettingsStore.getValue("layout")).toBe(Layout.Bubble)); }); }); @@ -77,7 +77,7 @@ describe("", () => { await renderLayoutSwitcher(); act(() => screen.getByRole("checkbox", { name: "Show compact text and messages" }).click()); - await waitFor(() => expect(SettingsStore.getValue("useCompactLayout")).toBe(true)); + await waitFor(() => expect(SettingsStore.getValue("useCompactLayout")).toBe(true)); }); it("should be disabled when the modern layout is not enabled", async () => { diff --git a/test/unit-tests/components/views/settings/discovery/DiscoverySettings-test.tsx b/test/unit-tests/components/views/settings/discovery/DiscoverySettings-test.tsx index 4cd55f6fc45..65b1e67f946 100644 --- a/test/unit-tests/components/views/settings/discovery/DiscoverySettings-test.tsx +++ b/test/unit-tests/components/views/settings/discovery/DiscoverySettings-test.tsx @@ -40,7 +40,7 @@ describe("DiscoverySettings", () => { const DiscoveryWrapper = (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; }); diff --git a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx index e1a451c9d57..75a1a7b9558 100644 --- a/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/room/RolesRoomSettingsTab-test.tsx @@ -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; }); }; diff --git a/test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx index efa072e7a1e..0f442788932 100644 --- a/test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx +++ b/test/unit-tests/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx @@ -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 = ( + 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); }; }; diff --git a/test/unit-tests/components/views/spaces/ThreadsActivityCentre-test.tsx b/test/unit-tests/components/views/spaces/ThreadsActivityCentre-test.tsx index da1fa7bb0f6..dfc9d0943ab 100644 --- a/test/unit-tests/components/views/spaces/ThreadsActivityCentre-test.tsx +++ b/test/unit-tests/components/views/spaces/ThreadsActivityCentre-test.tsx @@ -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()); diff --git a/test/unit-tests/createRoom-test.ts b/test/unit-tests/createRoom-test.ts index 0f2b61c04cd..b654cb314ba 100644 --- a/test/unit-tests/createRoom-test.ts +++ b/test/unit-tests/createRoom-test.ts @@ -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; }); diff --git a/test/unit-tests/models/Call-test.ts b/test/unit-tests/models/Call-test.ts index 316adbbea90..b675100ae99 100644 --- a/test/unit-tests/models/Call-test.ts +++ b/test/unit-tests/models/Call-test.ts @@ -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 = (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(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 = (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(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 = (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(name, roomId, excludeDefault); + return excludeDefault + ? originalGetValue(name, roomId, excludeDefault) + : originalGetValue(name, roomId, excludeDefault); } }; await ElementCall.create(room); diff --git a/test/unit-tests/settings/SettingsStore-test.ts b/test/unit-tests/settings/SettingsStore-test.ts index de0012c276b..044b5ec764e 100644 --- a/test/unit-tests/settings/SettingsStore-test.ts +++ b/test/unit-tests/settings/SettingsStore-test.ts @@ -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, }, diff --git a/test/unit-tests/settings/controllers/IncompatibleController-test.ts b/test/unit-tests/settings/controllers/IncompatibleController-test.ts index 3012f4d0e71..61a0be49cd2 100644 --- a/test/unit-tests/settings/controllers/IncompatibleController-test.ts +++ b/test/unit-tests/settings/controllers/IncompatibleController-test.ts @@ -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); diff --git a/test/unit-tests/settings/controllers/ServerSupportUnstableFeatureController-test.ts b/test/unit-tests/settings/controllers/ServerSupportUnstableFeatureController-test.ts index 310cb535d02..ebf4fbae158 100644 --- a/test/unit-tests/settings/controllers/ServerSupportUnstableFeatureController-test.ts +++ b/test/unit-tests/settings/controllers/ServerSupportUnstableFeatureController-test.ts @@ -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, diff --git a/test/unit-tests/settings/controllers/SystemFontController-test.ts b/test/unit-tests/settings/controllers/SystemFontController-test.ts index fbbe411ca0b..24a0f455638 100644 --- a/test/unit-tests/settings/controllers/SystemFontController-test.ts +++ b/test/unit-tests/settings/controllers/SystemFontController-test.ts @@ -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"; diff --git a/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx b/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx index 206dbcbcbc8..2af530361ed 100644 --- a/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx +++ b/test/unit-tests/settings/watchers/ThemeWatcher-test.tsx @@ -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(settingName: string, _roomId: string | null = null, _excludeDefault = false): T { +function makeGetValue(values: any): any { + return function getValue( + settingName: S, + _roomId: string | null = null, + _excludeDefault = false, + ): Settings[S] { return values[settingName]; }; } diff --git a/test/unit-tests/submit-rageshake-test.ts b/test/unit-tests/submit-rageshake-test.ts index 2f1b150349d..c21e27f88ed 100644 --- a/test/unit-tests/submit-rageshake-test.ts +++ b/test/unit-tests/submit-rageshake-test.ts @@ -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); diff --git a/test/unit-tests/theme-test.ts b/test/unit-tests/theme-test.ts index 8d18281dbfd..d2895cbd89f 100644 --- a/test/unit-tests/theme-test.ts +++ b/test/unit-tests/theme-test.ts @@ -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", diff --git a/test/unit-tests/utils/exportUtils/PlainTextExport-test.ts b/test/unit-tests/utils/exportUtils/PlainTextExport-test.ts index 7c90d03f4de..563c060dff2 100644 --- a/test/unit-tests/utils/exportUtils/PlainTextExport-test.ts +++ b/test/unit-tests/utils/exportUtils/PlainTextExport-test.ts @@ -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[] = [