Make more of the codebase conform to strict types (#10857)
This commit is contained in:
parent
7f017a84c2
commit
6a3f59cc76
45 changed files with 127 additions and 121 deletions
|
@ -56,7 +56,7 @@ class FilePanel extends React.Component<IProps, IState> {
|
|||
// This is used to track if a decrypted event was a live event and should be
|
||||
// added to the timeline.
|
||||
private decryptingEvents = new Set<string>();
|
||||
public noRoom: boolean;
|
||||
public noRoom = false;
|
||||
private card = createRef<HTMLDivElement>();
|
||||
|
||||
public state: IState = {
|
||||
|
|
|
@ -125,7 +125,7 @@ export function GenericDropdownMenu<T>({
|
|||
}: IProps<T>): JSX.Element {
|
||||
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
|
||||
|
||||
const selected: GenericDropdownMenuItem<T> | null = options
|
||||
const selected: GenericDropdownMenuItem<T> | undefined = options
|
||||
.flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it]))
|
||||
.find((option) => (toKey ? toKey(option.key) === toKey(value) : option.key === value));
|
||||
let contextMenuOptions: JSX.Element;
|
||||
|
|
|
@ -181,19 +181,19 @@ export default class ScrollPanel extends React.Component<IProps> {
|
|||
private unmounted = false;
|
||||
private scrollTimeout?: Timer;
|
||||
// Are we currently trying to backfill?
|
||||
private isFilling: boolean;
|
||||
private isFilling = false;
|
||||
// Is the current fill request caused by a props update?
|
||||
private isFillingDueToPropsUpdate = false;
|
||||
// Did another request to check the fill state arrive while we were trying to backfill?
|
||||
private fillRequestWhileRunning: boolean;
|
||||
private fillRequestWhileRunning = false;
|
||||
// Is that next fill request scheduled because of a props update?
|
||||
private pendingFillDueToPropsUpdate: boolean;
|
||||
private scrollState: IScrollState;
|
||||
private pendingFillDueToPropsUpdate = false;
|
||||
private scrollState!: IScrollState;
|
||||
private preventShrinkingState: IPreventShrinkingState | null = null;
|
||||
private unfillDebouncer: number | null = null;
|
||||
private bottomGrowth: number;
|
||||
private minListHeight: number;
|
||||
private heightUpdateInProgress: boolean;
|
||||
private bottomGrowth!: number;
|
||||
private minListHeight!: number;
|
||||
private heightUpdateInProgress = false;
|
||||
private divScroll: HTMLDivElement | null = null;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
|
|
|
@ -28,7 +28,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
phase?: Phase;
|
||||
lostKeys: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import classNames from "classnames";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
|
||||
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
import { _t, _td, UserFriendlyError } from "../../../languageHandler";
|
||||
import Login from "../../../Login";
|
||||
import { messageForConnectionError, messageForLoginError } from "../../../utils/ErrorUtils";
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
|
@ -110,7 +110,7 @@ type OnPasswordLogin = {
|
|||
*/
|
||||
export default class LoginComponent extends React.PureComponent<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private loginLogic: Login;
|
||||
private loginLogic!: Login;
|
||||
|
||||
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||
|
||||
|
@ -265,7 +265,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
|
|||
logger.error("Problem parsing URL or unhandled error doing .well-known discovery:", e);
|
||||
|
||||
let message = _t("Failed to perform homeserver discovery");
|
||||
if (e.translatedMessage) {
|
||||
if (e instanceof UserFriendlyError && e.translatedMessage) {
|
||||
message = e.translatedMessage;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
phase: Phase;
|
||||
phase?: Phase;
|
||||
verificationRequest: VerificationRequest | null;
|
||||
backupInfo: IKeyBackupInfo | null;
|
||||
lostKeys: boolean;
|
||||
|
|
|
@ -18,6 +18,7 @@ import React, { ChangeEvent, SyntheticEvent } from "react";
|
|||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { Optional } from "matrix-events-sdk";
|
||||
import { ISSOFlow, LoginFlow, SSOAction } from "matrix-js-sdk/src/@types/auth";
|
||||
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
@ -164,7 +165,11 @@ export default class SoftLogout extends React.Component<IProps, IState> {
|
|||
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
|
||||
} catch (e) {
|
||||
let errorText = _t("Failed to re-authenticate due to a homeserver problem");
|
||||
if (e.errcode === "M_FORBIDDEN" && (e.httpStatus === 401 || e.httpStatus === 403)) {
|
||||
if (
|
||||
e instanceof MatrixError &&
|
||||
e.errcode === "M_FORBIDDEN" &&
|
||||
(e.httpStatus === 401 || e.httpStatus === 403)
|
||||
) {
|
||||
errorText = _t("Incorrect password");
|
||||
}
|
||||
|
||||
|
|
|
@ -214,7 +214,7 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||
|
||||
let errorText = this.props.errorText;
|
||||
|
||||
let sitePublicKey;
|
||||
let sitePublicKey: string | undefined;
|
||||
if (!this.props.stageParams || !this.props.stageParams.public_key) {
|
||||
errorText = _t(
|
||||
"Missing captcha public key in homeserver configuration. Please report " +
|
||||
|
@ -224,7 +224,7 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||
sitePublicKey = this.props.stageParams.public_key;
|
||||
}
|
||||
|
||||
let errorSection;
|
||||
let errorSection: JSX.Element | undefined;
|
||||
if (errorText) {
|
||||
errorSection = (
|
||||
<div className="error" role="alert">
|
||||
|
@ -235,7 +235,9 @@ export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps
|
|||
|
||||
return (
|
||||
<div>
|
||||
<CaptchaForm sitePublicKey={sitePublicKey} onCaptchaResponse={this.onCaptchaResponse} />
|
||||
{sitePublicKey && (
|
||||
<CaptchaForm sitePublicKey={sitePublicKey} onCaptchaResponse={this.onCaptchaResponse} />
|
||||
)}
|
||||
{errorSection}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -28,7 +28,7 @@ interface IProps extends Omit<BaseProps, "matrixClient" | "children" | "onFinish
|
|||
specificLabel: string;
|
||||
noneLabel?: string;
|
||||
warningMessage?: string;
|
||||
onFinished(success: boolean, reason?: string, rooms?: Room[]): void;
|
||||
onFinished(success?: boolean, reason?: string, rooms?: Room[]): void;
|
||||
spaceChildFilter?(child: Room): boolean;
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
|
|||
return (
|
||||
<ConfirmUserActionDialog
|
||||
{...props}
|
||||
onFinished={(success: boolean, reason?: string) => {
|
||||
onFinished={(success?: boolean, reason?: string) => {
|
||||
onFinished(success, reason, roomsToLeave);
|
||||
}}
|
||||
className="mx_ConfirmSpaceUserActionDialog"
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { ReactNode, KeyboardEvent } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -30,7 +30,7 @@ interface IProps {
|
|||
button?: boolean | string;
|
||||
hasCloseButton?: boolean;
|
||||
fixedWidth?: boolean;
|
||||
onKeyDown?(event: KeyboardEvent): void;
|
||||
onKeyDown?(event: KeyboardEvent | React.KeyboardEvent): void;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
|
|||
result = await client.relations(roomId, eventId, RelationType.Replace, EventType.RoomMessage, opts);
|
||||
} catch (error) {
|
||||
// log if the server returned an error
|
||||
if (error.errcode) {
|
||||
if (error instanceof MatrixError && error.errcode) {
|
||||
logger.error("fetching /relations failed with error", error);
|
||||
}
|
||||
this.setState({ error: error as MatrixError }, () => reject(error));
|
||||
|
|
|
@ -20,7 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
|
||||
import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { _t, UserFriendlyError } from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import Field from "../elements/Field";
|
||||
|
@ -113,7 +113,7 @@ export default class ServerPickerDialog extends React.PureComponent<IProps, ISta
|
|||
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
|
||||
if (stateForError.serverErrorIsFatal) {
|
||||
let error = _t("Unable to validate homeserver");
|
||||
if (e.translatedMessage) {
|
||||
if (e instanceof UserFriendlyError && e.translatedMessage) {
|
||||
error = e.translatedMessage;
|
||||
}
|
||||
return { error };
|
||||
|
|
|
@ -54,12 +54,13 @@ export const stateKeyField = (defaultValue?: string): IFieldDef => ({
|
|||
});
|
||||
|
||||
const validateEventContent = withValidation<any, Error | undefined>({
|
||||
deriveData({ value }) {
|
||||
async deriveData({ value }) {
|
||||
try {
|
||||
JSON.parse(value!);
|
||||
} catch (e) {
|
||||
return e;
|
||||
return e as Error;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
|
|
|
@ -21,7 +21,7 @@ import BaseDialog from "../BaseDialog";
|
|||
import { _t } from "../../../../languageHandler";
|
||||
import { SetupEncryptionStore, Phase } from "../../../../stores/SetupEncryptionStore";
|
||||
|
||||
function iconFromPhase(phase: Phase): string {
|
||||
function iconFromPhase(phase?: Phase): string {
|
||||
if (phase === Phase.Done) {
|
||||
return require("../../../../../res/img/e2e/verified-deprecated.svg").default;
|
||||
} else {
|
||||
|
|
|
@ -21,17 +21,7 @@ import { IHierarchyRoom } from "matrix-js-sdk/src/@types/spaces";
|
|||
import { IPublicRoomsChunkRoom, MatrixClient, RoomMember, RoomType } from "matrix-js-sdk/src/matrix";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
KeyboardEvent,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { ChangeEvent, RefObject, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
|
||||
|
@ -1067,7 +1057,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
);
|
||||
}
|
||||
|
||||
const onDialogKeyDown = (ev: KeyboardEvent): void => {
|
||||
const onDialogKeyDown = (ev: KeyboardEvent | React.KeyboardEvent): void => {
|
||||
const navigationAction = getKeyBindingsManager().getNavigationAction(ev);
|
||||
switch (navigationAction) {
|
||||
case KeyBindingAction.FilterRooms:
|
||||
|
@ -1139,7 +1129,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (ev: KeyboardEvent): void => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
||||
|
||||
switch (action) {
|
||||
|
|
|
@ -38,7 +38,7 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
roomMember: RoomMember;
|
||||
roomMember: RoomMember | null;
|
||||
isWrapped: boolean;
|
||||
widgetDomain: string | null;
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ export default class AppPermission extends React.Component<IProps, IState> {
|
|||
|
||||
// The second step is to find the user's profile so we can show it on the prompt
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
let roomMember;
|
||||
let roomMember: RoomMember | null = null;
|
||||
if (room) roomMember = room.getMember(this.props.creatorUserId);
|
||||
|
||||
// Set all this into the initial state
|
||||
|
|
|
@ -126,7 +126,7 @@ export default class AppTile extends React.Component<IProps, IState> {
|
|||
private persistKey: string;
|
||||
private sgWidget: StopGapWidget | null;
|
||||
private dispatcherRef?: string;
|
||||
private unmounted: boolean;
|
||||
private unmounted = false;
|
||||
|
||||
public constructor(props: IProps) {
|
||||
super(props);
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject } from "react";
|
||||
import React, { InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, RefObject, createRef } from "react";
|
||||
import classNames from "classnames";
|
||||
import { debounce } from "lodash";
|
||||
|
||||
|
@ -118,7 +118,7 @@ interface IState {
|
|||
|
||||
export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||
private readonly id: string;
|
||||
private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
|
||||
private readonly _inputRef = createRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>();
|
||||
|
||||
public static readonly defaultProps = {
|
||||
element: "input",
|
||||
|
@ -228,6 +228,10 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
return valid;
|
||||
}
|
||||
|
||||
private get inputRef(): RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement> {
|
||||
return this.props.inputRef ?? this._inputRef;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
/* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
|
||||
const {
|
||||
|
@ -249,8 +253,6 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
|||
...inputProps
|
||||
} = this.props;
|
||||
|
||||
this.inputRef = inputRef || React.createRef();
|
||||
|
||||
// Handle displaying feedback on validity
|
||||
let fieldTooltip: JSX.Element | undefined;
|
||||
if (tooltipContent || this.state.feedback) {
|
||||
|
|
|
@ -302,7 +302,7 @@ interface IState {
|
|||
* tooltip along one edge of the target.
|
||||
*/
|
||||
export default class InteractiveTooltip extends React.Component<IProps, IState> {
|
||||
private target: HTMLElement;
|
||||
private target?: HTMLElement;
|
||||
|
||||
public static defaultProps = {
|
||||
side: Direction.Top,
|
||||
|
@ -345,6 +345,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
|
|||
|
||||
private onLeftOfTarget(): boolean {
|
||||
const { contentRect } = this.state;
|
||||
if (!this.target) return false;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
if (this.props.direction === Direction.Left) {
|
||||
|
@ -359,6 +360,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
|
|||
|
||||
private aboveTarget(): boolean {
|
||||
const { contentRect } = this.state;
|
||||
if (!this.target) return false;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
if (this.props.direction === Direction.Top) {
|
||||
|
@ -378,7 +380,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
|
|||
private onMouseMove = (ev: MouseEvent): void => {
|
||||
const { clientX: x, clientY: y } = ev;
|
||||
const { contentRect } = this.state;
|
||||
if (!contentRect) return;
|
||||
if (!contentRect || !this.target) return;
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
let direction: Direction;
|
||||
|
@ -423,6 +425,8 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
|
|||
return null;
|
||||
}
|
||||
|
||||
if (!this.target) return null;
|
||||
|
||||
const targetRect = this.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
|
|
|
@ -107,7 +107,7 @@ interface IProps {
|
|||
initialCaret?: DocumentOffset;
|
||||
disabled?: boolean;
|
||||
|
||||
onChange?(selection: Caret, inputType?: string, diff?: IDiff): void;
|
||||
onChange?(selection?: Caret, inputType?: string, diff?: IDiff): void;
|
||||
onPaste?(event: ClipboardEvent<HTMLDivElement>, model: EditorModel): boolean;
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
private isIMEComposing = false;
|
||||
private hasTextSelected = false;
|
||||
|
||||
private _isCaretAtEnd: boolean;
|
||||
private _isCaretAtEnd = false;
|
||||
private lastCaret: DocumentOffset;
|
||||
private lastSelection: ReturnType<typeof cloneSelection> | null = null;
|
||||
|
||||
|
@ -230,7 +230,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
|||
}
|
||||
}
|
||||
|
||||
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
private updateEditorState = (selection?: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
if (!this.editorRef.current) return;
|
||||
renderModel(this.editorRef.current, this.props.model);
|
||||
if (selection) {
|
||||
|
|
|
@ -39,6 +39,7 @@ import { useDispatcher } from "../../../hooks/useDispatcher";
|
|||
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
|
||||
import IconizedContextMenu, { IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
|
||||
import { EmojiButton } from "./EmojiButton";
|
||||
import { filterBoolean } from "../../../utils/arrays";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
||||
|
@ -118,8 +119,8 @@ const MessageComposerButtons: React.FC<IProps> = (props: IProps) => {
|
|||
];
|
||||
}
|
||||
|
||||
mainButtons = mainButtons.filter((x: ReactElement) => x);
|
||||
moreButtons = moreButtons.filter((x: ReactElement) => x);
|
||||
mainButtons = filterBoolean(mainButtons);
|
||||
moreButtons = filterBoolean(moreButtons);
|
||||
|
||||
const moreOptionsClasses = classNames({
|
||||
mx_MessageComposer_button: true,
|
||||
|
|
|
@ -313,7 +313,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private onResize = (
|
||||
e: MouseEvent | TouchEvent,
|
||||
travelDirection: Direction,
|
||||
refToElement: HTMLDivElement,
|
||||
refToElement: HTMLElement,
|
||||
delta: ResizeDelta,
|
||||
): void => {
|
||||
const newHeight = this.heightAtStart + delta.height;
|
||||
|
@ -329,7 +329,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
private onResizeStop = (
|
||||
e: MouseEvent | TouchEvent,
|
||||
travelDirection: Direction,
|
||||
refToElement: HTMLDivElement,
|
||||
refToElement: HTMLElement,
|
||||
delta: ResizeDelta,
|
||||
): void => {
|
||||
const newHeight = this.heightAtStart + delta.height;
|
||||
|
|
|
@ -27,7 +27,7 @@ import SearchWarning, { WarningKind } from "../elements/SearchWarning";
|
|||
|
||||
interface IProps {
|
||||
onCancelClick: () => void;
|
||||
onSearch: (query: string, scope: string) => void;
|
||||
onSearch: (query: string, scope: SearchScope) => void;
|
||||
searchInProgress?: boolean;
|
||||
isRoomEncrypted?: boolean;
|
||||
}
|
||||
|
|
|
@ -686,7 +686,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
|
|||
return false;
|
||||
};
|
||||
|
||||
private onChange = (selection: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
private onChange = (selection?: Caret, inputType?: string, diff?: IDiff): void => {
|
||||
// We call this in here rather than onKeyDown as that would trip it on global shortcuts e.g. Ctrl-k also
|
||||
if (!!diff) {
|
||||
this.prepareToEncrypt?.();
|
||||
|
|
|
@ -31,7 +31,7 @@ interface IKeyboardShortcutRowProps {
|
|||
}
|
||||
|
||||
// Filter out the labs section if labs aren't enabled.
|
||||
const visibleCategories = Object.entries(CATEGORIES).filter(
|
||||
const visibleCategories = (Object.entries(CATEGORIES) as [CategoryName, ICategory][]).filter(
|
||||
([categoryName]) => categoryName !== CategoryName.LABS || SdkConfig.get("show_labs_settings"),
|
||||
);
|
||||
|
||||
|
@ -73,7 +73,7 @@ const KeyboardUserSettingsTab: React.FC = () => {
|
|||
return (
|
||||
<div className="mx_SettingsTab mx_KeyboardUserSettingsTab">
|
||||
<div className="mx_SettingsTab_heading">{_t("Keyboard")}</div>
|
||||
{visibleCategories.map(([categoryName, category]: [CategoryName, ICategory]) => {
|
||||
{visibleCategories.map(([categoryName, category]) => {
|
||||
return <KeyboardShortcutSection key={categoryName} categoryName={categoryName} category={category} />;
|
||||
})}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue