Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harden Settings using mapped types #28775

Merged
merged 7 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/analyse_unused_exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ ignore.push("/OpenSpotlightPayload.ts");
ignore.push("/PinnedMessageBadge.tsx");
ignore.push("/editor/mock.ts");
ignore.push("DeviceIsolationModeController.ts");
ignore.push("/json.ts");
ignore.push("/ReleaseAnnouncementStore.ts");
ignore.push("/WidgetLayoutStore.ts");
ignore.push("/common.ts");

// We ignore js-sdk by default as it may export for other non element-web projects
if (!includeJSSDK) ignore.push("matrix-js-sdk");
Expand Down
8 changes: 8 additions & 0 deletions src/@types/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ type DeepReadonlyObject<T> = {
};

export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];

/**
* Returns a union type of the keys of the input Object type whose values are assignable to the given Item type.
* Based on https://stackoverflow.com/a/57862073
*/
export type Assignable<Object, Item> = {
[Key in keyof Object]: Object[Key] extends Item ? Key : never;
}[keyof Object];
13 changes: 13 additions & 0 deletions src/@types/json.ts
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/Notifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
url: string;
name: string;
type: string;
size: string;
size: number;
} | null {
// We do no caching here because the SDK caches setting
// and the browser will cache the sound.
Expand Down
2 changes: 1 addition & 1 deletion src/components/structures/LoggedInView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class LoggedInView extends React.Component<IProps, IState> {
} else {
backgroundImage = OwnProfileStore.instance.getHttpAvatarUrl();
}
this.setState({ backgroundImage });
this.setState({ backgroundImage: backgroundImage ?? undefined });
};

public canResetTimelineInRoom = (roomId: string): boolean => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/views/beta/BetaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import SettingsFlag from "../elements/SettingsFlag";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import InlineSpinner from "../elements/InlineSpinner";
import { shouldShowFeedback } from "../../../utils/Feedback";
import { FeatureSettingKey } from "../../../settings/Settings.tsx";

// XXX: Keep this around for re-use in future Betas

interface IProps {
title?: string;
featureId: string;
featureId: FeatureSettingKey;
}

interface IBetaPillProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export const RoomGeneralContextMenu: React.FC<RoomGeneralContextMenuProps> = ({
}
})();

const developerModeEnabled = useSettingValue<boolean>("developerMode");
const developerModeEnabled = useSettingValue("developerMode");
const developerToolsOption = developerModeEnabled ? (
<DeveloperToolsOption onFinished={onFinished} roomId={room.roomId} />
) : null;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/context_menus/WidgetContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const showDeleteButton = (canModify: boolean, onDeleteClick: undefined | (() =>

const showSnapshotButton = (widgetMessaging: ClientWidgetApi | undefined): boolean => {
return (
SettingsStore.getValue<boolean>("enableWidgetScreenshots") &&
SettingsStore.getValue("enableWidgetScreenshots") &&
!!widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
onFinished,
}) => {
const cli = useContext(MatrixClientContext);
const msc3946ProcessDynamicPredecessor = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const msc3946ProcessDynamicPredecessor = useSettingValue("feature_dynamic_room_predecessors");
const visibleRooms = useMemo(
() =>
cli
Expand Down
5 changes: 3 additions & 2 deletions src/components/views/dialogs/BetaFeedbackDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "./UserTab";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
import { SettingKey } from "../../../settings/Settings.tsx";

// XXX: Keep this around for re-use in future Betas

interface IProps {
featureId: string;
featureId: SettingKey;
onFinished(sendFeedback?: boolean): void;
}

Expand All @@ -35,7 +36,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({ featureId, onFinished }) => {
rageshakeLabel={info.feedbackLabel}
rageshakeData={Object.fromEntries(
(SettingsStore.getBetaInfo(featureId)?.extraSettings || []).map((k) => {
return SettingsStore.getValue(k);
return [k, SettingsStore.getValue(k)];
}),
)}
>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/ForwardDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,8 +253,8 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();

const previewLayout = useSettingValue<Layout>("layout");
const msc3946DynamicRoomPredecessors = useSettingValue<boolean>("feature_dynamic_room_predecessors");
const previewLayout = useSettingValue("layout");
const msc3946DynamicRoomPredecessors = useSettingValue("feature_dynamic_room_predecessors");

let rooms = useMemo(
() =>
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ type ShareDialogProps = XOR<Props, EventProps>;
* A dialog to share a link to a room, user, room member or a matrix event.
*/
export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element {
const showQrCode = useSettingValue<boolean>(UIFeature.ShareQRCode);
const showSocials = useSettingValue<boolean>(UIFeature.ShareSocial);
const showQrCode = useSettingValue(UIFeature.ShareQRCode);
const showSocials = useSettingValue(UIFeature.ShareSocial);

const timeoutIdRef = useRef<number>();
const [isCopied, setIsCopied] = useState(false);
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/dialogs/UserSettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ function titleForTabID(tabId: UserTab): React.ReactNode {
}

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

Expand Down
20 changes: 10 additions & 10 deletions src/components/views/dialogs/devtools/SettingExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import BaseTool, { DevtoolsContext, IDevtoolsProps } from "./BaseTool";
import AccessibleButton from "../../elements/AccessibleButton";
import SettingsStore, { LEVEL_ORDER } from "../../../../settings/SettingsStore";
import { SettingLevel } from "../../../../settings/SettingLevel";
import { SETTINGS } from "../../../../settings/Settings";
import { SettingKey, SETTINGS, SettingValueType } from "../../../../settings/Settings";
import Field from "../../elements/Field";

const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
const [setting, setSetting] = useState<string | null>(null);
const [setting, setSetting] = useState<SettingKey | null>(null);
const [editing, setEditing] = useState(false);

if (setting && editing) {
Expand All @@ -36,10 +36,10 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
};
return <ViewSetting setting={setting} onBack={onBack} onEdit={onEdit} />;
} else {
const onView = (setting: string): void => {
const onView = (setting: SettingKey): void => {
setSetting(setting);
};
const onEdit = (setting: string): void => {
const onEdit = (setting: SettingKey): void => {
setSetting(setting);
setEditing(true);
};
Expand All @@ -50,7 +50,7 @@ const SettingExplorer: React.FC<IDevtoolsProps> = ({ onBack }) => {
export default SettingExplorer;

interface ICanEditLevelFieldProps {
setting: string;
setting: SettingKey;
level: SettingLevel;
roomId?: string;
}
Expand All @@ -65,8 +65,8 @@ const CanEditLevelField: React.FC<ICanEditLevelFieldProps> = ({ setting, roomId,
);
};

function renderExplicitSettingValues(setting: string, roomId?: string): string {
const vals: Record<string, number | null> = {};
function renderExplicitSettingValues(setting: SettingKey, roomId?: string): string {
const vals: Record<string, SettingValueType> = {};
for (const level of LEVEL_ORDER) {
try {
vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
Expand All @@ -81,7 +81,7 @@ function renderExplicitSettingValues(setting: string, roomId?: string): string {
}

interface IEditSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
}

const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
Expand Down Expand Up @@ -191,7 +191,7 @@ const EditSetting: React.FC<IEditSettingProps> = ({ setting, onBack }) => {
};

interface IViewSettingProps extends Pick<IDevtoolsProps, "onBack"> {
setting: string;
setting: SettingKey;
onEdit(): Promise<void>;
}

Expand Down Expand Up @@ -258,7 +258,7 @@ const SettingsList: React.FC<ISettingsListProps> = ({ onBack, onView, onEdit })
const [query, setQuery] = useState("");

const allSettings = useMemo(() => {
let allSettings = Object.keys(SETTINGS);
let allSettings = Object.keys(SETTINGS) as SettingKey[];
if (query) {
const lcQuery = query.toLowerCase();
allSettings = allSettings.filter((setting) => setting.toLowerCase().includes(lcQuery));
Expand Down
40 changes: 30 additions & 10 deletions src/components/views/directory/NetworkDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import TextInputDialog from "../dialogs/TextInputDialog";
import AccessibleButton from "../elements/AccessibleButton";
import withValidation from "../elements/Validation";
import { SettingKey, Settings } from "../../../settings/Settings.tsx";

const SETTING_NAME = "room_directory_servers";

Expand Down Expand Up @@ -67,15 +68,32 @@ const validServer = withValidation<undefined, { error?: unknown }>({
memoize: true,
});

function useSettingsValueWithSetter<T>(
settingName: string,
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null,
excludeDefault: true,
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId?: string | null,
excludeDefault?: false,
): [Settings[S]["default"], (value: Settings[S]["default"]) => Promise<void>];
function useSettingsValueWithSetter<S extends SettingKey>(
settingName: S,
level: SettingLevel,
roomId: string | null = null,
excludeDefault = false,
): [T, (value: T) => Promise<void>] {
const [value, setValue] = useState(SettingsStore.getValue<T>(settingName, roomId ?? undefined, excludeDefault));
): [Settings[S]["default"] | undefined, (value: Settings[S]["default"]) => Promise<void>] {
const [value, setValue] = useState(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
const setter = useCallback(
async (value: T): Promise<void> => {
async (value: Settings[S]["default"]): Promise<void> => {
setValue(value);
SettingsStore.setValue(settingName, roomId, level, value);
},
Expand All @@ -84,7 +102,12 @@ function useSettingsValueWithSetter<T>(

useEffect(() => {
const ref = SettingsStore.watchSetting(settingName, roomId, () => {
setValue(SettingsStore.getValue<T>(settingName, roomId, excludeDefault));
setValue(
// XXX: This seems naff but is needed to convince TypeScript that the overload is fine
excludeDefault
? SettingsStore.getValue(settingName, roomId, excludeDefault)
: SettingsStore.getValue(settingName, roomId, excludeDefault),
);
});
// clean-up
return () => {
Expand All @@ -109,10 +132,7 @@ function removeAll<T>(target: Set<T>, ...toRemove: T[]): void {
}

function useServers(): ServerList {
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter<string[]>(
SETTING_NAME,
SettingLevel.ACCOUNT,
);
const [userDefinedServers, setUserDefinedServers] = useSettingsValueWithSetter(SETTING_NAME, SettingLevel.ACCOUNT);

const homeServer = MatrixClientPeg.safeGet().getDomain()!;
const configServers = new Set<string>(SdkConfig.getObject("room_directory")?.get("servers") ?? []);
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/elements/LanguageDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {

// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;
Expand Down
4 changes: 2 additions & 2 deletions src/components/views/elements/SettingsFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { _t } from "../../../languageHandler";
import ToggleSwitch from "./ToggleSwitch";
import StyledCheckbox from "./StyledCheckbox";
import { SettingLevel } from "../../../settings/SettingLevel";
import { defaultWatchManager } from "../../../settings/Settings";
import { BooleanSettingKey, defaultWatchManager } from "../../../settings/Settings";

interface IProps {
// The setting must be a boolean
name: string;
name: BooleanSettingKey;
level: SettingLevel;
roomId?: string; // for per-room settings
label?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component<

// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating
let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true);
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true);
let value: string | undefined;
if (language) {
value = this.props.value || language;
Expand Down
6 changes: 3 additions & 3 deletions src/components/views/messages/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ const ExpandCollapseButton: React.FC<{
};

const CodeBlock: React.FC<Props> = ({ children, onHeightChanged }) => {
const enableSyntaxHighlightLanguageDetection = useSettingValue<boolean>("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue<boolean>("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue<boolean>("expandCodeByDefault");
const enableSyntaxHighlightLanguageDetection = useSettingValue("enableSyntaxHighlightLanguageDetection");
const showCodeLineNumbers = useSettingValue("showCodeLineNumbers");
const expandCodeByDefault = useSettingValue("expandCodeByDefault");
const [expanded, setExpanded] = useState(expandCodeByDefault);

let expandCollapseButton: JSX.Element | undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/messages/TextualBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);

const htmlOpts = {
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
disableBigEmoji: isEmote || !SettingsStore.getValue("TextualBody.enableBigEmoji"),
// Part of Replies fallback support
stripReplyFallback: stripReply,
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/MessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above

const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = SettingsStore.getValue("feature_wysiwyg_composer");
let isRichTextEnabled = true;
let initialComposerContent = "";
if (isWysiwygLabEnabled) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/views/rooms/MessageComposerButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
const matrixClient = useContext(MatrixClientContext);
const { room, narrow } = useScopedRoomContext("room", "narrow");

const isWysiwygLabEnabled = useSettingValue<boolean>("feature_wysiwyg_composer");
const isWysiwygLabEnabled = useSettingValue("feature_wysiwyg_composer");

if (!matrixClient || !room || props.haveRecording) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function PlainTextComposer({
rightComponent,
eventRelation,
}: PlainTextComposerProps): JSX.Element {
const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const {
ref: editorRef,
autocompleteRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const WysiwygComposer = memo(function WysiwygComposer({

const inputEventProcessor = useInputEventProcessor(onSend, autocompleteRef, initialContent, eventRelation);

const isAutoReplaceEmojiEnabled = useSettingValue<boolean>("MessageComposerInput.autoReplaceEmoji");
const isAutoReplaceEmojiEnabled = useSettingValue("MessageComposerInput.autoReplaceEmoji");
const emojiSuggestions = useMemo(() => getEmojiSuggestions(isAutoReplaceEmojiEnabled), [isAutoReplaceEmojiEnabled]);

const { ref, isWysiwygReady, content, actionStates, wysiwyg, suggestion, messageContent } = useWysiwyg({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useInputEventProcessor(
const roomContext = useScopedRoomContext("liveTimeline", "room", "replyToEvent", "timelineRenderingType");
const composerContext = useComposerContext();
const mxClient = useMatrixClientContext();
const isCtrlEnterToSend = useSettingValue<boolean>("MessageComposerInput.ctrlEnterToSend");
const isCtrlEnterToSend = useSettingValue("MessageComposerInput.ctrlEnterToSend");

return useCallback(
(event: WysiwygEvent, composer: Wysiwyg, editor: HTMLElement) => {
Expand Down
Loading
Loading