Harden settings types (#33311)

* Harden settings types

* Fix types

* Update apps/web/src/emojipicker/recent.ts

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>

* Fix suggestion

---------

Co-authored-by: Will Hunt <2072976+Half-Shot@users.noreply.github.com>
This commit is contained in:
Michael Telatynski
2026-04-29 08:46:09 +00:00
committed by GitHub
co-authored by Will Hunt
parent 4bee845010
commit 5ff302539e
26 changed files with 104 additions and 97 deletions
+2 -2
View File
@@ -25,9 +25,9 @@ export interface MediaPreviewConfig extends Record<string, unknown> {
/** /**
* Media preview setting for thumbnails of media in rooms. * Media preview setting for thumbnails of media in rooms.
*/ */
media_previews: MediaPreviewValue; media_previews?: MediaPreviewValue;
/** /**
* Media preview settings for avatars of rooms we have been invited to. * Media preview settings for avatars of rooms we have been invited to.
*/ */
invite_avatars: MediaPreviewValue.On | MediaPreviewValue.Off; invite_avatars?: MediaPreviewValue.On | MediaPreviewValue.Off;
} }
+1 -1
View File
@@ -725,7 +725,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
); );
finished.then(([allow]) => { finished.then(([allow]) => {
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow ?? null);
}); });
} }
+11 -6
View File
@@ -142,6 +142,16 @@ interface EmittedEvents {
[NotifierEvent.NotificationHiddenChange]: (hidden: boolean) => void; [NotifierEvent.NotificationHiddenChange]: (hidden: boolean) => void;
} }
/**
* Type representing a notification sound setting
*/
export type NotificationSound = {
url: string;
name?: string;
type?: string;
size?: number;
};
class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> { class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents> {
private notifsByRoom: Record<string, Notification[]> = {}; private notifsByRoom: Record<string, Notification[]> = {};
@@ -223,12 +233,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
} }
} }
public getSoundForRoom(roomId: string): { public getSoundForRoom(roomId: string): NotificationSound | null {
url: string;
name: string;
type: string;
size: number;
} | 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.
const content = SettingsStore.getValue("notificationSound", roomId); const content = SettingsStore.getValue("notificationSound", roomId);
+3 -3
View File
@@ -19,7 +19,7 @@ import SettingsStore from "./settings/SettingsStore";
import { type ScreenName } from "./PosthogTrackers"; import { type ScreenName } from "./PosthogTrackers";
import { type ActionPayload } from "./dispatcher/payloads"; import { type ActionPayload } from "./dispatcher/payloads";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import { type SettingUpdatedPayload } from "./dispatcher/payloads/SettingUpdatedPayload"; import { isSettingUpdatedPayload, type SettingUpdatedPayload } from "./dispatcher/payloads/SettingUpdatedPayload";
import dis from "./dispatcher/dispatcher"; import dis from "./dispatcher/dispatcher";
import { Layout } from "./settings/enums/Layout"; import { Layout } from "./settings/enums/Layout";
@@ -199,8 +199,8 @@ export class PosthogAnalytics {
const settingsPayload = payload as SettingUpdatedPayload; const settingsPayload = payload as SettingUpdatedPayload;
if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) { if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
this.onLayoutUpdated(); this.onLayoutUpdated();
} else if (settingsPayload.settingName === "urlPreviewsEnabled" && !settingsPayload.roomId) { } else if (isSettingUpdatedPayload(settingsPayload, "urlPreviewsEnabled") && !settingsPayload.roomId) {
this.onUrlPreviewSettingUpdated(settingsPayload.newValue as boolean); this.onUrlPreviewSettingUpdated(settingsPayload.newValue);
} }
}; };
@@ -122,7 +122,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => { private onCrawlerSleepTimeChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) }); this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) });
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.valueAsNumber);
}; };
public render(): React.ReactNode { public render(): React.ReactNode {
@@ -1794,11 +1794,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
SettingsStore.watchSetting( SettingsStore.watchSetting(
"blacklistUnverifiedDevices", "blacklistUnverifiedDevices",
null, null,
(_settingName, _roomId, atLevel, blacklistEnabled: boolean) => { (_settingName, _roomId, atLevel, blacklistEnabled) => {
if (atLevel != SettingLevel.DEVICE) { if (atLevel != SettingLevel.DEVICE) {
return; return;
} }
crypto.globalBlacklistUnverifiedDevices = blacklistEnabled; crypto.globalBlacklistUnverifiedDevices = !!blacklistEnabled;
}, },
); );
} }
@@ -38,8 +38,8 @@ function LayoutSelector(): JSX.Element {
<Root <Root
className="mx_LayoutSwitcher_LayoutSelector" className="mx_LayoutSwitcher_LayoutSelector"
onChange={async (evt) => { onChange={async (evt) => {
// We don't have any file in the form, we can cast it as string safely // We don't have any file in the form, we can cast it as Layout safely
const newLayout = new FormData(evt.currentTarget).get("layout") as string | null; const newLayout = new FormData(evt.currentTarget).get("layout") as Layout;
await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, newLayout); await SettingsStore.setValue("layout", null, SettingLevel.DEVICE, newLayout);
}} }}
> >
@@ -211,17 +211,17 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => { private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ autocompleteDelay: e.target.value }); this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.valueAsNumber);
}; };
private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => { private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ readMarkerInViewThresholdMs: e.target.value }); this.setState({ readMarkerInViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.valueAsNumber);
}; };
private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => { private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent<HTMLInputElement>): void => {
this.setState({ readMarkerOutOfViewThresholdMs: e.target.value }); this.setState({ readMarkerOutOfViewThresholdMs: e.target.value });
SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.valueAsNumber);
}; };
private renderGroup(settingIds: BooleanSettingKey[], level = SettingLevel.ACCOUNT): JSX.Element { private renderGroup(settingIds: BooleanSettingKey[], level = SettingLevel.ACCOUNT): JSX.Element {
@@ -340,7 +340,7 @@ export class DeviceListener {
}); });
} }
private onRecordClientInformationSettingChange: CallbackFn = ( private onRecordClientInformationSettingChange: CallbackFn<"deviceClientInformationOptIn"> = (
_originalSettingName, _originalSettingName,
_roomId, _roomId,
_level, _level,
@@ -9,14 +9,26 @@ Please see LICENSE files in the repository root for full details.
import { type ActionPayload } from "../payloads"; import { type ActionPayload } from "../payloads";
import { type Action } from "../actions"; import { type Action } from "../actions";
import { type SettingLevel } from "../../settings/SettingLevel"; import { type SettingLevel } from "../../settings/SettingLevel";
import { type SettingValueType } from "../../settings/Settings"; import { type SettingKey, type Settings } from "../../settings/Settings";
export interface SettingUpdatedPayload extends ActionPayload { export interface SettingUpdatedPayload<S extends SettingKey = SettingKey> extends ActionPayload {
action: Action.SettingUpdated; action: Action.SettingUpdated;
settingName: string; settingName: S;
roomId: string | null; roomId: string | null;
level: SettingLevel; level: SettingLevel;
newValueAtLevel: SettingLevel; newValueAtLevel: Settings[S]["default"];
newValue: SettingValueType; newValue: Settings[S]["default"];
}
/**
* Type guard to check if a payload is a SettingUpdatedPayload for a specific setting.
* @param payload the payload to assert
* @param settingName the setting name to check for
*/
export function isSettingUpdatedPayload<S extends SettingKey>(
payload: SettingUpdatedPayload<any>,
settingName: S,
): payload is SettingUpdatedPayload<S> {
return payload.settingName === settingName;
} }
+2 -2
View File
@@ -29,7 +29,7 @@ const STORAGE_LIMIT = 100;
function migrate(): void { function migrate(): void {
const data: ILegacyFormat = JSON.parse(window.localStorage.mx_reaction_count || "{}"); const data: ILegacyFormat = JSON.parse(window.localStorage.mx_reaction_count || "{}");
const sorted = Object.entries(data).sort(([, [count1, date1]], [, [count2, date2]]) => date2 - date1); const sorted = Object.entries(data).sort(([, [count1, date1]], [, [count2, date2]]) => date2 - date1);
const newFormat = sorted.map(([emoji, [count, date]]) => [emoji, count]); const newFormat = sorted.map<RecentEmojiData[number]>(([emoji, [count, date]]) => [emoji, count]);
SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, newFormat.slice(0, STORAGE_LIMIT)); SettingsStore.setValue(SETTING_NAME, null, SettingLevel.ACCOUNT, newFormat.slice(0, STORAGE_LIMIT));
} }
@@ -41,7 +41,7 @@ export function add(emoji: string): void {
const recents = getRecentEmoji(); const recents = getRecentEmoji();
const i = recents.findIndex(([e]) => e === emoji); const i = recents.findIndex(([e]) => e === emoji);
let newEntry; let newEntry: RecentEmojiData[number];
if (i >= 0) { if (i >= 0) {
// first remove the existing tuple so that we can increment it and push it to the front // first remove the existing tuple so that we can increment it and push it to the front
[newEntry] = recents.splice(i, 1); [newEntry] = recents.splice(i, 1);
+5 -10
View File
@@ -11,7 +11,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../MatrixClientPeg"; import { MatrixClientPeg } from "../MatrixClientPeg";
import { ALL_RULE_TYPES, BanList } from "./BanList"; import { ALL_RULE_TYPES, BanList } from "./BanList";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore, { type CallbackFn } from "../settings/SettingsStore";
import { _t } from "../languageHandler"; import { _t } from "../languageHandler";
import dis from "../dispatcher/dispatcher"; import dis from "../dispatcher/dispatcher";
import { SettingLevel } from "../settings/SettingLevel"; import { SettingLevel } from "../settings/SettingLevel";
@@ -38,7 +38,7 @@ export class Mjolnir {
} }
public start(): void { public start(): void {
this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged.bind(this)); this.mjolnirWatchRef = SettingsStore.watchSetting("mjolnirRooms", null, this.onListsChanged);
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
dis.dispatch<DoAfterSyncPreparedPayload<ActionPayload>>({ dis.dispatch<DoAfterSyncPreparedPayload<ActionPayload>>({
@@ -130,15 +130,10 @@ export class Mjolnir {
this.updateLists(this._roomIds); this.updateLists(this._roomIds);
}; };
private onListsChanged( private onListsChanged: CallbackFn<"mjolnirRooms"> = (settingName, roomId, atLevel, newValue): void => {
settingName: string,
roomId: string | null,
atLevel: SettingLevel,
newValue: string[],
): void {
// We know that ban lists are only recorded at one level so we don't need to re-eval them // We know that ban lists are only recorded at one level so we don't need to re-eval them
this.updateLists(newValue); this.updateLists(newValue ?? []);
} };
private updateLists(listRoomIds: string[]): void { private updateLists(listRoomIds: string[]): void {
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
+2 -9
View File
@@ -54,6 +54,7 @@ import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
import BlockInvitesConfigController from "./controllers/BlockInvitesConfigController.ts"; import BlockInvitesConfigController from "./controllers/BlockInvitesConfigController.ts";
import RequiresSettingsController from "./controllers/RequiresSettingsController.ts"; import RequiresSettingsController from "./controllers/RequiresSettingsController.ts";
import { type OrderedCustomSections, type CustomSectionsData } from "../stores/room-list-v3/section.ts"; import { type OrderedCustomSections, type CustomSectionsData } from "../stores/room-list-v3/section.ts";
import { type NotificationSound } from "../Notifier.ts";
export const defaultWatchManager = new WatchManager(); export const defaultWatchManager = new WatchManager();
@@ -311,15 +312,7 @@ export interface Settings {
"urlPreviewsEnabled_e2ee": IBaseSetting<boolean>; "urlPreviewsEnabled_e2ee": IBaseSetting<boolean>;
"notificationsEnabled": IBaseSetting<boolean>; "notificationsEnabled": IBaseSetting<boolean>;
"deviceNotificationsEnabled": IBaseSetting<boolean>; "deviceNotificationsEnabled": IBaseSetting<boolean>;
"notificationSound": IBaseSetting< "notificationSound": IBaseSetting<NotificationSound | false>;
| {
name: string;
type: string;
size: number;
url: string;
}
| false
>;
"notificationBodyEnabled": IBaseSetting<boolean>; "notificationBodyEnabled": IBaseSetting<boolean>;
"audioNotificationsEnabled": IBaseSetting<boolean>; "audioNotificationsEnabled": IBaseSetting<boolean>;
"enableWidgetScreenshots": IBaseSetting<boolean>; "enableWidgetScreenshots": IBaseSetting<boolean>;
+32 -28
View File
@@ -79,7 +79,7 @@ export const LEVEL_ORDER = [
SettingLevel.DEFAULT, SettingLevel.DEFAULT,
]; ];
function getLevelOrder(setting: ISetting): SettingLevel[] { function getLevelOrder(setting: Settings[keyof Settings]): SettingLevel[] {
// Settings which support only a single setting level are inherently ordered // Settings which support only a single setting level are inherently ordered
if (setting.supportedLevelsAreOrdered || setting.supportedLevels.length === 1) { if (setting.supportedLevelsAreOrdered || setting.supportedLevels.length === 1) {
// return a copy to prevent callers from modifying the array // return a copy to prevent callers from modifying the array
@@ -88,12 +88,12 @@ function getLevelOrder(setting: ISetting): SettingLevel[] {
return LEVEL_ORDER; return LEVEL_ORDER;
} }
export type CallbackFn = ( export type CallbackFn<S extends SettingKey> = (
settingName: SettingKey, settingName: S,
roomId: string | null, roomId: string | null,
atLevel: SettingLevel, atLevel: SettingLevel,
newValAtLevel: any, newValAtLevel: Settings[S]["default"] | null,
newVal: any, newVal: Settings[S]["default"] | null,
) => void; ) => void;
type HandlerMap = Partial<{ type HandlerMap = Partial<{
@@ -167,7 +167,11 @@ 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: SettingKey, roomId: string | null, callbackFn: CallbackFn): string { public static watchSetting<S extends SettingKey>(
settingName: S,
roomId: string | null,
callbackFn: CallbackFn<S>,
): string {
const setting = SETTINGS[settingName]; const setting = SETTINGS[settingName];
if (!setting) throw new Error(`${settingName} is not a setting`); if (!setting) throw new Error(`${settingName} is not a setting`);
@@ -175,7 +179,11 @@ export default class SettingsStore {
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${finalSettingName}_${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: Settings[S]["default"],
): void => {
if (!SettingsStore.doesSettingSupportLevel(settingName, 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: ` +
@@ -220,7 +228,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: SettingKey, roomId: string | null): void { public static monitorSetting<S extends SettingKey>(settingName: S, 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());
@@ -228,7 +236,7 @@ export default class SettingsStore {
const registerWatcher = (): void => { const registerWatcher = (): void => {
this.monitors.get(settingName)!.set( this.monitors.get(settingName)!.set(
roomId, roomId,
SettingsStore.watchSetting( SettingsStore.watchSetting<S>(
settingName, settingName,
roomId, roomId,
(settingName, inRoomId, level, newValueAtLevel, newValue) => { (settingName, inRoomId, level, newValueAtLevel, newValue) => {
@@ -449,11 +457,10 @@ export default class SettingsStore {
/** /**
* Gets the default value of a setting. * Gets the default value of a setting.
* @param {string} settingName The name of the setting to read the value of. * @param settingName The name of the setting to read the value of.
* @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: SettingKey): any { public static getDefaultValue<S extends SettingKey>(settingName: S): Settings[S]["default"] {
// 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.");
@@ -462,13 +469,13 @@ export default class SettingsStore {
return SETTINGS[settingName].default; return SETTINGS[settingName].default;
} }
private static getFinalValue( private static getFinalValue<S extends SettingKey>(
setting: ISetting, setting: Settings[S],
level: SettingLevel, level: SettingLevel,
roomId: string | null, roomId: string | null,
calculatedValue: any, calculatedValue: Settings[S]["default"],
calculatedAtLevel: SettingLevel | null, calculatedAtLevel: SettingLevel | null,
): any { ): Settings[S]["default"] {
let resultingValue = calculatedValue; let resultingValue = calculatedValue;
if (setting.controller) { if (setting.controller) {
@@ -480,25 +487,22 @@ export default class SettingsStore {
return resultingValue; return resultingValue;
} }
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/** /**
* Sets the value for a setting. The room ID is optional if the setting is not being * Sets the value for a setting. The room ID is optional if the setting is not being
* set for a particular room, otherwise it should be supplied. The value may be null * set for a particular room, otherwise it should be supplied. The value may be null
* to indicate that the level should no longer have an override. * to indicate that the level should no longer have an override.
* @param {string} settingName The name of the setting to change. * @param settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be null. * @param roomId The room ID to change the value in, may be null.
* @param {SettingLevel} level The level * @param level The level
* to change the value at. * to change the value at.
* @param {*} value The new value of the setting, may be null. * @param value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed. * @return Resolves when the setting has been changed.
*/ */
public static async setValue<S extends SettingKey>(
/* eslint-enable valid-jsdoc */ settingName: S,
public static async setValue(
settingName: SettingKey,
roomId: string | null, roomId: string | null,
level: SettingLevel, level: SettingLevel,
value: any, value: Settings[S]["default"] | null,
): Promise<void> { ): Promise<void> {
// Verify that the setting is actually a setting // Verify that the setting is actually a setting
const setting = SETTINGS[settingName]; const setting = SETTINGS[settingName];
@@ -38,8 +38,8 @@ export default class MediaPreviewConfigController extends MatrixClientBackedCont
const validMediaPreviews = Object.values(MediaPreviewValue); const validMediaPreviews = Object.values(MediaPreviewValue);
const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On]; const validInviteAvatars = [MediaPreviewValue.Off, MediaPreviewValue.On];
return { return {
invite_avatars: validInviteAvatars.includes(inviteAvatars) ? inviteAvatars : undefined, invite_avatars: validInviteAvatars.includes(inviteAvatars!) ? inviteAvatars : undefined,
media_previews: validMediaPreviews.includes(mediaPreviews) ? mediaPreviews : undefined, media_previews: validMediaPreviews.includes(mediaPreviews!) ? mediaPreviews : undefined,
}; };
} }
@@ -60,8 +60,8 @@ export type IRightPanelForRoomStored = {
history: Array<IRightPanelCardStored>; history: Array<IRightPanelCardStored>;
}; };
export function convertToStorePanel(cacheRoom?: IRightPanelForRoom): IRightPanelForRoomStored | undefined { export function convertToStorePanel(cacheRoom?: IRightPanelForRoom): IRightPanelForRoomStored | null {
if (!cacheRoom) return undefined; if (!cacheRoom) return null;
const storeHistory = [...cacheRoom.history].map((panelState) => convertCardToStore(panelState)); const storeHistory = [...cacheRoom.history].map((panelState) => convertCardToStore(panelState));
return { isOpen: cacheRoom.isOpen, history: storeHistory }; return { isOpen: cacheRoom.isOpen, history: storeHistory };
} }
@@ -69,7 +69,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<EmptyObject> implem
"feature_dynamic_room_predecessors", "feature_dynamic_room_predecessors",
null, null,
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
this.msc3946ProcessDynamicPredecessor = newVal; this.msc3946ProcessDynamicPredecessor = !!newVal;
this.regenerateAllLists({ trigger: true }); this.regenerateAllLists({ trigger: true });
}, },
); );
@@ -231,7 +231,7 @@ export class EventContentBodyViewModel
"TextualBody.enableBigEmoji", "TextualBody.enableBigEmoji",
null, null,
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
this.setEnableBigEmoji(newVal); this.setEnableBigEmoji(!!newVal);
}, },
); );
this.disposables.track(() => SettingsStore.unwatchSetting(enableBigEmojiWatcherRef)); this.disposables.track(() => SettingsStore.unwatchSetting(enableBigEmojiWatcherRef));
@@ -240,7 +240,7 @@ export class EventContentBodyViewModel
"Pill.shouldShowPillAvatar", "Pill.shouldShowPillAvatar",
null, null,
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
this.setShouldShowPillAvatar(newVal); this.setShouldShowPillAvatar(!!newVal);
}, },
); );
this.disposables.track(() => SettingsStore.unwatchSetting(shouldShowPillAvatarWatcherRef)); this.disposables.track(() => SettingsStore.unwatchSetting(shouldShowPillAvatarWatcherRef));
@@ -75,7 +75,7 @@ export class RedactedBodyViewModel
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
if (this.showTwelveHour === newVal) return; if (this.showTwelveHour === newVal) return;
this.showTwelveHour = newVal; this.showTwelveHour = !!newVal;
this.updateTooltip(); this.updateTooltip();
}, },
); );
@@ -296,7 +296,7 @@ export class UrlPreviewGroupViewModel
null, null,
(_setting, _roomid, _level, compactLayout) => { (_setting, _roomid, _level, compactLayout) => {
this.snapshot.merge({ this.snapshot.merge({
compactLayout, compactLayout: !!compactLayout,
}); });
}, },
); );
@@ -79,7 +79,7 @@ export class DateSeparatorViewModel
"feature_jump_to_date", "feature_jump_to_date",
null, null,
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
this.jumpToDateEnabled = newVal; this.jumpToDateEnabled = !!newVal;
this.updateSnapshot(); this.updateSnapshot();
}, },
); );
@@ -89,7 +89,7 @@ export class DateSeparatorViewModel
UIFeature.TimelineEnableRelativeDates, UIFeature.TimelineEnableRelativeDates,
null, null,
(_settingName, _roomId, _level, _newValAtLevel, newVal) => { (_settingName, _roomId, _level, _newValAtLevel, newVal) => {
this.relativeDatesEnabled = newVal; this.relativeDatesEnabled = !!newVal;
this.updateSnapshot(); this.updateSnapshot();
}, },
); );
@@ -557,7 +557,7 @@ describe("<MatrixChat />", () => {
}); });
it("should not persist device language when not available", async () => { it("should not persist device language when not available", async () => {
await SettingsStore.setValue("language", null, SettingLevel.DEVICE, undefined); await SettingsStore.setValue("language", null, SettingLevel.DEVICE, null);
const languageBefore = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true); const languageBefore = SettingsStore.getValueAt(SettingLevel.DEVICE, "language", null, true, true);
jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin");
@@ -225,7 +225,7 @@ describe("<UserSettingsDialog />", () => {
}); });
it("watches settings", async () => { it("watches settings", async () => {
const watchSettingCallbacks: Record<string, CallbackFn> = {}; const watchSettingCallbacks: Record<string, CallbackFn<any>> = {};
mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => { mockSettingsStore.watchSetting.mockImplementation((settingName, roomId, callback) => {
watchSettingCallbacks[settingName] = callback; watchSettingCallbacks[settingName] = callback;
@@ -28,7 +28,7 @@ describe("ReleaseAnnouncementStore", () => {
settings = { settings = {
releaseAnnouncementData: {}, releaseAnnouncementData: {},
}; };
const watchCallbacks: Array<CallbackFn> = []; const watchCallbacks: Array<CallbackFn<any>> = [];
mocked(SettingsStore.getValue).mockImplementation((setting: string) => { mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
return settings[setting]; return settings[setting];
@@ -203,13 +203,11 @@ describe("RoomListStore", () => {
let featureFlagValue = false; let featureFlagValue = false;
jest.spyOn(SettingsStore, "getValue").mockImplementation(() => featureFlagValue); jest.spyOn(SettingsStore, "getValue").mockImplementation(() => featureFlagValue);
let watchCallback: CallbackFn | undefined; let watchCallback: CallbackFn<any> | undefined;
jest.spyOn(SettingsStore, "watchSetting").mockImplementation( jest.spyOn(SettingsStore, "watchSetting").mockImplementation((_settingName, _roomId, callbackFn) => {
(_settingName: string, _roomId: string | null, callbackFn: CallbackFn) => { watchCallback = callbackFn;
watchCallback = callbackFn; return "dyn_pred_ref";
return "dyn_pred_ref"; });
},
);
jest.spyOn(SettingsStore, "unwatchSetting"); jest.spyOn(SettingsStore, "unwatchSetting");
const { store } = createStore(); const { store } = createStore();
@@ -664,7 +664,7 @@ describe("RoomListItemViewModel", () => {
}); });
it("should update sections when OrderedCustomSections setting changes", () => { it("should update sections when OrderedCustomSections setting changes", () => {
let watchCallback: CallbackFn = () => {}; let watchCallback: CallbackFn<"RoomList.OrderedCustomSections"> = () => {};
jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => { jest.spyOn(SettingsStore, "watchSetting").mockImplementation((setting, _room, callback) => {
if (setting === "RoomList.OrderedCustomSections") watchCallback = callback; if (setting === "RoomList.OrderedCustomSections") watchCallback = callback;
return "watcher-id"; return "watcher-id";