Harden Settings using mapped types (#28775)

* Harden Settings using mapped types

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

* Fix issues found during hardening

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

* Iterate

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

* Iterate

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

* Iterate

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

* Remove oidc native flow stale key

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

---------

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

View File

@ -19,6 +19,10 @@ ignore.push("/OpenSpotlightPayload.ts");
ignore.push("/PinnedMessageBadge.tsx"); ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts"); ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.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 // We ignore js-sdk by default as it may export for other non element-web projects
if (!includeJSSDK) ignore.push("matrix-js-sdk"); if (!includeJSSDK) ignore.push("matrix-js-sdk");

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -252,10 +252,13 @@ export default class RightPanelStore extends ReadyWatchingStore {
const room = this.mxClient?.getRoom(this.viewedRoomId); const room = this.mxClient?.getRoom(this.viewedRoomId);
if (!!room) { if (!!room) {
this.global = 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] =
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 { } else {
logger.warn( logger.warn(
"Could not restore the right panel after load because there was no associated room object.", "Could not restore the right panel after load because there was no associated room object.",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -263,9 +263,9 @@ export function getCustomTheme(themeName: string): CustomTheme {
if (!customThemes) { if (!customThemes) {
throw new Error(`No custom themes set, can't set custom theme "${themeName}"`); 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) { 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}`); throw new Error(`Can't find custom theme "${themeName}", only know ${knownNames}`);
} }
return customTheme; return customTheme;

View File

@ -20,6 +20,7 @@ import { IndicatorIcon } from "@vector-im/compound-web";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { NotificationLevel } from "../stores/notifications/NotificationLevel"; import { NotificationLevel } from "../stores/notifications/NotificationLevel";
import { doesRoomHaveUnreadMessages } from "../Unread"; import { doesRoomHaveUnreadMessages } from "../Unread";
import { SettingKey } from "../settings/Settings.tsx";
// MSC2867 is not yet spec at time of writing. We read from both stable // 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, // 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 MARKED_UNREAD_TYPE_STABLE = "m.marked_unread";
export const deviceNotificationSettingsKeys = [ export const deviceNotificationSettingsKeys: SettingKey[] = [
"notificationsEnabled", "notificationsEnabled",
"notificationBodyEnabled", "notificationBodyEnabled",
"audioNotificationsEnabled", "audioNotificationsEnabled",

View File

@ -343,7 +343,7 @@ describe("Notifier", () => {
describe("getSoundForRoom", () => { describe("getSoundForRoom", () => {
it("should not explode if given invalid url", () => { 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" } }; return { url: { content_uri: "foobar" } };
}); });
expect(Notifier.getSoundForRoom("!roomId:server")).toBeNull(); expect(Notifier.getSoundForRoom("!roomId:server")).toBeNull();

View File

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

View File

@ -1493,7 +1493,7 @@ describe("<MatrixChat />", () => {
}; };
const enabledMobileRegistration = (): void => { 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 === "Registration.mobileRegistrationHelper") return true;
if (settingName === UIFeature.Registration) return true; if (settingName === UIFeature.Registration) return true;
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -176,7 +176,7 @@ describe("RoomHeader", () => {
}); });
it("opens the notifications panel", async () => { 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; if (name === "feature_notifications") return true;
}); });

View File

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

View File

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

View File

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

View File

@ -40,7 +40,7 @@ describe("DiscoverySettings", () => {
const DiscoveryWrapper = (props = {}) => <MatrixClientContext.Provider value={client} {...props} />; const DiscoveryWrapper = (props = {}) => <MatrixClientContext.Provider value={client} {...props} />;
it("is empty if 3pid features are disabled", async () => { 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; if (key === UIFeature.ThirdPartyID) return false;
}); });

View File

@ -69,7 +69,7 @@ describe("RolesRoomSettingsTab", () => {
describe("Element Call", () => { describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => { 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; if (name === "feature_group_calls") return val;
}); });
}; };

View File

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

View File

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

View File

@ -105,7 +105,7 @@ describe("createRoom", () => {
}); });
it("correctly sets up MSC3401 power levels", async () => { 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; if (name === "feature_group_calls") return true;
}); });

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,7 @@ describe("SystemFontController", () => {
it("dispatches a system font update action on change", () => { it("dispatches a system font update action on change", () => {
const controller = new SystemFontController(); 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 === "useBundledEmojiFont") return false;
if (settingName === "useSystemFont") return true; if (settingName === "useSystemFont") return true;
if (settingName === "systemFont") return "Comic Sans MS"; if (settingName === "systemFont") return "Comic Sans MS";

View File

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

View File

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

View File

@ -149,7 +149,7 @@ describe("theme", () => {
}); });
it("should be robust to malformed custom_themes values", () => { 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({ expect(enumerateThemes()).toEqual({
"light": "Light", "light": "Light",
"light-high-contrast": "Light high contrast", "light-high-contrast": "Light high contrast",

View File

@ -44,7 +44,7 @@ describe("PlainTextExport", () => {
[24, false, "Fri, Apr 16, 2021, 17:20:00 - @alice:example.com: Hello, world!\n"], [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"], [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) => { ])("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, settingName === "showTwelveHourTimestamps" ? setting : undefined,
); );
const events: MatrixEvent[] = [ const events: MatrixEvent[] = [