diff --git a/package.json b/package.json index b874779511..ebc99caa15 100644 --- a/package.json +++ b/package.json @@ -154,11 +154,13 @@ "@types/flux": "^3.1.9", "@types/fs-extra": "^11.0.0", "@types/geojson": "^7946.0.8", + "@types/glob-to-regexp": "^0.4.1", "@types/jest": "^29.2.1", "@types/katex": "^0.14.0", "@types/lodash": "^4.14.168", "@types/modernizr": "^3.5.3", "@types/node": "^16", + "@types/node-fetch": "^2.6.2", "@types/pako": "^2.0.0", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", @@ -168,6 +170,7 @@ "@types/react-test-renderer": "^17.0.1", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", + "@types/tar-js": "^0.3.2", "@types/ua-parser-js": "^0.7.36", "@types/zxcvbn": "^4.4.0", "@typescript-eslint/eslint-plugin": "^5.35.1", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index f1e72ca2fc..9d3b64fd6a 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -218,7 +218,7 @@ declare global { processorCtor: (new (options?: AudioWorkletNodeOptions) => AudioWorkletProcessor) & { parameterDescriptors?: AudioParamDescriptor[]; }, - ); + ): void; // eslint-disable-next-line no-var var grecaptcha: diff --git a/src/@types/opus-recorder.d.ts b/src/@types/opus-recorder.d.ts new file mode 100644 index 0000000000..a964278aa1 --- /dev/null +++ b/src/@types/opus-recorder.d.ts @@ -0,0 +1,65 @@ +/* +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +declare module "opus-recorder/dist/recorder.min.js" { + export default class Recorder { + public static isRecordingSupported(): boolean; + + public constructor(config: { + bufferLength?: number; + encoderApplication?: number; + encoderFrameSize?: number; + encoderPath?: string; + encoderSampleRate?: number; + encoderBitRate?: number; + maxFramesPerPage?: number; + mediaTrackConstraints?: boolean; + monitorGain?: number; + numberOfChannels?: number; + recordingGain?: number; + resampleQuality?: number; + streamPages?: boolean; + wavBitDepth?: number; + sourceNode?: MediaStreamAudioSourceNode; + encoderComplexity?: number; + }); + + public ondataavailable?(data: ArrayBuffer): void; + + public readonly encodedSamplePosition: number; + + public start(): Promise; + + public stop(): Promise; + + public close(): void; + } +} + +declare module "opus-recorder/dist/encoderWorker.min.js" { + const path: string; + export default path; +} + +declare module "opus-recorder/dist/waveWorker.min.js" { + const path: string; + export default path; +} + +declare module "opus-recorder/dist/decoderWorker.min.js" { + const path: string; + export default path; +} diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index b6ed0d5738..5d8d947854 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; +import { IAuthData, IRequestMsisdnTokenResponse, IRequestTokenResponse } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "./MatrixClientPeg"; import Modal from "./Modal"; @@ -29,6 +29,12 @@ function getIdServerDomain(): string { return MatrixClientPeg.get().idBaseUrl.split("://")[1]; } +export type Binding = { + bind: boolean; + label: string; + errorTitle: string; +}; + /** * Allows a user to add a third party identifier to their homeserver and, * optionally, the identity servers. @@ -178,7 +184,7 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async checkEmailLinkClicked(): Promise { + public async checkEmailLinkClicked(): Promise<[boolean, IAuthData | Error | null]> { try { if (await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind()) { if (this.bind) { @@ -220,16 +226,19 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog, { - title: _t("Add Email Address"), - matrixClient: MatrixClientPeg.get(), - authData: e.data, - makeRequest: this.makeAddThreepidOnlyRequest, - aestheticsForStagePhases: { - [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, - [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + const { finished } = Modal.createDialog<[boolean, IAuthData | Error | null]>( + InteractiveAuthDialog, + { + title: _t("Add Email Address"), + matrixClient: MatrixClientPeg.get(), + authData: e.data, + makeRequest: this.makeAddThreepidOnlyRequest, + aestheticsForStagePhases: { + [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, + [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, + }, }, - }); + ); return finished; } } diff --git a/src/AsyncWrapper.tsx b/src/AsyncWrapper.tsx index 226f5b692b..9d0ba11b06 100644 --- a/src/AsyncWrapper.tsx +++ b/src/AsyncWrapper.tsx @@ -42,10 +42,7 @@ interface IState { export default class AsyncWrapper extends React.Component { private unmounted = false; - public state = { - component: null, - error: null, - }; + public state: IState = {}; public componentDidMount(): void { // XXX: temporary logging to try to diagnose diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 46f964995a..7676305c4f 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -197,7 +197,7 @@ export default abstract class BasePlatform { room: Room, ev?: MatrixEvent, ): Notification { - const notifBody = { + const notifBody: NotificationOptions = { body: msg, silent: true, // we play our own sounds }; diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index f2452327e5..a6b999fa27 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -228,7 +228,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // Sanitise and transform data-mx-color and data-mx-bg-color to their CSS // equivalents - const customCSSMapper = { + const customCSSMapper: Record = { "data-mx-color": "color", "data-mx-bg-color": "background-color", // $customAttributeKey: $cssAttributeKey diff --git a/src/IConfigOptions.ts b/src/IConfigOptions.ts index 8234f5bc75..1db73cc074 100644 --- a/src/IConfigOptions.ts +++ b/src/IConfigOptions.ts @@ -169,10 +169,18 @@ export interface IConfigOptions { inline?: { left?: string; right?: string; + pattern?: { + tex?: string; + latex?: string; + }; }; display?: { left?: string; right?: string; + pattern?: { + tex?: string; + latex?: string; + }; }; }; diff --git a/src/Keyboard.ts b/src/Keyboard.ts index 9d4d3f6152..7b1ea4031b 100644 --- a/src/Keyboard.ts +++ b/src/Keyboard.ts @@ -16,6 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; + export const Key = { HOME: "Home", END: "End", @@ -76,7 +78,7 @@ export const Key = { export const IS_MAC = navigator.platform.toUpperCase().includes("MAC"); -export function isOnlyCtrlOrCmdKeyEvent(ev: KeyboardEvent): boolean { +export function isOnlyCtrlOrCmdKeyEvent(ev: React.KeyboardEvent | KeyboardEvent): boolean { if (IS_MAC) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; } else { diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index 82e5cac996..c3ca256646 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -158,9 +158,9 @@ export default class LegacyCallHandler extends EventEmitter { private transferees = new Map(); // callId (target) -> call (transferee) private audioPromises = new Map>(); private audioElementsWithListeners = new Map(); - private supportsPstnProtocol = null; - private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol - private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native + private supportsPstnProtocol: boolean | null = null; + private pstnSupportPrefixed: boolean | null = null; // True if the server only support the prefixed pstn protocol + private supportsSipNativeVirtual: boolean | null = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native // Map of the asserted identity users after we've looked them up using the API. // We need to be be able to determine the mapped room synchronously, so we @@ -187,7 +187,7 @@ export default class LegacyCallHandler extends EventEmitter { // check asserted identity: if we're not obeying asserted identity, // this map will never be populated, but we check anyway for sanity if (this.shouldObeyAssertedfIdentity()) { - const nativeUser = this.assertedIdentityNativeUsers[call.callId]; + const nativeUser = this.assertedIdentityNativeUsers.get(call.callId); if (nativeUser) { const room = findDMForUser(MatrixClientPeg.get(), nativeUser); if (room) return room.roomId; @@ -466,8 +466,8 @@ export default class LegacyCallHandler extends EventEmitter { return this.getAllActiveCallsNotInRoom(roomId); } - public getTransfereeForCallId(callId: string): MatrixCall { - return this.transferees[callId]; + public getTransfereeForCallId(callId: string): MatrixCall | undefined { + return this.transferees.get(callId); } public play(audioId: AudioID): void { @@ -621,7 +621,7 @@ export default class LegacyCallHandler extends EventEmitter { logger.log(`Asserted identity ${newAssertedIdentity} mapped to ${newNativeAssertedIdentity}`); if (newNativeAssertedIdentity) { - this.assertedIdentityNativeUsers[call.callId] = newNativeAssertedIdentity; + this.assertedIdentityNativeUsers.set(call.callId, newNativeAssertedIdentity); // If we don't already have a room with this user, make one. This will be slightly odd // if they called us because we'll be inviting them, but there's not much we can do about @@ -917,7 +917,7 @@ export default class LegacyCallHandler extends EventEmitter { return; } if (transferee) { - this.transferees[call.callId] = transferee; + this.transferees.set(call.callId, transferee); } this.setCallListeners(call); diff --git a/src/Login.ts b/src/Login.ts index 6475a9f5c9..dbcdfe954e 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -91,12 +91,12 @@ export default class Login { } public loginViaPassword( - username: string, - phoneCountry: string, - phoneNumber: string, + username: string | undefined, + phoneCountry: string | undefined, + phoneNumber: string | undefined, password: string, ): Promise { - const isEmail = username.indexOf("@") > 0; + const isEmail = username?.indexOf("@") > 0; let identifier; if (phoneCountry && phoneNumber) { diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 329741fe51..fcb066e92f 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -37,7 +37,7 @@ export enum MediaDeviceHandlerEvent { } export default class MediaDeviceHandler extends EventEmitter { - private static internalInstance; + private static internalInstance?: MediaDeviceHandler; public static get instance(): MediaDeviceHandler { if (!MediaDeviceHandler.internalInstance) { @@ -67,7 +67,7 @@ export default class MediaDeviceHandler extends EventEmitter { public static async getDevices(): Promise { try { const devices = await navigator.mediaDevices.enumerateDevices(); - const output = { + const output: Record = { [MediaDeviceKindEnum.AudioOutput]: [], [MediaDeviceKindEnum.AudioInput]: [], [MediaDeviceKindEnum.VideoInput]: [], diff --git a/src/NodeAnimator.tsx b/src/NodeAnimator.tsx index 24b4e85ae3..be2a3442e9 100644 --- a/src/NodeAnimator.tsx +++ b/src/NodeAnimator.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactInstance } from "react"; import ReactDom from "react-dom"; interface IChildProps { @@ -41,7 +41,7 @@ interface IProps { * automatic positional animation, look at react-shuffle or similar libraries. */ export default class NodeAnimator extends React.Component { - private nodes = {}; + private nodes: Record = {}; private children: { [key: string]: React.DetailedReactHTMLElement }; public static defaultProps: Partial = { startStyles: [], @@ -65,7 +65,7 @@ export default class NodeAnimator extends React.Component { */ private applyStyles(node: HTMLElement, styles: React.CSSProperties): void { Object.entries(styles).forEach(([property, value]) => { - node.style[property] = value; + node.style[property as keyof Omit] = value; }); } diff --git a/src/Notifier.ts b/src/Notifier.ts index 42909a2632..0faa533341 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -68,7 +68,7 @@ Override both the content body and the TextForEvent handler for specific msgtype This is useful when the content body contains fallback text that would explain that the client can't handle a particular type of tile. */ -const msgTypeHandlers = { +const msgTypeHandlers: Record string> = { [MsgType.KeyVerificationRequest]: (event: MatrixEvent) => { const name = (event.sender || {}).name; return _t("%(name)s is requesting verification", { name }); @@ -95,22 +95,26 @@ const msgTypeHandlers = { }, }; -export const Notifier = { - notifsByRoom: {}, +class NotifierClass { + private notifsByRoom: Record = {}; // A list of event IDs that we've received but need to wait until // they're decrypted until we decide whether to notify for them // or not - pendingEncryptedEventIds: [], + private pendingEncryptedEventIds: string[] = []; - notificationMessageForEvent: function (ev: MatrixEvent): string { + private toolbarHidden?: boolean; + private isSyncing?: boolean; + + public notificationMessageForEvent(ev: MatrixEvent): string { if (msgTypeHandlers.hasOwnProperty(ev.getContent().msgtype)) { return msgTypeHandlers[ev.getContent().msgtype](ev); } return TextForEvent.textForEvent(ev); - }, + } - _displayPopupNotification: function (ev: MatrixEvent, room: Room): void { + // XXX: exported for tests + public displayPopupNotification(ev: MatrixEvent, room: Room): void { const plaf = PlatformPeg.get(); const cli = MatrixClientPeg.get(); if (!plaf) { @@ -165,9 +169,14 @@ export const Notifier = { if (this.notifsByRoom[ev.getRoomId()] === undefined) this.notifsByRoom[ev.getRoomId()] = []; this.notifsByRoom[ev.getRoomId()].push(notif); } - }, + } - getSoundForRoom: function (roomId: string) { + public getSoundForRoom(roomId: string): { + url: string; + name: string; + type: string; + size: string; + } | null { // We do no caching here because the SDK caches setting // and the browser will cache the sound. const content = SettingsStore.getValue("notificationSound", roomId); @@ -193,9 +202,10 @@ export const Notifier = { type: content.type, size: content.size, }; - }, + } - _playAudioNotification: async function (ev: MatrixEvent, room: Room): Promise { + // XXX: Exported for tests + public async playAudioNotification(ev: MatrixEvent, room: Room): Promise { const cli = MatrixClientPeg.get(); if (localNotificationsAreSilenced(cli)) { return; @@ -224,39 +234,32 @@ export const Notifier = { } catch (ex) { logger.warn("Caught error when trying to fetch room notification sound:", ex); } - }, + } - start: function (this: typeof Notifier) { - // do not re-bind in the case of repeated call - this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this); - this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this); - this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this); - this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this); - - MatrixClientPeg.get().on(RoomEvent.Timeline, this.boundOnEvent); - MatrixClientPeg.get().on(RoomEvent.Receipt, this.boundOnRoomReceipt); - MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); - MatrixClientPeg.get().on(ClientEvent.Sync, this.boundOnSyncStateChange); + public start(): void { + MatrixClientPeg.get().on(RoomEvent.Timeline, this.onEvent); + MatrixClientPeg.get().on(RoomEvent.Receipt, this.onRoomReceipt); + MatrixClientPeg.get().on(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().on(ClientEvent.Sync, this.onSyncStateChange); this.toolbarHidden = false; this.isSyncing = false; - }, + } - stop: function (this: typeof Notifier) { + public stop(): void { if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.boundOnEvent); - MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.boundOnRoomReceipt); - MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.boundOnEventDecrypted); - MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.boundOnSyncStateChange); + MatrixClientPeg.get().removeListener(RoomEvent.Timeline, this.onEvent); + MatrixClientPeg.get().removeListener(RoomEvent.Receipt, this.onRoomReceipt); + MatrixClientPeg.get().removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted); + MatrixClientPeg.get().removeListener(ClientEvent.Sync, this.onSyncStateChange); } this.isSyncing = false; - }, + } - supportsDesktopNotifications: function () { - const plaf = PlatformPeg.get(); - return plaf && plaf.supportsNotifications(); - }, + public supportsDesktopNotifications(): boolean { + return PlatformPeg.get()?.supportsNotifications() ?? false; + } - setEnabled: function (enable: boolean, callback?: () => void) { + public setEnabled(enable: boolean, callback?: () => void): void { const plaf = PlatformPeg.get(); if (!plaf) return; @@ -320,31 +323,30 @@ export const Notifier = { // set the notifications_hidden flag, as the user has knowingly interacted // with the setting we shouldn't nag them any further this.setPromptHidden(true); - }, + } - isEnabled: function () { + public isEnabled(): boolean { return this.isPossible() && SettingsStore.getValue("notificationsEnabled"); - }, + } - isPossible: function () { + public isPossible(): boolean { const plaf = PlatformPeg.get(); - if (!plaf) return false; - if (!plaf.supportsNotifications()) return false; + if (!plaf?.supportsNotifications()) return false; if (!plaf.maySendNotifications()) return false; return true; // possible, but not necessarily enabled - }, + } - isBodyEnabled: function () { + public isBodyEnabled(): boolean { return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled"); - }, + } - isAudioEnabled: function () { + public isAudioEnabled(): boolean { // We don't route Audio via the HTML Notifications API so it is possible regardless of other things return SettingsStore.getValue("audioNotificationsEnabled"); - }, + } - setPromptHidden: function (this: typeof Notifier, hidden: boolean, persistent = true) { + public setPromptHidden(hidden: boolean, persistent = true): void { this.toolbarHidden = hidden; hideNotificationsToast(); @@ -353,9 +355,9 @@ export const Notifier = { if (persistent && global.localStorage) { global.localStorage.setItem("notifications_hidden", String(hidden)); } - }, + } - shouldShowPrompt: function () { + public shouldShowPrompt(): boolean { const client = MatrixClientPeg.get(); if (!client) { return false; @@ -366,25 +368,21 @@ export const Notifier = { this.supportsDesktopNotifications() && !isPushNotifyDisabled() && !this.isEnabled() && - !this._isPromptHidden() + !this.isPromptHidden() ); - }, + } - _isPromptHidden: function (this: typeof Notifier) { + private isPromptHidden(): boolean { // Check localStorage for any such meta data if (global.localStorage) { return global.localStorage.getItem("notifications_hidden") === "true"; } return this.toolbarHidden; - }, + } - onSyncStateChange: function ( - this: typeof Notifier, - state: SyncState, - prevState?: SyncState, - data?: ISyncStateData, - ) { + // XXX: Exported for tests + public onSyncStateChange = (state: SyncState, prevState?: SyncState, data?: ISyncStateData): void => { if (state === SyncState.Syncing) { this.isSyncing = true; } else if (state === SyncState.Stopped || state === SyncState.Error) { @@ -395,16 +393,15 @@ export const Notifier = { if (![SyncState.Stopped, SyncState.Error].includes(state) && !data?.fromCache) { createLocalNotificationSettingsIfNeeded(MatrixClientPeg.get()); } - }, + }; - onEvent: function ( - this: typeof Notifier, + private onEvent = ( ev: MatrixEvent, room: Room | undefined, toStartOfTimeline: boolean | undefined, removed: boolean, data: IRoomTimelineData, - ) { + ): void => { if (!data.liveEvent) return; // only notify for new things, not old. if (!this.isSyncing) return; // don't alert for any messages initially if (ev.getSender() === MatrixClientPeg.get().getUserId()) return; @@ -422,10 +419,10 @@ export const Notifier = { return; } - this._evaluateEvent(ev); - }, + this.evaluateEvent(ev); + }; - onEventDecrypted: function (ev: MatrixEvent) { + private onEventDecrypted = (ev: MatrixEvent): void => { // 'decrypted' means the decryption process has finished: it may have failed, // in which case it might decrypt soon if the keys arrive if (ev.isDecryptionFailure()) return; @@ -434,10 +431,10 @@ export const Notifier = { if (idx === -1) return; this.pendingEncryptedEventIds.splice(idx, 1); - this._evaluateEvent(ev); - }, + this.evaluateEvent(ev); + }; - onRoomReceipt: function (ev: MatrixEvent, room: Room) { + private onRoomReceipt = (ev: MatrixEvent, room: Room): void => { if (room.getUnreadNotificationCount() === 0) { // ideally we would clear each notification when it was read, // but we have no way, given a read receipt, to know whether @@ -453,12 +450,12 @@ export const Notifier = { } delete this.notifsByRoom[room.roomId]; } - }, + }; - _evaluateEvent: function (ev: MatrixEvent) { + // XXX: exported for tests + public evaluateEvent(ev: MatrixEvent): void { // Mute notifications for broadcast info events if (ev.getType() === VoiceBroadcastInfoEventType) return; - let roomId = ev.getRoomId(); if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { // Attempt to translate a virtual room to a native one @@ -477,7 +474,7 @@ export const Notifier = { const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions?.notify) { - this._performCustomEventHandling(ev); + this.performCustomEventHandling(ev); const store = SdkContextClass.instance.roomViewStore; const isViewingRoom = store.getRoomId() === room.roomId; @@ -492,19 +489,19 @@ export const Notifier = { } if (this.isEnabled()) { - this._displayPopupNotification(ev, room); + this.displayPopupNotification(ev, room); } if (actions.tweaks.sound && this.isAudioEnabled()) { PlatformPeg.get().loudNotification(ev, room); - this._playAudioNotification(ev, room); + this.playAudioNotification(ev, room); } } - }, + } /** * Some events require special handling such as showing in-app toasts */ - _performCustomEventHandling: function (ev: MatrixEvent) { + private performCustomEventHandling(ev: MatrixEvent): void { if (ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType()) && SettingsStore.getValue("feature_group_calls")) { ToastStore.sharedInstance().addOrReplaceToast({ key: getIncomingCallToastKey(ev.getStateKey()), @@ -514,11 +511,12 @@ export const Notifier = { props: { callEvent: ev }, }); } - }, -}; + } +} if (!window.mxNotifier) { - window.mxNotifier = Notifier; + window.mxNotifier = new NotifierClass(); } export default window.mxNotifier; +export const Notifier: NotifierClass = window.mxNotifier; diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts index c8a99ab426..173fdb0b19 100644 --- a/src/PosthogAnalytics.ts +++ b/src/PosthogAnalytics.ts @@ -132,8 +132,8 @@ export class PosthogAnalytics { private anonymity = Anonymity.Disabled; // set true during the constructor if posthog config is present, otherwise false private readonly enabled: boolean = false; - private static _instance = null; - private platformSuperProperties = {}; + private static _instance: PosthogAnalytics | null = null; + private platformSuperProperties: Properties = {}; public static readonly ANALYTICS_EVENT_TYPE = "im.vector.analytics"; private propertiesForNextEvent: Partial> = {}; private userPropertyCache: UserProperties = {}; diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 20db6594b0..bc1c4bb4b9 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -182,7 +182,7 @@ async function getSecretStorageKey({ export async function getDehydrationKey( keyInfo: ISecretStorageKeyInfo, - checkFunc: (Uint8Array) => void, + checkFunc: (data: Uint8Array) => void, ): Promise { const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.(); if (keyFromCustomisations) { @@ -196,7 +196,7 @@ export async function getDehydrationKey( /* props= */ { keyInfo, - checkPrivateKey: async (input): Promise => { + checkPrivateKey: async (input: KeyParams): Promise => { const key = await inputToKey(input); try { checkFunc(key); @@ -290,7 +290,7 @@ export async function promptForBackupPassphrase(): Promise { RestoreKeyBackupDialog, { showSummary: false, - keyCallback: (k) => (key = k), + keyCallback: (k: Uint8Array) => (key = k), }, null, /* priority = */ false, diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 1ffe43fec7..43350e44d0 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -697,11 +697,8 @@ export const Commands = [ } if (viaServers) { - // For the join - dispatch["opts"] = { - // These are passed down to the js-sdk's /join call - viaServers: viaServers, - }; + // For the join, these are passed down to the js-sdk's /join call + dispatch["opts"] = { viaServers }; // For if the join fails (rejoin button) dispatch["via_servers"] = viaServers; diff --git a/src/Terms.ts b/src/Terms.ts index bb18a18cf7..f66f543887 100644 --- a/src/Terms.ts +++ b/src/Terms.ts @@ -52,11 +52,13 @@ export type Policies = { [policy: string]: Policy; }; +export type ServicePolicyPair = { + policies: Policies; + service: Service; +}; + export type TermsInteractionCallback = ( - policiesAndServicePairs: { - service: Service; - policies: Policies; - }[], + policiesAndServicePairs: ServicePolicyPair[], agreedUrls: string[], extraClassNames?: string, ) => Promise; @@ -117,9 +119,9 @@ export async function startTermsFlow( // but then they'd assume they can un-check the boxes to un-agree to a policy, // but that is not a thing the API supports, so probably best to just show // things they've not agreed to yet. - const unagreedPoliciesAndServicePairs = []; + const unagreedPoliciesAndServicePairs: ServicePolicyPair[] = []; for (const { service, policies } of policiesAndServicePairs) { - const unagreedPolicies = {}; + const unagreedPolicies: Policies = {}; for (const [policyName, policy] of Object.entries(policies)) { let policyAgreed = false; for (const lang of Object.keys(policy)) { diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 7f874f8a89..a515576748 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -33,7 +33,7 @@ import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases"; import defaultDispatcher from "./dispatcher/dispatcher"; import { MatrixClientPeg } from "./MatrixClientPeg"; import { ROOM_SECURITY_TAB } from "./components/views/dialogs/RoomSettingsDialog"; -import AccessibleButton from "./components/views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "./components/views/elements/AccessibleButton"; import RightPanelStore from "./stores/right-panel/RightPanelStore"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { ElementCall } from "./models/Call"; @@ -308,7 +308,7 @@ function textForServerACLEvent(ev: MatrixEvent): () => string | null { allow_ip_literals: prevContent.allow_ip_literals !== false, }; - let getText = null; + let getText: () => string = null; if (prev.deny.length === 0 && prev.allow.length === 0) { getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", { senderDisplayName }); } else { @@ -360,8 +360,8 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { const oldAltAliases = ev.getPrevContent().alt_aliases || []; const newAlias = ev.getContent().alias; const newAltAliases = ev.getContent().alt_aliases || []; - const removedAltAliases = oldAltAliases.filter((alias) => !newAltAliases.includes(alias)); - const addedAltAliases = newAltAliases.filter((alias) => !oldAltAliases.includes(alias)); + const removedAltAliases = oldAltAliases.filter((alias: string) => !newAltAliases.includes(alias)); + const addedAltAliases = newAltAliases.filter((alias: string) => !oldAltAliases.includes(alias)); if (!removedAltAliases.length && !addedAltAliases.length) { if (newAlias) { @@ -533,8 +533,8 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render const senderName = getSenderName(event); const roomId = event.getRoomId(); - const pinned = event.getContent().pinned ?? []; - const previouslyPinned = event.getPrevContent().pinned ?? []; + const pinned = event.getContent<{ pinned: string[] }>().pinned ?? []; + const previouslyPinned: string[] = event.getPrevContent().pinned ?? []; const newlyPinned = pinned.filter((item) => previouslyPinned.indexOf(item) < 0); const newlyUnpinned = previouslyPinned.filter((item) => pinned.indexOf(item) < 0); @@ -550,7 +550,10 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render { senderName }, { a: (sub) => ( - highlightEvent(roomId, messageId)}> + highlightEvent(roomId, messageId)} + > {sub} ), @@ -580,7 +583,10 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => Render { senderName }, { a: (sub) => ( - highlightEvent(roomId, messageId)}> + highlightEvent(roomId, messageId)} + > {sub} ), diff --git a/src/UserActivity.ts b/src/UserActivity.ts index 44c8abb2c7..ae6417d4f4 100644 --- a/src/UserActivity.ts +++ b/src/UserActivity.ts @@ -168,7 +168,7 @@ export default class UserActivity { return this.activeRecentlyTimeout.isRunning(); } - private onPageVisibilityChanged = (e): void => { + private onPageVisibilityChanged = (e: Event): void => { if (this.document.visibilityState === "hidden") { this.activeNowTimeout.abort(); this.activeRecentlyTimeout.abort(); @@ -182,7 +182,8 @@ export default class UserActivity { this.activeRecentlyTimeout.abort(); }; - private onUserActivity = (event: Event): void => { + // XXX: exported for tests + public onUserActivity = (event: Event): void => { // ignore anything if the window isn't focused if (!this.document.hasFocus()) return; diff --git a/src/accessibility/KeyboardShortcutUtils.ts b/src/accessibility/KeyboardShortcutUtils.ts index bb42f7c1ce..8ba866be3f 100644 --- a/src/accessibility/KeyboardShortcutUtils.ts +++ b/src/accessibility/KeyboardShortcutUtils.ts @@ -27,6 +27,7 @@ import { KEYBOARD_SHORTCUTS, MAC_ONLY_SHORTCUTS, } from "./KeyboardShortcuts"; +import { IBaseSetting } from "../settings/Settings"; /** * This function gets the keyboard shortcuts that should be presented in the UI @@ -103,7 +104,7 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { return true; }) .reduce((o, key) => { - o[key] = KEYBOARD_SHORTCUTS[key]; + o[key as KeyBindingAction] = KEYBOARD_SHORTCUTS[key as KeyBindingAction]; return o; }, {} as IKeyboardShortcuts); }; @@ -112,7 +113,10 @@ export const getKeyboardShortcuts = (): IKeyboardShortcuts => { * Gets keyboard shortcuts that should be presented to the user in the UI. */ export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { - const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())]; + const entries = [...Object.entries(getUIOnlyShortcuts()), ...Object.entries(getKeyboardShortcuts())] as [ + KeyBindingAction, + IBaseSetting, + ][]; return entries.reduce((acc, [key, value]) => { acc[key] = value; @@ -120,11 +124,11 @@ export const getKeyboardShortcutsForUI = (): IKeyboardShortcuts => { }, {} as IKeyboardShortcuts); }; -export const getKeyboardShortcutValue = (name: string): KeyCombo | undefined => { +export const getKeyboardShortcutValue = (name: KeyBindingAction): KeyCombo | undefined => { return getKeyboardShortcutsForUI()[name]?.default; }; -export const getKeyboardShortcutDisplayName = (name: string): string | undefined => { +export const getKeyboardShortcutDisplayName = (name: KeyBindingAction): string | undefined => { const keyboardShortcutDisplayName = getKeyboardShortcutsForUI()[name]?.displayName; - return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName); + return keyboardShortcutDisplayName && _t(keyboardShortcutDisplayName as string); }; diff --git a/src/accessibility/KeyboardShortcuts.ts b/src/accessibility/KeyboardShortcuts.ts index 0e536ac149..3011a5b5bd 100644 --- a/src/accessibility/KeyboardShortcuts.ts +++ b/src/accessibility/KeyboardShortcuts.ts @@ -156,10 +156,8 @@ export enum KeyBindingAction { type KeyboardShortcutSetting = IBaseSetting; -export type IKeyboardShortcuts = { - // TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager - [k in KeyBindingAction]?: KeyboardShortcutSetting; -}; +// TODO: We should figure out what to do with the keyboard shortcuts that are not handled by KeybindingManager +export type IKeyboardShortcuts = Partial>; export interface ICategory { categoryLabel?: string; diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 605ffb1f5b..b449b10710 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -25,6 +25,7 @@ import React, { Reducer, Dispatch, RefObject, + ReactNode, } from "react"; import { getKeyBindingsManager } from "../KeyBindingsManager"; @@ -158,8 +159,8 @@ interface IProps { handleHomeEnd?: boolean; handleUpDown?: boolean; handleLeftRight?: boolean; - children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent) }); - onKeyDown?(ev: React.KeyboardEvent, state: IState); + children(renderProps: { onKeyDownHandler(ev: React.KeyboardEvent): void }): ReactNode; + onKeyDown?(ev: React.KeyboardEvent, state: IState): void; } export const findSiblingElement = ( diff --git a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx index ee3a0e4d36..e8e69865d7 100644 --- a/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx +++ b/src/accessibility/context_menu/StyledMenuItemCheckbox.tsx @@ -25,7 +25,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; - onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent onClose(): void; // gets called after onChange on KeyBindingAction.ActivateSelectedButton } diff --git a/src/accessibility/context_menu/StyledMenuItemRadio.tsx b/src/accessibility/context_menu/StyledMenuItemRadio.tsx index 2fe8738434..7a394a3d1f 100644 --- a/src/accessibility/context_menu/StyledMenuItemRadio.tsx +++ b/src/accessibility/context_menu/StyledMenuItemRadio.tsx @@ -25,7 +25,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; interface IProps extends React.ComponentProps { label?: string; - onChange(); // we handle keyup/down ourselves so lose the ChangeEvent + onChange(): void; // we handle keyup/down ourselves so lose the ChangeEvent onClose(): void; // gets called after onChange on KeyBindingAction.Enter } diff --git a/src/accessibility/roving/RovingAccessibleButton.tsx b/src/accessibility/roving/RovingAccessibleButton.tsx index 3968ef6d6b..71818c6cda 100644 --- a/src/accessibility/roving/RovingAccessibleButton.tsx +++ b/src/accessibility/roving/RovingAccessibleButton.tsx @@ -30,7 +30,7 @@ export const RovingAccessibleButton: React.FC = ({ inputRef, onFocus, .. return ( { + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} diff --git a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx index f30225f0f7..f06cc934bb 100644 --- a/src/accessibility/roving/RovingAccessibleTooltipButton.tsx +++ b/src/accessibility/roving/RovingAccessibleTooltipButton.tsx @@ -31,7 +31,7 @@ export const RovingAccessibleTooltipButton: React.FC = ({ inputRef, onFo return ( { + onFocus={(event: React.FocusEvent) => { onFocusInternal(); onFocus?.(event); }} diff --git a/src/accessibility/roving/RovingTabIndexWrapper.tsx b/src/accessibility/roving/RovingTabIndexWrapper.tsx index b549f18119..4208d47499 100644 --- a/src/accessibility/roving/RovingTabIndexWrapper.tsx +++ b/src/accessibility/roving/RovingTabIndexWrapper.tsx @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { useRovingTabIndex } from "../RovingTabIndex"; import { FocusHandler, Ref } from "./types"; interface IProps { inputRef?: Ref; - children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref }); + children(renderProps: { onFocus: FocusHandler; isActive: boolean; ref: Ref }): ReactElement; } // Wrapper to allow use of useRovingTabIndex outside of React Functional Components. diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index 637b57071e..49351757ca 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -54,7 +54,7 @@ export default class RoomListActions { oldIndex: number | null, newIndex: number | null, ): AsyncActionPayload { - let metaData = null; + let metaData: Parameters[2] | null = null; // Is the tag ordered manually? const store = RoomListStore.instance; @@ -81,7 +81,7 @@ export default class RoomListActions { return asyncAction( "RoomListActions.tagRoom", () => { - const promises = []; + const promises: Promise[] = []; const roomId = room.roomId; // Evil hack to get DMs behaving @@ -120,7 +120,7 @@ export default class RoomListActions { if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData)) { // metaData is the body of the PUT to set the tag, so it must // at least be an empty object. - metaData = metaData || {}; + metaData = metaData || ({} as typeof metaData); const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { logger.error("Failed to add tag " + newTag + " to room: " + err); diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index 63a132077f..f93e097a9d 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from "../../../../languageHandler"; import SdkConfig from "../../../../SdkConfig"; @@ -27,6 +28,7 @@ import Field from "../../../../components/views/elements/Field"; import BaseDialog from "../../../../components/views/dialogs/BaseDialog"; import DialogButtons from "../../../../components/views/elements/DialogButtons"; import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps"; +import { IIndexStats } from "../../../../indexing/BaseEventIndexManager"; interface IProps extends IDialogProps {} @@ -43,7 +45,7 @@ interface IState { * Allows the user to introspect the event index state and disable it. */ export default class ManageEventIndexDialog extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -56,9 +58,9 @@ export default class ManageEventIndexDialog extends React.Component => { + public updateCurrentRoom = async (room: Room): Promise => { const eventIndex = EventIndexPeg.get(); - let stats; + let stats: IIndexStats; try { stats = await eventIndex.getStats(); @@ -136,8 +138,8 @@ export default class ManageEventIndexDialog extends React.Component { - this.setState({ crawlerSleepTime: e.target.value }); + private onCrawlerSleepTimeChange = (e: ChangeEvent): void => { + this.setState({ crawlerSleepTime: parseInt(e.target.value, 10) }); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx index b595a60a2e..41b91fba1d 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.tsx @@ -20,7 +20,7 @@ import FileSaver from "file-saver"; import { logger } from "matrix-js-sdk/src/logger"; import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup"; import { TrustInfo } from "matrix-js-sdk/src/crypto/backup"; -import { CrossSigningKeys } from "matrix-js-sdk/src/matrix"; +import { CrossSigningKeys, UIAFlow } from "matrix-js-sdk/src/matrix"; import { IRecoveryKey } from "matrix-js-sdk/src/crypto/api"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; @@ -206,7 +206,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { return f.stages.length === 1 && f.stages[0] === "m.login.password"; }); this.setState({ diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx index c8a561e7da..7bfdf400cf 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import FileSaver from "file-saver"; -import React from "react"; +import React, { ChangeEvent } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; @@ -163,7 +163,9 @@ export default class ExportE2eKeysDialog extends React.Component this.onPassphraseChange(e, "passphrase1")} + onChange={(e: ChangeEvent) => + this.onPassphraseChange(e, "passphrase1") + } autoFocus={true} size={64} type="password" @@ -174,7 +176,9 @@ export default class ExportE2eKeysDialog extends React.Component this.onPassphraseChange(e, "passphrase2")} + onChange={(e: ChangeEvent) => + this.onPassphraseChange(e, "passphrase2") + } size={64} type="password" disabled={disableForm} diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx index 079271b021..e49c090a75 100644 --- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx +++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.tsx @@ -73,9 +73,9 @@ export default class ImportE2eKeysDialog extends React.Component } private onFormChange = (): void => { - const files = this.file.current.files || []; + const files = this.file.current.files; this.setState({ - enableSubmit: this.state.passphrase !== "" && files.length > 0, + enableSubmit: this.state.passphrase !== "" && !!files?.length, }); }; diff --git a/src/audio/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts index 0c0cc56cd6..3cb9cf03d2 100644 --- a/src/audio/RecorderWorklet.ts +++ b/src/audio/RecorderWorklet.ts @@ -43,7 +43,11 @@ class MxVoiceWorklet extends AudioWorkletProcessor { private nextAmplitudeSecond = 0; private amplitudeIndex = 0; - public process(inputs, outputs, parameters): boolean { + public process( + inputs: Float32Array[][], + outputs: Float32Array[][], + parameters: Record, + ): boolean { const currentSecond = roundTimeToTargetFreq(currentTime); // We special case the first ping because there's a fairly good chance that we'll miss the zeroth // update. Firefox for instance takes 0.06 seconds (roughly) to call this function for the first diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts index 32fcb5a97a..f8f9f01b66 100644 --- a/src/audio/VoiceRecording.ts +++ b/src/audio/VoiceRecording.ts @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// @ts-ignore import Recorder from "opus-recorder/dist/recorder.min.js"; import encoderPath from "opus-recorder/dist/encoderWorker.min.js"; import { SimpleObservable } from "matrix-widget-api"; diff --git a/src/audio/compat.ts b/src/audio/compat.ts index ab63f644a1..ce0fc30816 100644 --- a/src/audio/compat.ts +++ b/src/audio/compat.ts @@ -69,7 +69,7 @@ export function decodeOgg(audioBuffer: ArrayBuffer): Promise { command: "encode", buffers: ev.data, }, - ev.data.map((b) => b.buffer), + ev.data.map((b: Float32Array) => b.buffer), ); }; diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index cc25068db8..6b55dbf857 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -19,7 +19,7 @@ limitations under the License. */ import React from "react"; -import { uniq, sortBy } from "lodash"; +import { uniq, sortBy, ListIteratee } from "lodash"; import EMOTICON_REGEX from "emojibase-regex/emoticon"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -55,7 +55,11 @@ const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => { _orderBy: index, })); -function score(query: string, space: string): number { +function score(query: string, space: string[] | string): number { + if (Array.isArray(space)) { + return Math.min(...space.map((s) => score(query, s))); + } + const index = space.indexOf(query); if (index === -1) { return Infinity; @@ -113,7 +117,7 @@ export default class EmojiProvider extends AutocompleteProvider { // Do second match with shouldMatchWordsOnly in order to match against 'name' completions = completions.concat(this.nameMatcher.match(matchedString)); - let sorters = []; + let sorters: ListIteratee[] = []; // make sure that emoticons come first sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 65de4b1bb4..5e8d25f4c8 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -110,17 +110,14 @@ export default class UserProvider extends AutocompleteProvider { // lazy-load user list into matcher if (!this.users) this.makeUsers(); - let completions = []; const { command, range } = this.getCurrentCommand(rawQuery, selection, force); - if (!command) return completions; - - const fullMatch = command[0]; + const fullMatch = command?.[0]; // Don't search if the query is a single "@" if (fullMatch && fullMatch !== "@") { // Don't include the '@' in our search query - it's only used as a way to trigger completion const query = fullMatch.startsWith("@") ? fullMatch.substring(1) : fullMatch; - completions = this.matcher.match(query, limit).map((user) => { + return this.matcher.match(query, limit).map((user) => { const description = UserIdentifierCustomisations.getDisplayUserIdentifier(user.userId, { roomId: this.room.roomId, withDisplayName: true, @@ -143,7 +140,7 @@ export default class UserProvider extends AutocompleteProvider { }; }); } - return completions; + return []; } public getName(): string { @@ -152,7 +149,7 @@ export default class UserProvider extends AutocompleteProvider { private makeUsers(): void { const events = this.room.getLiveTimeline().getEvents(); - const lastSpoken = {}; + const lastSpoken: Record = {}; for (const event of events) { lastSpoken[event.getSender()] = event.getTs(); diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 978dd07be9..118e1d8d95 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -99,9 +99,9 @@ export interface IProps extends MenuProps { closeOnInteraction?: boolean; // Function to be called on menu close - onFinished(); + onFinished(): void; // on resize callback - windowResize?(); + windowResize?(): void; } interface IState { @@ -119,8 +119,8 @@ export default class ContextMenu extends React.PureComponent { managed: true, }; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { contextMenuElem: null, @@ -387,13 +387,13 @@ export default class ContextMenu extends React.PureComponent { menuStyle["paddingRight"] = menuPaddingRight; } - const wrapperStyle = {}; + const wrapperStyle: CSSProperties = {}; if (!isNaN(Number(zIndex))) { menuStyle["zIndex"] = zIndex + 1; wrapperStyle["zIndex"] = zIndex; } - let background; + let background: JSX.Element; if (hasBackground) { background = (
, ): { close: (...args: any[]) => void } { - const onFinished = function (...args): void { + const onFinished = function (...args: any[]): void { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); props?.onFinished?.apply(null, args); }; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 4390dcf36e..1f6c1d2583 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -43,7 +43,7 @@ interface IProps { } interface IState { - timelineSet: EventTimelineSet; + timelineSet: EventTimelineSet | null; narrow: boolean; } @@ -59,7 +59,7 @@ class FilePanel extends React.Component { public noRoom: boolean; private card = createRef(); - public state = { + public state: IState = { timelineSet: null, narrow: false, }; diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 6b727c5140..98dfbf0851 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -79,6 +79,12 @@ export function GenericDropdownMenuGroup({ ); } +function isGenericDropdownMenuGroupArray( + items: readonly GenericDropdownMenuItem[], +): items is GenericDropdownMenuGroup[] { + return isGenericDropdownMenuGroup(items[0]); +} + function isGenericDropdownMenuGroup(item: GenericDropdownMenuItem): item is GenericDropdownMenuGroup { return "options" in item; } @@ -123,19 +129,19 @@ export function GenericDropdownMenu({ .flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it])) .find((option) => (toKey ? toKey(option.key) === toKey(value) : option.key === value)); let contextMenuOptions: JSX.Element; - if (options && isGenericDropdownMenuGroup(options[0])) { + if (options && isGenericDropdownMenuGroupArray(options)) { contextMenuOptions = ( <> {options.map((group) => ( {group.options.map((option) => ( { @@ -156,7 +162,7 @@ export function GenericDropdownMenu({ <> {options.map((option) => ( { diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 99be8705a4..f11b239052 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -80,7 +80,7 @@ interface IProps { // Called when the stage changes, or the stage's phase changes. First // argument is the stage, second is the phase. Some stages do not have // phases and will be counted as 0 (numeric). - onStagePhaseChange?(stage: string, phase: string | number): void; + onStagePhaseChange?(stage: AuthType, phase: number): void; } interface IState { @@ -99,7 +99,7 @@ export default class InteractiveAuthComponent extends React.Component { private listContainerRef = createRef(); private roomListRef = createRef(); - private focusedElement = null; + private focusedElement: Element = null; private isDoingStickyHeaders = false; public constructor(props: IProps) { diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 242bbdc028..1cffd6b6f4 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -136,8 +136,8 @@ class LoggedInView extends React.Component { protected backgroundImageWatcherRef: string; protected resizer: Resizer; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { syncErrorData: undefined, @@ -229,8 +229,8 @@ class LoggedInView extends React.Component { }; private createResizer(): Resizer { - let panelSize; - let panelCollapsed; + let panelSize: number; + let panelCollapsed: boolean; const collapseConfig: ICollapseConfig = { // TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel toggleSize: 206 - 50, @@ -341,7 +341,7 @@ class LoggedInView extends React.Component { const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; if (!serverNoticeList) return; - const events = []; + const events: MatrixEvent[] = []; let pinnedEventTs = 0; for (const room of serverNoticeList) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); @@ -369,7 +369,7 @@ class LoggedInView extends React.Component { e.getContent()["server_notice_type"] === "m.server_notice.usage_limit_reached" ); }); - const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent(); + const usageLimitEventContent = usageLimitEvent?.getContent(); this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent); this.setState({ usageLimitEventContent, @@ -422,13 +422,13 @@ class LoggedInView extends React.Component { We also listen with a native listener on the document to get keydown events when no element is focused. Bubbling is irrelevant here as the target is the body element. */ - private onReactKeyDown = (ev): void => { + private onReactKeyDown = (ev: React.KeyboardEvent): void => { // events caught while bubbling up on the root element // of this component, so something must be focused. this.onKeyDown(ev); }; - private onNativeKeyDown = (ev): void => { + private onNativeKeyDown = (ev: KeyboardEvent): void => { // only pass this if there is no focused element. // if there is, onKeyDown will be called by the // react keydown handler that respects the react bubbling order. @@ -437,7 +437,7 @@ class LoggedInView extends React.Component { } }; - private onKeyDown = (ev): void => { + private onKeyDown = (ev: React.KeyboardEvent | KeyboardEvent): void => { let handled = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); @@ -571,7 +571,7 @@ class LoggedInView extends React.Component { ) { dis.dispatch({ action: Action.SwitchSpace, - num: ev.code.slice(5), // Cut off the first 5 characters - "Digit" + num: parseInt(ev.code.slice(5), 10), // Cut off the first 5 characters - "Digit" }); handled = true; } @@ -615,10 +615,8 @@ class LoggedInView extends React.Component { * dispatch a page-up/page-down/etc to the appropriate component * @param {Object} ev The key event */ - private onScrollKeyPressed = (ev): void => { - if (this._roomView.current) { - this._roomView.current.handleScrollKey(ev); - } + private onScrollKeyPressed = (ev: React.KeyboardEvent | KeyboardEvent): void => { + this._roomView.current?.handleScrollKey(ev); }; public render(): JSX.Element { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7fdda93eb6..8bc29eb33a 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -419,7 +419,7 @@ export default class MatrixChat extends React.PureComponent { window.addEventListener("resize", this.onWindowResized); } - public componentDidUpdate(prevProps, prevState): void { + public componentDidUpdate(prevProps: IProps, prevState: IState): void { if (this.shouldTrackPageChange(prevState, this.state)) { const durationMs = this.stopPageChangeTimer(); PosthogTrackers.instance.trackPageChange(this.state.view, this.state.page_type, durationMs); @@ -544,12 +544,11 @@ export default class MatrixChat extends React.PureComponent { if (state.view === undefined) { throw new Error("setStateForNewView with no view!"); } - const newState = { - currentUserId: null, + this.setState({ + currentUserId: undefined, justRegistered: false, - }; - Object.assign(newState, state); - this.setState(newState); + ...state, + } as IState); } private onAction = (payload: ActionPayload): void => { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 2dd432cb92..c5dc249199 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, KeyboardEvent, ReactNode, TransitionEvent } from "react"; +import React, { createRef, ReactNode, TransitionEvent } from "react"; import ReactDOM from "react-dom"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; @@ -34,7 +34,7 @@ import SettingsStore from "../../settings/SettingsStore"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; -import EventTile, { UnwrappedEventTile, GetRelationsForEvent, IReadReceiptProps } from "../views/rooms/EventTile"; +import EventTile, { GetRelationsForEvent, IReadReceiptProps, UnwrappedEventTile } from "../views/rooms/EventTile"; import { hasText } from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; @@ -272,7 +272,7 @@ export default class MessagePanel extends React.Component { // A map to allow groupers to maintain consistent keys even if their first event is uprooted due to back-pagination. public grouperKeyMap = new WeakMap(); - public constructor(props, context) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { @@ -308,7 +308,7 @@ export default class MessagePanel extends React.Component { SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } - public componentDidUpdate(prevProps, prevState): void { + public componentDidUpdate(prevProps: IProps, prevState: IState): void { if (prevProps.layout !== this.props.layout) { this.calculateRoomMembersCount(); } @@ -410,17 +410,13 @@ export default class MessagePanel extends React.Component { /* jump to the top of the content. */ public scrollToTop(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToTop(); - } + this.scrollPanel.current?.scrollToTop(); } /* jump to the bottom of the content. */ public scrollToBottom(): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToBottom(); - } + this.scrollPanel.current?.scrollToBottom(); } /** @@ -428,10 +424,8 @@ export default class MessagePanel extends React.Component { * * @param {KeyboardEvent} ev: the keyboard event to handle */ - public handleScrollKey(ev: KeyboardEvent): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.handleScrollKey(ev); - } + public handleScrollKey(ev: React.KeyboardEvent | KeyboardEvent): void { + this.scrollPanel.current?.handleScrollKey(ev); } /* jump to the given event id. @@ -752,7 +746,7 @@ export default class MessagePanel extends React.Component { const readReceipts = this.readReceiptsByEvent[eventId]; let isLastSuccessful = false; - const isSentState = (s): boolean => !s || s === "sent"; + const isSentState = (s: EventStatus): boolean => !s || s === EventStatus.SENT; const isSent = isSentState(mxEv.getAssociatedStatus()); const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { @@ -869,8 +863,14 @@ export default class MessagePanel extends React.Component { // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. private getReadReceiptsByShownEvent(): Record { - const receiptsByEvent = {}; - const receiptsByUserId = {}; + const receiptsByEvent: Record = {}; + const receiptsByUserId: Record< + string, + { + lastShownEventId: string; + receipt: IReadReceiptProps; + } + > = {}; let lastShownEventId; for (const event of this.props.events) { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 813522ffcb..508812ae16 100644 --- a/src/components/structures/NonUrgentToastContainer.tsx +++ b/src/components/structures/NonUrgentToastContainer.tsx @@ -27,8 +27,8 @@ interface IState { } export default class NonUrgentToastContainer extends React.PureComponent { - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { toasts: NonUrgentToastStore.instance.components, diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index ac351399d4..7a72ea67f7 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -43,7 +43,7 @@ export default class NotificationPanel extends React.PureComponent(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 3748ee0ec7..60138dd442 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -63,7 +63,7 @@ export default class RightPanel extends React.Component { public static contextType = MatrixClientContext; public context!: React.ContextType; - public constructor(props, context) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index f370091a8a..3a4af77441 100644 --- a/src/components/structures/RoomStatusBar.tsx +++ b/src/components/structures/RoomStatusBar.tsx @@ -14,10 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SyncState, ISyncStateData } from "matrix-js-sdk/src/sync"; import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixError } from "matrix-js-sdk/src/matrix"; import { _t, _td } from "../../languageHandler"; import Resend from "../../Resend"; @@ -192,10 +193,10 @@ export default class RoomStatusBar extends React.PureComponent { private getUnsentMessageContent(): JSX.Element { const unsentMessages = this.state.unsentMessages; - let title; + let title: ReactNode; - let consentError = null; - let resourceLimitError = null; + let consentError: MatrixError | null = null; + let resourceLimitError: MatrixError | null = null; for (const m of unsentMessages) { if (m.error && m.error.errcode === "M_CONSENT_NOT_GIVEN") { consentError = m.error; diff --git a/src/components/structures/RoomStatusBarUnsentMessages.tsx b/src/components/structures/RoomStatusBarUnsentMessages.tsx index 3ce300f0eb..38dbae281e 100644 --- a/src/components/structures/RoomStatusBarUnsentMessages.tsx +++ b/src/components/structures/RoomStatusBarUnsentMessages.tsx @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement } from "react"; +import React, { ReactElement, ReactNode } from "react"; import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../views/rooms/NotificationBadge"; interface RoomStatusBarUnsentMessagesProps { - title: string; + title: ReactNode; description?: string; notificationState: StaticNotificationState; buttons: ReactElement; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 221d2151ef..6e2b72cf9f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -33,6 +33,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { HistoryVisibility } from "matrix-js-sdk/src/@types/partials"; import { ISearchResults } from "matrix-js-sdk/src/@types/search"; +import { IRoomTimelineData } from "matrix-js-sdk/src/models/event-timeline-set"; import shouldHideEvent from "../../shouldHideEvent"; import { _t } from "../../languageHandler"; @@ -49,7 +50,7 @@ import RoomScrollStateStore, { ScrollState } from "../../stores/RoomScrollStateS import WidgetEchoStore from "../../stores/WidgetEchoStore"; import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { Action } from "../../dispatcher/actions"; @@ -856,7 +857,7 @@ export class RoomView extends React.Component { window.addEventListener("beforeunload", this.onPageUnload); } - public shouldComponentUpdate(nextProps, nextState): boolean { + public shouldComponentUpdate(nextProps: IRoomProps, nextState: IRoomState): boolean { const hasPropsDiff = objectHasDiff(this.props, nextProps); const { upgradeRecommendation, ...state } = this.state; @@ -958,7 +959,7 @@ export class RoomView extends React.Component { }); }; - private onPageUnload = (event): string => { + private onPageUnload = (event: BeforeUnloadEvent): string => { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return (event.returnValue = _t("You seem to be uploading files, are you sure you want to quit?")); } else if (this.getCallForRoom() && this.state.callState !== "ended") { @@ -966,7 +967,7 @@ export class RoomView extends React.Component { } }; - private onReactKeyDown = (ev): void => { + private onReactKeyDown = (ev: React.KeyboardEvent): void => { let handled = false; const action = getKeyBindingsManager().getRoomAction(ev); @@ -1130,7 +1131,13 @@ export class RoomView extends React.Component { createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } - private onRoomTimeline = (ev: MatrixEvent, room: Room | null, toStartOfTimeline: boolean, removed, data): void => { + private onRoomTimeline = ( + ev: MatrixEvent, + room: Room | null, + toStartOfTimeline: boolean, + removed: boolean, + data?: IRoomTimelineData, + ): void => { if (this.unmounted) return; // ignore events for other rooms or the notification timeline set @@ -1150,7 +1157,7 @@ export class RoomView extends React.Component { // ignore anything but real-time updates at the end of the room: // updates from pagination will happen when the paginate completes. - if (toStartOfTimeline || !data || !data.liveEvent) return; + if (toStartOfTimeline || !data?.liveEvent) return; // no point handling anything while we're waiting for the join to finish: // we'll only be showing a spinner. @@ -1702,7 +1709,7 @@ export class RoomView extends React.Component { }; // update the read marker to match the read-receipt - private forgetReadMarker = (ev): void => { + private forgetReadMarker = (ev: ButtonEvent): void => { ev.stopPropagation(); this.messagePanel.forgetReadMarker(); }; @@ -1775,7 +1782,7 @@ export class RoomView extends React.Component { * * We pass it down to the scroll panel. */ - public handleScrollKey = (ev): void => { + public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { let panel: ScrollPanel | TimelinePanel; if (this.searchResultsPanel.current) { panel = this.searchResultsPanel.current; @@ -1798,7 +1805,7 @@ export class RoomView extends React.Component { // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. - private gatherTimelinePanelRef = (r): void => { + private gatherTimelinePanelRef = (r?: TimelinePanel): void => { this.messagePanel = r; }; diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index f51cba66a3..7779f97a54 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, CSSProperties, ReactNode, KeyboardEvent } from "react"; +import React, { createRef, CSSProperties, ReactNode } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import SettingsStore from "../../settings/SettingsStore"; @@ -195,8 +195,8 @@ export default class ScrollPanel extends React.Component { private heightUpdateInProgress: boolean; private divScroll: HTMLDivElement; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.props.resizeNotifier?.on("middlePanelResizedNoisy", this.onResize); @@ -440,9 +440,9 @@ export default class ScrollPanel extends React.Component { // pagination. // // If backwards is true, we unpaginate (remove) tiles from the back (top). - let tile; + let tile: HTMLElement; for (let i = 0; i < tiles.length; i++) { - tile = tiles[backwards ? i : tiles.length - 1 - i]; + tile = tiles[backwards ? i : tiles.length - 1 - i] as HTMLElement; // Subtract height of tile as if it were unpaginated excessHeight -= tile.clientHeight; //If removing the tile would lead to future pagination, break before setting scroll token @@ -587,7 +587,7 @@ export default class ScrollPanel extends React.Component { * Scroll up/down in response to a scroll key * @param {object} ev the keyboard event */ - public handleScrollKey = (ev: KeyboardEvent): void => { + public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { case KeyBindingAction.ScrollUp: diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index 60a80bc25f..03401119b0 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -280,7 +280,7 @@ const Tile: React.FC = ({ ); if (showChildren) { - const onChildrenKeyDown = (e): void => { + const onChildrenKeyDown = (e: React.KeyboardEvent): void => { const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { case KeyBindingAction.ArrowLeft: diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 96ff90936d..6d28106ddb 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -318,7 +318,7 @@ const SpaceSetupFirstRooms: React.FC<{ label={_t("Room name")} placeholder={placeholders[i]} value={roomNames[i]} - onChange={(ev) => setRoomName(i, ev.target.value)} + onChange={(ev: React.ChangeEvent) => setRoomName(i, ev.target.value)} autoFocus={i === 2} disabled={busy} autoComplete="off" diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 8d553b5549..ae93080937 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -137,7 +137,7 @@ export default class ThreadView extends React.Component { }); } - public componentDidUpdate(prevProps): void { + public componentDidUpdate(prevProps: IProps): void { if (prevProps.mxEvent !== this.props.mxEvent) { this.setupThread(this.props.mxEvent); } @@ -316,7 +316,7 @@ export default class ThreadView extends React.Component { }; private get threadRelation(): IEventRelation { - const relation = { + const relation: IEventRelation = { rel_type: THREAD_RELATION_TYPE.name, event_id: this.state.thread?.id, is_falling_back: true, diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index d0f0f9e876..f49a3ea896 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1316,7 +1316,7 @@ class TimelinePanel extends React.Component { * * We pass it down to the scroll panel. */ - public handleScrollKey = (ev: React.KeyboardEvent): void => { + public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => { if (!this.messagePanel.current) return; // jump to the live timeline on ctrl-end, rather than the end of the @@ -1977,9 +1977,9 @@ class TimelinePanel extends React.Component { * * @return An event ID list for every timeline in every timelineSet */ -function serializeEventIdsFromTimelineSets(timelineSets): { [key: string]: string[] }[] { +function serializeEventIdsFromTimelineSets(timelineSets: EventTimelineSet[]): { [key: string]: string[] }[] { const serializedEventIdsInTimelineSet = timelineSets.map((timelineSet) => { - const timelineMap = {}; + const timelineMap: Record = {}; const timelines = timelineSet.getTimelines(); const liveTimeline = timelineSet.getLiveTimeline(); diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index c4d121d12a..378c33b513 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -25,8 +25,8 @@ interface IState { } export default class ToastContainer extends React.Component<{}, IState> { - public constructor(props, context) { - super(props, context); + public constructor(props: {}) { + super(props); this.state = { toasts: ToastStore.sharedInstance().getToasts(), countSeen: ToastStore.sharedInstance().getCountSeen(), diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index 9d256b6aa5..c928c42ec4 100644 --- a/src/components/structures/UploadBar.tsx +++ b/src/components/structures/UploadBar.tsx @@ -57,7 +57,7 @@ export default class UploadBar extends React.PureComponent { private dispatcherRef: Optional; private mounted = false; - public constructor(props) { + public constructor(props: IProps) { super(props); // Set initial state to any available upload in this room - we might be mounting diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 4cbe0f5bc6..b0303245f9 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -37,7 +37,7 @@ import SSOButtons from "../../views/elements/SSOButtons"; import ServerPicker from "../../views/elements/ServerPicker"; import AuthBody from "../../views/auth/AuthBody"; import AuthHeader from "../../views/auth/AuthHeader"; -import AccessibleButton from "../../views/elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; // These are used in several places, and come from the js-sdk's autodiscovery @@ -101,6 +101,11 @@ interface IState { serverDeadError?: ReactNode; } +type OnPasswordLogin = { + (username: string, phoneCountry: undefined, phoneNumber: undefined, password: string): Promise; + (username: undefined, phoneCountry: string, phoneNumber: string, password: string): Promise; +}; + /* * A wire component which glues together login UI components and Login logic */ @@ -110,7 +115,7 @@ export default class LoginComponent extends React.PureComponent private readonly stepRendererMap: Record ReactNode>; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -152,7 +157,7 @@ export default class LoginComponent extends React.PureComponent this.unmounted = true; } - public componentDidUpdate(prevProps): void { + public componentDidUpdate(prevProps: IProps): void { if ( prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl || prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl @@ -164,7 +169,12 @@ export default class LoginComponent extends React.PureComponent public isBusy = (): boolean => this.state.busy || this.props.busy; - public onPasswordLogin = async (username, phoneCountry, phoneNumber, password): Promise => { + public onPasswordLogin: OnPasswordLogin = async ( + username: string | undefined, + phoneCountry: string | undefined, + phoneNumber: string | undefined, + password: string, + ): Promise => { if (!this.state.serverIsAlive) { this.setState({ busy: true }); // Do a quick liveliness check on the URLs @@ -207,10 +217,10 @@ export default class LoginComponent extends React.PureComponent if (this.unmounted) { return; } - let errorText; + let errorText: ReactNode; // Some error strings only apply for logging in - const usingEmail = username.indexOf("@") > 0; + const usingEmail = username?.indexOf("@") > 0; if (error.httpStatus === 400 && usingEmail) { errorText = _t("This homeserver does not support login using email address."); } else if (error.errcode === "M_RESOURCE_LIMIT_EXCEEDED") { @@ -264,11 +274,11 @@ export default class LoginComponent extends React.PureComponent ); }; - public onUsernameChanged = (username): void => { - this.setState({ username: username }); + public onUsernameChanged = (username: string): void => { + this.setState({ username }); }; - public onUsernameBlur = async (username): Promise => { + public onUsernameBlur = async (username: string): Promise => { const doWellknownLookup = username[0] === "@"; this.setState({ username: username, @@ -315,23 +325,21 @@ export default class LoginComponent extends React.PureComponent } }; - public onPhoneCountryChanged = (phoneCountry): void => { - this.setState({ phoneCountry: phoneCountry }); + public onPhoneCountryChanged = (phoneCountry: string): void => { + this.setState({ phoneCountry }); }; - public onPhoneNumberChanged = (phoneNumber): void => { - this.setState({ - phoneNumber: phoneNumber, - }); + public onPhoneNumberChanged = (phoneNumber: string): void => { + this.setState({ phoneNumber }); }; - public onRegisterClick = (ev): void => { + public onRegisterClick = (ev: ButtonEvent): void => { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); }; - public onTryRegisterClick = (ev): void => { + public onTryRegisterClick = (ev: ButtonEvent): void => { const hasPasswordFlow = this.state.flows?.find((flow) => flow.type === "m.login.password"); const ssoFlow = this.state.flows?.find((flow) => flow.type === "m.login.sso" || flow.type === "m.login.cas"); // If has no password flow but an SSO flow guess that the user wants to register with SSO. @@ -540,7 +548,7 @@ export default class LoginComponent extends React.PureComponent ); }; - private renderSsoStep = (loginType): JSX.Element => { + private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => { const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; return ( diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index aac39334a0..7cfebc6d0a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -14,9 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { AuthType, createClient, IAuthData, IInputs } from "matrix-js-sdk/src/matrix"; +import { AuthType, createClient, IAuthData, IInputs, MatrixError } from "matrix-js-sdk/src/matrix"; import React, { Fragment, ReactNode } from "react"; -import { IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client"; +import { IRegisterRequestParams, IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; @@ -125,7 +125,7 @@ export default class Registration extends React.Component { // `replaceClient` tracks latest serverConfig to spot when it changes under the async method which fetches flows private latestServerConfig: ValidatedServerConfig; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -166,7 +166,7 @@ export default class Registration extends React.Component { } }; - public componentDidUpdate(prevProps): void { + public componentDidUpdate(prevProps: IProps): void { if ( prevProps.serverConfig.hsUrl !== this.props.serverConfig.hsUrl || prevProps.serverConfig.isUrl !== this.props.serverConfig.isUrl @@ -307,7 +307,7 @@ export default class Registration extends React.Component { if (!success) { let errorText: ReactNode = (response as Error).message || (response as Error).toString(); // can we give a better error message? - if (response.errcode === "M_RESOURCE_LIMIT_EXCEEDED") { + if (response instanceof MatrixError && response.errcode === "M_RESOURCE_LIMIT_EXCEEDED") { const errorTop = messageForResourceLimitError(response.data.limit_type, response.data.admin_contact, { "monthly_active_user": _td("This homeserver has hit its Monthly Active User limit."), "hs_blocked": _td("This homeserver has been blocked by its administrator."), @@ -326,17 +326,17 @@ export default class Registration extends React.Component {

{errorDetail}

); - } else if (response.required_stages && response.required_stages.includes(AuthType.Msisdn)) { + } else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) { let msisdnAvailable = false; - for (const flow of response.available_flows) { + for (const flow of (response as IAuthData).available_flows) { msisdnAvailable = msisdnAvailable || flow.stages.includes(AuthType.Msisdn); } if (!msisdnAvailable) { errorText = _t("This server does not support authentication with a phone number."); } - } else if (response.errcode === "M_USER_IN_USE") { + } else if (response instanceof MatrixError && response.errcode === "M_USER_IN_USE") { errorText = _t("Someone already has that username, please try another."); - } else if (response.errcode === "M_THREEPID_IN_USE") { + } else if (response instanceof MatrixError && response.errcode === "M_THREEPID_IN_USE") { errorText = _t("That e-mail address or phone number is already in use."); } @@ -348,11 +348,11 @@ export default class Registration extends React.Component { return; } - MatrixClientPeg.setJustRegisteredUserId(response.user_id); + MatrixClientPeg.setJustRegisteredUserId((response as IAuthData).user_id); - const newState = { + const newState: Partial = { doingUIAuth: false, - registeredUsername: response.user_id, + registeredUsername: (response as IAuthData).user_id, differentLoggedInUserId: null, completedNoSignin: false, // we're still busy until we get unmounted: don't show the registration form again @@ -365,8 +365,10 @@ export default class Registration extends React.Component { // starting the registration process. This isn't perfect since it's possible // the user had a separate guest session they didn't actually mean to replace. const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner(); - if (sessionOwner && !sessionIsGuest && sessionOwner !== response.user_id) { - logger.log(`Found a session for ${sessionOwner} but ${response.user_id} has just registered.`); + if (sessionOwner && !sessionIsGuest && sessionOwner !== (response as IAuthData).user_id) { + logger.log( + `Found a session for ${sessionOwner} but ${(response as IAuthData).user_id} has just registered.`, + ); newState.differentLoggedInUserId = sessionOwner; } @@ -383,7 +385,7 @@ export default class Registration extends React.Component { // as the client that started registration may be gone by the time we've verified the email, and only the client // that verified the email is guaranteed to exist, we'll always do the login in that client. const hasEmail = Boolean(this.state.formVals.email); - const hasAccessToken = Boolean(response.access_token); + const hasAccessToken = Boolean((response as IAuthData).access_token); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { @@ -391,11 +393,11 @@ export default class Registration extends React.Component { // the email, not the client that started the registration flow await this.props.onLoggedIn( { - userId: response.user_id, - deviceId: response.device_id, + userId: (response as IAuthData).user_id, + deviceId: (response as IAuthData).device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken: response.access_token, + accessToken: (response as IAuthData).access_token, }, this.state.formVals.password, ); @@ -406,7 +408,7 @@ export default class Registration extends React.Component { newState.completedNoSignin = true; } - this.setState(newState); + this.setState(newState as IState); }; private setupPushers(): Promise { @@ -455,7 +457,7 @@ export default class Registration extends React.Component { }; private makeRegisterRequest = (auth: IAuthData | null): Promise => { - const registerParams = { + const registerParams: IRegisterRequestParams = { username: this.state.formVals.username, password: this.state.formVals.password, initial_device_display_name: this.props.defaultDeviceDisplayName, diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 8102cbcfab..4eec31a226 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -45,7 +45,7 @@ interface IState { } export default class SetupEncryptionBody extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); const store = SetupEncryptionStore.sharedInstance(); store.on("update", this.onStoreUpdate); diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 253e89aa04..e3cb98bd86 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +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"; @@ -44,7 +44,7 @@ enum LoginView { Unsupported, } -const STATIC_FLOWS_TO_VIEWS = { +const STATIC_FLOWS_TO_VIEWS: Record = { "m.login.password": LoginView.Password, "m.login.cas": LoginView.CAS, "m.login.sso": LoginView.SSO, @@ -133,7 +133,7 @@ export default class SoftLogout extends React.Component { this.setState({ flows, loginView: chosenView }); } - private onPasswordChange = (ev): void => { + private onPasswordChange = (ev: ChangeEvent): void => { this.setState({ password: ev.target.value }); }; @@ -141,7 +141,7 @@ export default class SoftLogout extends React.Component { dis.dispatch({ action: "start_password_recovery" }); }; - private onPasswordLogin = async (ev): Promise => { + private onPasswordLogin = async (ev: SyntheticEvent): Promise => { ev.preventDefault(); ev.stopPropagation(); diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 975fb79a36..805c8f4f83 100644 --- a/src/components/views/audio_messages/DurationClock.tsx +++ b/src/components/views/audio_messages/DurationClock.tsx @@ -31,7 +31,7 @@ interface IState { * A clock which shows a clip's maximum duration. */ export default class DurationClock extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index 1a3f30b108..50dd272a0d 100644 --- a/src/components/views/audio_messages/LiveRecordingClock.tsx +++ b/src/components/views/audio_messages/LiveRecordingClock.tsx @@ -34,12 +34,12 @@ interface IState { */ export default class LiveRecordingClock extends React.PureComponent { private seconds = 0; - private scheduledUpdate = new MarkedExecution( + private scheduledUpdate: MarkedExecution = new MarkedExecution( () => this.updateClock(), () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), ); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { seconds: 0, diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 36ca2b4e13..050a01a21e 100644 --- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx @@ -39,12 +39,12 @@ export default class LiveRecordingWaveform extends React.PureComponent this.updateWaveform(), () => requestAnimationFrame(() => this.scheduledUpdate.trigger()), ); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { waveform: arraySeed(0, RECORDING_PLAYBACK_SAMPLES), diff --git a/src/components/views/audio_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx index eb31c01c33..a865f0aeef 100644 --- a/src/components/views/audio_messages/PlayPauseButton.tsx +++ b/src/components/views/audio_messages/PlayPauseButton.tsx @@ -35,7 +35,7 @@ interface IProps extends Omit { - public constructor(props) { + public constructor(props: IProps) { super(props); } diff --git a/src/components/views/audio_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx index e225bf1150..c5e569ce92 100644 --- a/src/components/views/audio_messages/PlaybackClock.tsx +++ b/src/components/views/audio_messages/PlaybackClock.tsx @@ -39,7 +39,7 @@ interface IState { * A clock for a playback of a recording. */ export default class PlaybackClock extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index fe86ef3326..32c34db6b2 100644 --- a/src/components/views/audio_messages/PlaybackWaveform.tsx +++ b/src/components/views/audio_messages/PlaybackWaveform.tsx @@ -34,7 +34,7 @@ interface IState { * A waveform which shows the waveform of a previously recorded recording */ export default class PlaybackWaveform extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx index 85fef77278..4af84325e5 100644 --- a/src/components/views/audio_messages/SeekBar.tsx +++ b/src/components/views/audio_messages/SeekBar.tsx @@ -46,7 +46,7 @@ export default class SeekBar extends React.PureComponent { // We use an animation frame request to avoid overly spamming prop updates, even if we aren't // really using anything demanding on the CSS front. - private animationFrameFn = new MarkedExecution( + private animationFrameFn: MarkedExecution = new MarkedExecution( () => this.doUpdate(), () => requestAnimationFrame(() => this.animationFrameFn.trigger()), ); diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index ae155696c0..4b4396ebb5 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -21,7 +21,7 @@ import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; -const COUNTRIES_BY_ISO2 = {}; +const COUNTRIES_BY_ISO2: Record = {}; for (const c of COUNTRIES) { COUNTRIES_BY_ISO2[c.iso2] = c; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 4a995e4d06..2d11ff3fab 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -100,7 +100,7 @@ interface IPasswordAuthEntryState { export class PasswordAuthEntry extends React.Component { public static LOGIN_TYPE = AuthType.Password; - public constructor(props) { + public constructor(props: IAuthEntryProps) { super(props); this.state = { @@ -264,7 +264,7 @@ interface ITermsAuthEntryState { export class TermsAuthEntry extends React.Component { public static LOGIN_TYPE = AuthType.Terms; - public constructor(props) { + public constructor(props: ITermsAuthEntryProps) { super(props); // example stageParams: @@ -288,8 +288,12 @@ export class TermsAuthEntry extends React.Component = {}; + const pickedPolicies: { + id: string; + name: string; + url: string; + }[] = []; for (const policyId of Object.keys(allPolicies)) { const policy = allPolicies[policyId]; @@ -325,7 +329,7 @@ export class TermsAuthEntry extends React.Component = {}; for (const policy of this.state.policies) { let checked = this.state.toggledPolicies[policy.id]; if (policy.id === policyId) checked = !checked; @@ -484,7 +488,7 @@ export class EmailIdentityAuthEntry extends React.Component< { a: (text: string) => ( - null} disabled> + {text} @@ -555,7 +559,7 @@ export class MsisdnAuthEntry extends React.Component { private popupWindow: Window; private fallbackButton = createRef(); - public constructor(props) { + public constructor(props: IAuthEntryProps) { super(props); // we have to make the user click a button, as browsers will block diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 74e69c96b7..4ca07323f7 100644 --- a/src/components/views/auth/LoginWithQR.tsx +++ b/src/components/views/auth/LoginWithQR.tsx @@ -75,7 +75,7 @@ interface IState { * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ export default class LoginWithQR extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/auth/LoginWithQRFlow.tsx b/src/components/views/auth/LoginWithQRFlow.tsx index be79d8d2f3..b84250c97e 100644 --- a/src/components/views/auth/LoginWithQRFlow.tsx +++ b/src/components/views/auth/LoginWithQRFlow.tsx @@ -41,7 +41,7 @@ interface IProps { * This uses the unstable feature of MSC3906: https://github.com/matrix-org/matrix-spec-proposals/pull/3906 */ export default class LoginWithQRFlow extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); } diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index 36b6bdb7fb..1d002310fa 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -30,8 +30,8 @@ interface IProps extends Omit { labelRequired?: string; labelInvalid?: string; - onChange(ev: React.FormEvent); - onValidate?(result: IValidationResult); + onChange(ev: React.FormEvent): void; + onValidate?(result: IValidationResult): void; } class PassphraseConfirmField extends PureComponent { diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index f8e82d68bf..f9cbc8fe06 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -36,8 +36,8 @@ interface IProps extends Omit { labelStrongPassword?: string; labelAllowedButUnsafe?: string; - onChange(ev: React.FormEvent); - onValidate?(result: IValidationResult); + onChange(ev: React.FormEvent): void; + onValidate?(result: IValidationResult): void; } class PassphraseField extends PureComponent { diff --git a/src/components/views/auth/PasswordLogin.tsx b/src/components/views/auth/PasswordLogin.tsx index 164860ab41..3100b36a83 100644 --- a/src/components/views/auth/PasswordLogin.tsx +++ b/src/components/views/auth/PasswordLogin.tsx @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { SyntheticEvent } from "react"; import classNames from "classnames"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; -import AccessibleButton from "../elements/AccessibleButton"; -import withValidation, { IValidationResult } from "../elements/Validation"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; +import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import Field from "../elements/Field"; import CountryDropdown from "./CountryDropdown"; import EmailField from "./EmailField"; +import { PhoneNumberCountryDefinition } from "../../../phonenumber"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -51,7 +52,7 @@ interface IProps { interface IState { fieldValid: Partial>; loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone; - password: ""; + password: string; } const enum LoginField { @@ -66,6 +67,10 @@ const enum LoginField { * The email/username/phone fields are fully-controlled, the password field is not. */ export default class PasswordLogin extends React.PureComponent { + private [LoginField.Email]: Field; + private [LoginField.Phone]: Field; + private [LoginField.MatrixId]: Field; + public static defaultProps = { onUsernameChanged: function () {}, onUsernameBlur: function () {}, @@ -75,7 +80,7 @@ export default class PasswordLogin extends React.PureComponent { disableSubmit: false, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { // Field error codes by field ID @@ -85,13 +90,13 @@ export default class PasswordLogin extends React.PureComponent { }; } - private onForgotPasswordClick = (ev): void => { + private onForgotPasswordClick = (ev: ButtonEvent): void => { ev.preventDefault(); ev.stopPropagation(); this.props.onForgotPasswordClick(); }; - private onSubmitForm = async (ev): Promise => { + private onSubmitForm = async (ev: SyntheticEvent): Promise => { ev.preventDefault(); const allFieldsValid = await this.verifyFieldsBeforeSubmit(); @@ -99,47 +104,40 @@ export default class PasswordLogin extends React.PureComponent { return; } - let username = ""; // XXX: Synapse breaks if you send null here: - let phoneCountry = null; - let phoneNumber = null; - switch (this.state.loginType) { case LoginField.Email: case LoginField.MatrixId: - username = this.props.username; + this.props.onSubmit(this.props.username, undefined, undefined, this.state.password); break; case LoginField.Phone: - phoneCountry = this.props.phoneCountry; - phoneNumber = this.props.phoneNumber; + this.props.onSubmit(undefined, this.props.phoneCountry, this.props.phoneNumber, this.state.password); break; } - - this.props.onSubmit(username, phoneCountry, phoneNumber, this.state.password); }; - private onUsernameChanged = (ev): void => { + private onUsernameChanged = (ev: React.ChangeEvent): void => { this.props.onUsernameChanged(ev.target.value); }; - private onUsernameBlur = (ev): void => { + private onUsernameBlur = (ev: React.FocusEvent): void => { this.props.onUsernameBlur(ev.target.value); }; - private onLoginTypeChange = (ev): void => { - const loginType = ev.target.value; + private onLoginTypeChange = (ev: React.ChangeEvent): void => { + const loginType = ev.target.value as IState["loginType"]; this.setState({ loginType }); this.props.onUsernameChanged(""); // Reset because email and username use the same state }; - private onPhoneCountryChanged = (country): void => { + private onPhoneCountryChanged = (country: PhoneNumberCountryDefinition): void => { this.props.onPhoneCountryChanged(country.iso2); }; - private onPhoneNumberChanged = (ev): void => { + private onPhoneNumberChanged = (ev: React.ChangeEvent): void => { this.props.onPhoneNumberChanged(ev.target.value); }; - private onPasswordChanged = (ev): void => { + private onPasswordChanged = (ev: React.ChangeEvent): void => { this.setState({ password: ev.target.value }); }; @@ -151,7 +149,7 @@ export default class PasswordLogin extends React.PureComponent { activeElement.blur(); } - const fieldIDsInDisplayOrder = [this.state.loginType, LoginField.Password]; + const fieldIDsInDisplayOrder: LoginField[] = [this.state.loginType, LoginField.Password]; // Run all fields with stricter validation that no longer allows empty // values for required fields. @@ -221,7 +219,7 @@ export default class PasswordLogin extends React.PureComponent { ], }); - private onUsernameValidate = async (fieldState): Promise => { + private onUsernameValidate = async (fieldState: IFieldState): Promise => { const result = await this.validateUsernameRules(fieldState); this.markFieldValid(LoginField.MatrixId, result.valid); return result; @@ -248,7 +246,7 @@ export default class PasswordLogin extends React.PureComponent { ], }); - private onPhoneNumberValidate = async (fieldState): Promise => { + private onPhoneNumberValidate = async (fieldState: IFieldState): Promise => { const result = await this.validatePhoneNumberRules(fieldState); this.markFieldValid(LoginField.Password, result.valid); return result; @@ -266,7 +264,7 @@ export default class PasswordLogin extends React.PureComponent { ], }); - private onPasswordValidate = async (fieldState): Promise => { + private onPasswordValidate = async (fieldState: IFieldState): Promise => { const result = await this.validatePasswordRules(fieldState); this.markFieldValid(LoginField.Password, result.valid); return result; diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 3f41cb6127..4d2cd9214c 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -15,18 +15,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { BaseSyntheticEvent } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixError } from "matrix-js-sdk/src/matrix"; import * as Email from "../../../email"; -import { looksValid as phoneNumberLooksValid } from "../../../phonenumber"; +import { looksValid as phoneNumberLooksValid, PhoneNumberCountryDefinition } from "../../../phonenumber"; import Modal from "../../../Modal"; import { _t, _td } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; import { SAFE_LOCALPART_REGEX } from "../../../Registration"; -import withValidation, { IValidationResult } from "../elements/Validation"; +import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import { ValidatedServerConfig } from "../../../utils/ValidatedServerConfig"; import EmailField from "./EmailField"; import PassphraseField from "./PassphraseField"; @@ -95,12 +95,18 @@ interface IState { * A pure UI component which displays a registration form. */ export default class RegistrationForm extends React.PureComponent { + private [RegistrationField.Email]: Field; + private [RegistrationField.Password]: Field; + private [RegistrationField.PasswordConfirm]: Field; + private [RegistrationField.Username]: Field; + private [RegistrationField.PhoneNumber]: Field; + public static defaultProps = { onValidationChange: logger.error, canSubmit: true, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -115,7 +121,9 @@ export default class RegistrationForm extends React.PureComponent => { + private onSubmit = async ( + ev: BaseSyntheticEvent, + ): Promise => { ev.preventDefault(); ev.persist(); @@ -152,7 +160,9 @@ export default class RegistrationForm extends React.PureComponent, + ): void { PosthogAnalytics.instance.setAuthenticationType("Password"); const email = this.state.email.trim(); @@ -248,7 +258,7 @@ export default class RegistrationForm extends React.PureComponent { + private onEmailChange = (ev: React.ChangeEvent): void => { this.setState({ email: ev.target.value.trim(), }); @@ -277,7 +287,7 @@ export default class RegistrationForm extends React.PureComponent { + private onPasswordChange = (ev: React.ChangeEvent): void => { this.setState({ password: ev.target.value, }); @@ -287,7 +297,7 @@ export default class RegistrationForm extends React.PureComponent { + private onPasswordConfirmChange = (ev: React.ChangeEvent): void => { this.setState({ passwordConfirm: ev.target.value, }); @@ -297,19 +307,19 @@ export default class RegistrationForm extends React.PureComponent { + private onPhoneCountryChange = (newVal: PhoneNumberCountryDefinition): void => { this.setState({ phoneCountry: newVal.iso2, }); }; - private onPhoneNumberChange = (ev): void => { + private onPhoneNumberChange = (ev: React.ChangeEvent): void => { this.setState({ phoneNumber: ev.target.value, }); }; - private onPhoneNumberValidate = async (fieldState): Promise => { + private onPhoneNumberValidate = async (fieldState: IFieldState): Promise => { const result = await this.validatePhoneNumberRules(fieldState); this.markFieldValid(RegistrationField.PhoneNumber, result.valid); return result; @@ -334,13 +344,13 @@ export default class RegistrationForm extends React.PureComponent { + private onUsernameChange = (ev: React.ChangeEvent): void => { this.setState({ username: ev.target.value, }); }; - private onUsernameValidate = async (fieldState): Promise => { + private onUsernameValidate = async (fieldState: IFieldState): Promise => { const result = await this.validateUsernameRules(fieldState); this.markFieldValid(RegistrationField.Username, result.valid); return result; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 025cb9d271..469c5fbb2a 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -48,7 +48,7 @@ interface IProps { tabIndex?: number; } -const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): string[] => { +const calculateUrls = (url?: string, urls?: string[], lowBandwidth = false): string[] => { // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, ...props.urls ] @@ -66,7 +66,7 @@ const calculateUrls = (url: string, urls: string[], lowBandwidth: boolean): stri return Array.from(new Set(_urls)); }; -const useImageUrl = ({ url, urls }): [string, () => void] => { +const useImageUrl = ({ url, urls }: { url?: string; urls?: string[] }): [string, () => void] => { // Since this is a hot code path and the settings store can be slow, we // use the cached lowBandwidth value from the room context if it exists const roomContext = useContext(RoomContext); diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index b0af3f4cc7..9d8c5c8e90 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -34,7 +34,7 @@ interface IState { export default class DialpadContextMenu extends React.Component { private numberEntryFieldRef: React.RefObject = createRef(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -58,14 +58,14 @@ export default class DialpadContextMenu extends React.Component this.props.onFinished(); }; - public onKeyDown = (ev): void => { + public onKeyDown = (ev: React.KeyboardEvent): void => { // Prevent Backspace and Delete keys from functioning in the entry field if (ev.code === "Backspace" || ev.code === "Delete") { ev.preventDefault(); } }; - public onChange = (ev): void => { + public onChange = (ev: React.ChangeEvent): void => { this.setState({ value: ev.target.value }); }; diff --git a/src/components/views/context_menus/LegacyCallContextMenu.tsx b/src/components/views/context_menus/LegacyCallContextMenu.tsx index 8e4efa28e4..4de776ff06 100644 --- a/src/components/views/context_menus/LegacyCallContextMenu.tsx +++ b/src/components/views/context_menus/LegacyCallContextMenu.tsx @@ -26,7 +26,7 @@ interface IProps extends IContextMenuProps { } export default class LegacyCallContextMenu extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); } diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 2eb5234caa..1f16821ee5 100644 --- a/src/components/views/dialogs/BaseDialog.tsx +++ b/src/components/views/dialogs/BaseDialog.tsx @@ -88,7 +88,7 @@ export default class BaseDialog extends React.Component { fixedWidth: true, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.matrixClient = MatrixClientPeg.get(); @@ -132,7 +132,7 @@ export default class BaseDialog extends React.Component { headerImage = ; } - const lockProps = { + const lockProps: Record = { "onKeyDown": this.onKeyDown, "role": "dialog", // This should point to a node describing the dialog. diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 7b389a3e9b..e3ae127024 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -54,7 +54,7 @@ interface IState { export default class BugReportDialog extends React.Component { private unmounted: boolean; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { sendLogs: true, diff --git a/src/components/views/dialogs/BulkRedactDialog.tsx b/src/components/views/dialogs/BulkRedactDialog.tsx index 9c503d24fe..e1f9a11cd2 100644 --- a/src/components/views/dialogs/BulkRedactDialog.tsx +++ b/src/components/views/dialogs/BulkRedactDialog.tsx @@ -21,6 +21,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; @@ -42,7 +43,7 @@ const BulkRedactDialog: React.FC = (props) => { const [keepStateEvents, setKeepStateEvents] = useState(true); let timeline = room.getLiveTimeline(); - let eventsToRedact = []; + let eventsToRedact: MatrixEvent[] = []; while (timeline) { eventsToRedact = [ ...eventsToRedact, diff --git a/src/components/views/dialogs/ChangelogDialog.tsx b/src/components/views/dialogs/ChangelogDialog.tsx index 696c3616bd..1d7c3e196a 100644 --- a/src/components/views/dialogs/ChangelogDialog.tsx +++ b/src/components/views/dialogs/ChangelogDialog.tsx @@ -27,16 +27,26 @@ interface IProps { onFinished: (success: boolean) => void; } -const REPOS = ["vector-im/element-web", "matrix-org/matrix-react-sdk", "matrix-org/matrix-js-sdk"]; +type State = Partial>; -export default class ChangelogDialog extends React.Component { - public constructor(props) { +interface Commit { + sha: string; + html_url: string; + commit: { + message: string; + }; +} + +const REPOS = ["vector-im/element-web", "matrix-org/matrix-react-sdk", "matrix-org/matrix-js-sdk"] as const; + +export default class ChangelogDialog extends React.Component { + public constructor(props: IProps) { super(props); this.state = {}; } - private async fetchChanges(repo: string, oldVersion: string, newVersion: string): Promise { + private async fetchChanges(repo: typeof REPOS[number], oldVersion: string, newVersion: string): Promise { const url = `https://riot.im/github/repos/${repo}/compare/${oldVersion}...${newVersion}`; try { @@ -66,7 +76,7 @@ export default class ChangelogDialog extends React.Component { } } - private elementsForCommit(commit): JSX.Element { + private elementsForCommit(commit: Commit): JSX.Element { return (
  • @@ -86,7 +96,7 @@ export default class ChangelogDialog extends React.Component { msg: this.state[repo], }); } else { - content = this.state[repo].map(this.elementsForCommit); + content = (this.state[repo] as Commit[]).map(this.elementsForCommit); } return (
    diff --git a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx index a9cf28d91a..1d8e488073 100644 --- a/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx +++ b/src/components/views/dialogs/ConfirmAndWaitRedactDialog.tsx @@ -45,7 +45,7 @@ interface IState { * To avoid this, we keep the dialog open as long as /redact is in progress. */ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { isRedacting: false, diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index 0fcb6e1353..c13f7921ab 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -62,7 +62,7 @@ export default class CreateRoomDialog extends React.Component { private nameField = createRef(); private aliasField = createRef(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.supportsRestricted = !!this.props.parentSpace; diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 96d8eec1b8..aaf14452e3 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -21,7 +21,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; -import AccessibleButton from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { BetaPill } from "../beta/BetaCard"; import Field from "../elements/Field"; @@ -54,7 +54,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick } const [joinRule, setJoinRule] = useState(defaultJoinRule); - const onCreateSubspaceClick = async (e): Promise => { + const onCreateSubspaceClick = async (e: ButtonEvent): Promise => { e.preventDefault(); if (busy) return; diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index ad9f657baa..8876038ec4 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -28,6 +28,16 @@ import BaseDialog from "./BaseDialog"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; +type DialogAesthetics = Partial<{ + [x in AuthType]: { + [x: number]: { + body: string; + continueText?: string; + continueKind?: string; + }; + }; +}>; + interface IProps { onFinished: (success: boolean) => void; } @@ -46,7 +56,7 @@ interface IState { } export default class DeactivateAccountDialog extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -65,7 +75,7 @@ export default class DeactivateAccountDialog extends React.Component { + private onStagePhaseChange = (stage: AuthType, phase: number): void => { const dialogAesthetics = { [SSOAuthEntry.PHASE_PREAUTH]: { body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), @@ -80,7 +90,7 @@ export default class DeactivateAccountDialog extends React.Component = ({ roomId, onFinished }) => { {Object.entries(Tools).map(([category, tools]) => (
    -

    {_t(categoryLabels[category])}

    +

    {_t(categoryLabels[category as unknown as Category])}

    {tools.map(([label, tool]) => { const onClick = (): void => { setTool([label, tool]); diff --git a/src/components/views/dialogs/ErrorDialog.tsx b/src/components/views/dialogs/ErrorDialog.tsx index d0d4b22f6e..298398b078 100644 --- a/src/components/views/dialogs/ErrorDialog.tsx +++ b/src/components/views/dialogs/ErrorDialog.tsx @@ -46,9 +46,6 @@ interface IState { export default class ErrorDialog extends React.Component { public static defaultProps = { focus: true, - title: null, - description: null, - button: null, }; private onClick = (): void => { diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index 6b7df64cb5..58b84436a5 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -25,7 +25,14 @@ import DialogButtons from "../elements/DialogButtons"; import Field from "../elements/Field"; import StyledRadioGroup from "../elements/StyledRadioGroup"; import StyledCheckbox from "../elements/StyledCheckbox"; -import { ExportFormat, ExportType, textForFormat, textForType } from "../../../utils/exportUtils/exportUtils"; +import { + ExportFormat, + ExportFormatKey, + ExportType, + ExportTypeKey, + textForFormat, + textForType, +} from "../../../utils/exportUtils/exportUtils"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import HTMLExporter from "../../../utils/exportUtils/HtmlExport"; import JSONExporter from "../../../utils/exportUtils/JSONExport"; @@ -237,15 +244,15 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { setExporter(null); }; - const exportFormatOptions = Object.keys(ExportFormat).map((format) => ({ - value: ExportFormat[format], - label: textForFormat(ExportFormat[format]), + const exportFormatOptions = Object.values(ExportFormat).map((format) => ({ + value: format, + label: textForFormat(format), })); - const exportTypeOptions = Object.keys(ExportType).map((type) => { + const exportTypeOptions = Object.values(ExportType).map((type) => { return ( - ); }); @@ -332,7 +339,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { setExportFormat(ExportFormat[key])} + onChange={(key: ExportFormatKey) => setExportFormat(ExportFormat[key])} definitions={exportFormatOptions} /> @@ -347,7 +354,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { element="select" value={exportType} onChange={(e) => { - setExportType(ExportType[e.target.value]); + setExportType(ExportType[e.target.value as ExportTypeKey]); }} > {exportTypeOptions} diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index 561bbd0b1c..0bd462c65a 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -243,7 +243,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr } const [truncateAt, setTruncateAt] = useState(20); - function overflowTile(overflowCount, totalCount): JSX.Element { + function overflowTile(overflowCount: number, totalCount: number): JSX.Element { const text = _t("and %(count)s others...", { count: overflowCount }); return ( => { + private onAccountDetailsDialogFinished = async (result: boolean): Promise => { if (result) { return this.sendAccountDetails(); } diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 3ebd9d4eac..00eb40278f 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { IAuthData } from "matrix-js-sdk/src/interactive-auth"; +import { AuthType, IAuthData } from "matrix-js-sdk/src/interactive-auth"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -27,8 +27,8 @@ import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import BaseDialog from "./BaseDialog"; import { IDialogProps } from "./IDialogProps"; -interface IDialogAesthetics { - [x: string]: { +type DialogAesthetics = Partial<{ + [x in AuthType]: { [x: number]: { title: string; body: string; @@ -36,7 +36,7 @@ interface IDialogAesthetics { continueKind: string; }; }; -} +}>; export interface InteractiveAuthDialogProps extends IDialogProps { // matrix client to use for UI auth requests @@ -71,15 +71,15 @@ export interface InteractiveAuthDialogProps extends IDialogProps { // } // // Default is defined in _getDefaultDialogAesthetics() - aestheticsForStagePhases?: IDialogAesthetics; + aestheticsForStagePhases?: DialogAesthetics; } interface IState { authError: Error; // See _onUpdateStagePhase() - uiaStage: number | string; - uiaStagePhase: number | string; + uiaStage: AuthType | null; + uiaStagePhase: number | null; } export default class InteractiveAuthDialog extends React.Component { @@ -95,7 +95,7 @@ export default class InteractiveAuthDialog extends React.Component { + private onUpdateStagePhase = (newStage: AuthType, newPhase: number): void => { // We copy the stage and stage phase params into state for title selection in render() this.setState({ uiaStage: newStage, uiaStagePhase: newPhase }); }; diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ff28f6636a..4cb29c61d7 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef, ReactNode } from "react"; +import React, { createRef, ReactNode, SyntheticEvent } from "react"; import classNames from "classnames"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -92,7 +92,7 @@ enum TabId { } class DMUserTile extends React.PureComponent { - private onRemove = (e): void => { + private onRemove = (e: ButtonEvent): void => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -139,7 +139,7 @@ interface IDMRoomTileProps { } class DMRoomTile extends React.PureComponent { - private onClick = (e): void => { + private onClick = (e: ButtonEvent): void => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -271,6 +271,10 @@ interface InviteRoomProps extends BaseProps { roomId: string; } +function isRoomInvite(props: Props): props is InviteRoomProps { + return props.kind === KIND_INVITE; +} + interface InviteCallProps extends BaseProps { kind: typeof KIND_CALL_TRANSFER; @@ -311,7 +315,7 @@ export default class InviteDialog extends React.PureComponent = createRef(); private unmounted = false; - public constructor(props) { + public constructor(props: Props) { super(props); if (props.kind === KIND_INVITE && !props.roomId) { @@ -321,7 +325,7 @@ export default class InviteDialog extends React.PureComponent alreadyInvited.add(m.userId)); @@ -361,7 +365,7 @@ export default class InviteDialog extends React.PureComponent { + private onConsultFirstChange = (ev: React.ChangeEvent): void => { this.setState({ consultFirst: ev.target.checked }); }; @@ -538,11 +542,11 @@ export default class InviteDialog extends React.PureComponent { + private onKeyDown = (e: React.KeyboardEvent): void => { if (this.state.busy) return; let handled = false; - const value = e.target.value.trim(); + const value = e.currentTarget.value.trim(); const action = getKeyBindingsManager().getAccessibilityAction(e); switch (action) { @@ -692,7 +696,7 @@ export default class InviteDialog extends React.PureComponent { + private updateFilter = (e: React.ChangeEvent): void => { const term = e.target.value; this.setState({ filterText: term }); @@ -750,7 +754,7 @@ export default class InviteDialog extends React.PureComponent => { + private onPaste = async (e: React.ClipboardEvent): Promise => { if (this.state.filterText) { // if the user has already typed something, just let them // paste normally. @@ -825,7 +829,7 @@ export default class InviteDialog extends React.PureComponent { + private onClickInputArea = (e: React.MouseEvent): void => { // Stop the browser from highlighting text e.preventDefault(); e.stopPropagation(); @@ -835,7 +839,7 @@ export default class InviteDialog extends React.PureComponent { + private onUseDefaultIdentityServerClick = (e: ButtonEvent): void => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -844,7 +848,7 @@ export default class InviteDialog extends React.PureComponent { + private onManageSettingsClick = (e: ButtonEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.props.onFinished(false); @@ -864,8 +868,8 @@ export default class InviteDialog extends React.PureComponent { + private onDialFormSubmit = (ev: SyntheticEvent): void => { ev.preventDefault(); this.transferCall(); }; - private onDialChange = (ev): void => { + private onDialChange = (ev: React.ChangeEvent): void => { this.setState({ dialPadValue: ev.currentTarget.value }); }; @@ -1066,9 +1070,9 @@ export default class InviteDialog extends React.PureComponent { + private async onLinkClick(e: React.MouseEvent): Promise { e.preventDefault(); - selectText(e.target); + selectText(e.currentTarget); } private get screenName(): ScreenName { diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 6e4fb1b2e9..dfb4a5fb37 100644 --- a/src/components/views/dialogs/LogoutDialog.tsx +++ b/src/components/views/dialogs/LogoutDialog.tsx @@ -45,7 +45,7 @@ export default class LogoutDialog extends React.Component { onFinished: function () {}, }; - public constructor(props) { + public constructor(props: IProps) { super(props); const cli = MatrixClientPeg.get(); diff --git a/src/components/views/dialogs/MessageEditHistoryDialog.tsx b/src/components/views/dialogs/MessageEditHistoryDialog.tsx index 8775b4eb5c..2e3d0aca05 100644 --- a/src/components/views/dialogs/MessageEditHistoryDialog.tsx +++ b/src/components/views/dialogs/MessageEditHistoryDialog.tsx @@ -121,8 +121,8 @@ export default class MessageEditHistoryDialog extends React.PureComponent b.disabled).map((b) => b.id), }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.widget = new ElementWidget({ diff --git a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx index e3be541014..1e6c9901ee 100644 --- a/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx +++ b/src/components/views/dialogs/RegistrationEmailPromptDialog.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import * as React from "react"; -import { useRef, useState } from "react"; +import { SyntheticEvent, useRef, useState } from "react"; import { _t, _td } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; @@ -32,7 +32,7 @@ const RegistrationEmailPromptDialog: React.FC = ({ onFinished }) => { const [email, setEmail] = useState(""); const fieldRef = useRef(); - const onSubmit = async (e): Promise => { + const onSubmit = async (e: SyntheticEvent): Promise => { e.preventDefault(); if (email) { const valid = await fieldRef.current.validate({}); diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx index 28ae30b6f1..00e430b2c3 100644 --- a/src/components/views/dialogs/ReportEventDialog.tsx +++ b/src/components/views/dialogs/ReportEventDialog.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; @@ -189,7 +189,7 @@ export default class ReportEventDialog extends React.Component { }; // The user has written down a freeform description of the abuse. - private onReasonChange = ({ target: { value: reason } }): void => { + private onReasonChange = ({ target: { value: reason } }: ChangeEvent): void => { this.setState({ reason }); }; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 68616cf117..67b46b371e 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.tsx +++ b/src/components/views/dialogs/RoomSettingsDialog.tsx @@ -33,6 +33,7 @@ import { UIFeature } from "../../../settings/UIFeature"; import BaseDialog from "./BaseDialog"; import { Action } from "../../../dispatcher/actions"; import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab"; +import { ActionPayload } from "../../../dispatcher/payloads"; export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB"; @@ -74,7 +75,7 @@ export default class RoomSettingsDialog extends React.Component MatrixClientPeg.get().removeListener(RoomEvent.Name, this.onRoomName); } - private onAction = (payload): void => { + private onAction = (payload: ActionPayload): void => { // When view changes below us, close the room settings // whilst the modal is open this can only be triggered when someone hits Leave Room if (payload.action === Action.ViewHomePage) { diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx index a62c1467dc..c3b2cd19bf 100644 --- a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx +++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { ReactNode, SyntheticEvent } from "react"; import { EventType } from "matrix-js-sdk/src/@types/event"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; @@ -53,7 +53,7 @@ export default class RoomUpgradeWarningDialog extends React.Component { + private openBugReportDialog = (e: SyntheticEvent): void => { e.preventDefault(); e.stopPropagation(); diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 31c7a00659..7c567dadf6 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React, { ChangeEvent, createRef, SyntheticEvent } from "react"; import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery"; import { logger } from "matrix-js-sdk/src/logger"; @@ -45,7 +45,7 @@ export default class ServerPickerDialog extends React.PureComponent(); private validatedConf: ValidatedServerConfig; - public constructor(props) { + public constructor(props: IProps) { super(props); const config = SdkConfig.get(); @@ -75,7 +75,7 @@ export default class ServerPickerDialog extends React.PureComponent { + private onHomeserverChange = (ev: ChangeEvent): void => { this.setState({ otherHomeserver: ev.target.value }); }; @@ -149,7 +149,7 @@ export default class ServerPickerDialog extends React.PureComponent => this.validate(fieldState); - private onSubmit = async (ev): Promise => { + private onSubmit = async (ev: SyntheticEvent): Promise => { ev.preventDefault(); const valid = await this.fieldRef.current.validate({ allowEmpty: false }); diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index b3761a49b6..9a78b35b2f 100644 --- a/src/components/views/dialogs/ShareDialog.tsx +++ b/src/components/views/dialogs/ShareDialog.tsx @@ -36,12 +36,12 @@ const socials = [ { name: "Facebook", img: require("../../../../res/img/social/facebook.png"), - url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`, + url: (url: String) => `https://www.facebook.com/sharer/sharer.php?u=${url}`, }, { name: "Twitter", img: require("../../../../res/img/social/twitter-2.png"), - url: (url) => `https://twitter.com/home?status=${url}`, + url: (url: string) => `https://twitter.com/home?status=${url}`, }, /* // icon missing name: 'Google Plus', @@ -50,17 +50,17 @@ const socials = [ },*/ { name: "LinkedIn", img: require("../../../../res/img/social/linkedin.png"), - url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, + url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, }, { name: "Reddit", img: require("../../../../res/img/social/reddit.png"), - url: (url) => `https://www.reddit.com/submit?url=${url}`, + url: (url: string) => `https://www.reddit.com/submit?url=${url}`, }, { name: "email", img: require("../../../../res/img/social/email-1.png"), - url: (url) => `mailto:?body=${url}`, + url: (url: string) => `mailto:?body=${url}`, }, ]; @@ -75,7 +75,7 @@ interface IState { } export default class ShareDialog extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); let permalinkCreator: RoomPermalinkCreator = null; @@ -91,9 +91,9 @@ export default class ShareDialog extends React.PureComponent { }; } - public static onLinkClick(e): void { + public static onLinkClick(e: React.MouseEvent): void { e.preventDefault(); - selectText(e.target); + selectText(e.currentTarget); } private onLinkSpecificEventCheckboxClick = (): void => { diff --git a/src/components/views/dialogs/SlashCommandHelpDialog.tsx b/src/components/views/dialogs/SlashCommandHelpDialog.tsx index f12143418c..1324babbeb 100644 --- a/src/components/views/dialogs/SlashCommandHelpDialog.tsx +++ b/src/components/views/dialogs/SlashCommandHelpDialog.tsx @@ -17,14 +17,14 @@ limitations under the License. import React from "react"; import { _t } from "../../../languageHandler"; -import { CommandCategories, Commands } from "../../../SlashCommands"; +import { Command, CommandCategories, Commands } from "../../../SlashCommands"; import { IDialogProps } from "./IDialogProps"; import InfoDialog from "./InfoDialog"; interface IProps extends IDialogProps {} const SlashCommandHelpDialog: React.FC = ({ onFinished }) => { - const categories = {}; + const categories: Record = {}; Commands.forEach((cmd) => { if (!cmd.isEnabled()) return; if (!categories[cmd.category]) { diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index 819e14be7c..fd420d436c 100644 --- a/src/components/views/dialogs/TermsDialog.tsx +++ b/src/components/views/dialogs/TermsDialog.tsx @@ -21,6 +21,7 @@ import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types"; import { _t, pickBestLanguage } from "../../../languageHandler"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "./BaseDialog"; +import { ServicePolicyPair } from "../../../Terms"; interface ITermsCheckboxProps { onChange: (url: string, checked: boolean) => void; @@ -43,7 +44,7 @@ interface ITermsDialogProps { * Array of [Service, policies] pairs, where policies is the response from the * /terms endpoint for that service */ - policiesAndServicePairs: any[]; + policiesAndServicePairs: ServicePolicyPair[]; /** * urls that the user has already agreed to @@ -63,7 +64,7 @@ interface IState { } export default class TermsDialog extends React.PureComponent { - public constructor(props) { + public constructor(props: ITermsDialogProps) { super(props); this.state = { // url -> boolean diff --git a/src/components/views/dialogs/UploadConfirmDialog.tsx b/src/components/views/dialogs/UploadConfirmDialog.tsx index 9b22b33b67..688ee03b81 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.tsx +++ b/src/components/views/dialogs/UploadConfirmDialog.tsx @@ -39,7 +39,7 @@ export default class UploadConfirmDialog extends React.Component { totalFiles: 1, }; - public constructor(props) { + public constructor(props: IProps) { super(props); // Create a fresh `Blob` for previewing (even though `File` already is diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 7b33ca58da..aca4a1b37b 100644 --- a/src/components/views/dialogs/UserSettingsDialog.tsx +++ b/src/components/views/dialogs/UserSettingsDialog.tsx @@ -50,7 +50,7 @@ interface IState { export default class UserSettingsDialog extends React.Component { private settingsWatchers: string[] = []; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 25e7e835bd..b8b1e2ed8b 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -35,7 +35,7 @@ interface IState { } export default class VerificationRequestDialog extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { verificationRequest: this.props.verificationRequest, diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx index 4c3acb8960..4ede87b1eb 100644 --- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx +++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx @@ -33,13 +33,12 @@ interface IProps extends IDialogProps { widgetKind: WidgetKind; // TODO: Refactor into the Widget class } -interface IBooleanStates { - // @ts-ignore - TS wants a string key, but we know better - [capability: Capability]: boolean; -} +type BooleanStates = Partial<{ + [capability in Capability]: boolean; +}>; interface IState { - booleanStates: IBooleanStates; + booleanStates: BooleanStates; rememberSelection: boolean; } @@ -52,7 +51,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< const parsedEvents = WidgetEventCapability.findEventCapabilities(this.props.requestedCapabilities); parsedEvents.forEach((e) => this.eventPermissionsMap.set(e.raw, e)); - const states: IBooleanStates = {}; + const states: BooleanStates = {}; this.props.requestedCapabilities.forEach((c) => (states[c] = true)); this.state = { @@ -71,7 +70,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< this.setState({ rememberSelection: newVal }); }; - private onSubmit = async (ev): Promise => { + private onSubmit = async (): Promise => { this.closeAndTryRemember( Object.entries(this.state.booleanStates) .filter(([_, isSelected]) => isSelected) @@ -79,7 +78,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< ); }; - private onReject = async (ev): Promise => { + private onReject = async (): Promise => { this.closeAndTryRemember([]); // nothing was approved }; diff --git a/src/components/views/dialogs/devtools/Event.tsx b/src/components/views/dialogs/devtools/Event.tsx index 6af242b68b..ce7b1e2361 100644 --- a/src/components/views/dialogs/devtools/Event.tsx +++ b/src/components/views/dialogs/devtools/Event.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext, useMemo, useRef, useState } from "react"; +import React, { ChangeEvent, useContext, useMemo, useRef, useState } from "react"; import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t, _td } from "../../../../languageHandler"; @@ -87,7 +87,7 @@ export const EventEditor: React.FC = ({ fieldDefs, defaultCon type="text" autoComplete="on" value={fieldData[i]} - onChange={(ev) => + onChange={(ev: ChangeEvent) => setFieldData((data) => { data[i] = ev.target.value; return [...data]; diff --git a/src/components/views/dialogs/devtools/FilteredList.tsx b/src/components/views/dialogs/devtools/FilteredList.tsx index 46d3f95661..11ee015671 100644 --- a/src/components/views/dialogs/devtools/FilteredList.tsx +++ b/src/components/views/dialogs/devtools/FilteredList.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useEffect, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import { _t } from "../../../../languageHandler"; import Field from "../../elements/Field"; @@ -72,7 +72,7 @@ const FilteredList: React.FC = ({ children, query, onChange }) => { type="text" autoComplete="off" value={query} - onChange={(ev) => onChange(ev.target.value)} + onChange={(ev: ChangeEvent) => onChange(ev.target.value)} className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" // force re-render so that autoFocus is applied when this component is re-used key={children?.[0]?.key ?? ""} diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index 72876f3551..c0801cd062 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useContext, useMemo, useState } from "react"; +import React, { ChangeEvent, useContext, useMemo, useState } from "react"; import { logger } from "matrix-js-sdk/src/logger"; import { _t } from "../../../../languageHandler"; @@ -74,7 +74,7 @@ const CanEditLevelField: React.FC = ({ setting, roomId, }; function renderExplicitSettingValues(setting: string, roomId: string): string { - const vals = {}; + const vals: Record = {}; for (const level of LEVEL_ORDER) { try { vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true); @@ -283,7 +283,7 @@ const SettingsList: React.FC = ({ onBack, onView, onEdit }) type="text" autoComplete="off" value={query} - onChange={(ev) => setQuery(ev.target.value)} + onChange={(ev: ChangeEvent) => setQuery(ev.target.value)} className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" /> diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index d7154b3aa2..8964b18992 100644 --- a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx +++ b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx @@ -62,7 +62,7 @@ interface IState { export default class AccessSecretStorageDialog extends React.PureComponent { private fileUpload = React.createRef(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx index b69fc9cbb3..0b6438df72 100644 --- a/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx +++ b/src/components/views/dialogs/security/CreateCrossSigningDialog.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from "react"; import { CrossSigningKeys } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; +import { UIAFlow } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../../../MatrixClientPeg"; import { _t } from "../../../../languageHandler"; @@ -82,7 +83,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent { + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { return f.stages.length === 1 && f.stages[0] === "m.login.password"; }); this.setState({ diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx index 0d1286a29b..66a9314ea3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { IKeyBackupInfo, IKeyBackupRestoreResult } from "matrix-js-sdk/src/crypto/keybackup"; import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/crypto/api"; @@ -81,7 +81,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { + private progressCallback = (data: IState["progress"]): void => { this.setState({ progress: data, }); @@ -128,7 +128,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent => {}, /* forceReset = */ true); }; - private onRecoveryKeyChange = (e): void => { + private onRecoveryKeyChange = (e: ChangeEvent): void => { this.setState({ recoveryKey: e.target.value, recoveryKeyValid: MatrixClientPeg.get().isValidRecoveryKey(e.target.value), @@ -213,7 +213,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { + private onPassPhraseChange = (e: ChangeEvent): void => { this.setState({ passPhrase: e.target.value, }); @@ -247,7 +247,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { + private async restoreWithCachedKey(backupInfo?: IKeyBackupInfo): Promise { if (!backupInfo) return false; try { const recoverInfo = await MatrixClientPeg.get().restoreKeyBackupWithCache( diff --git a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index bf25569568..0102935f28 100644 --- a/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -77,7 +77,7 @@ import BaseAvatar from "../../avatars/BaseAvatar"; import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; import { SearchResultAvatar } from "../../avatars/SearchResultAvatar"; import { NetworkDropdown } from "../../directory/NetworkDropdown"; -import AccessibleButton from "../../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../../elements/AccessibleButton"; import LabelledCheckbox from "../../elements/LabelledCheckbox"; import Spinner from "../../elements/Spinner"; import NotificationBadge from "../../rooms/NotificationBadge"; @@ -625,7 +625,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const showViewButton = clientRoom?.getMyMembership() === "join" || result.publicRoom.world_readable || cli.isGuest(); - const listener = (ev): void => { + const listener = (ev: ButtonEvent): void => { const { publicRoom } = result; viewRoom( { diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index b75de2ee44..de0b1a17e1 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -18,7 +18,7 @@ limitations under the License. */ import url from "url"; -import React, { ContextType, createRef, MutableRefObject, ReactNode } from "react"; +import React, { ContextType, createRef, CSSProperties, MutableRefObject, ReactNode } from "react"; import classNames from "classnames"; import { MatrixCapabilities } from "matrix-widget-api"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; @@ -81,7 +81,7 @@ interface IProps { // Is this an instance of a user widget userWidget: boolean; // sets the pointer-events property on the iframe - pointerEvents?: string; + pointerEvents?: CSSProperties["pointerEvents"]; widgetPageTitle?: string; showLayoutButtons?: boolean; // Handle to manually notify the PersistedElement that it needs to move @@ -562,9 +562,9 @@ export default class AppTile extends React.Component { "microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;"; const appTileBodyClass = "mx_AppTileBody" + (this.props.miniMode ? "_mini " : " "); - const appTileBodyStyles = {}; + const appTileBodyStyles: CSSProperties = {}; if (this.props.pointerEvents) { - appTileBodyStyles["pointerEvents"] = this.props.pointerEvents; + appTileBodyStyles.pointerEvents = this.props.pointerEvents; } const loadingElement = ( diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index c7abf5246c..a41aa5321d 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -373,7 +373,7 @@ export default class Dropdown extends React.Component { ); } - const dropdownClasses = { + const dropdownClasses: Record = { mx_Dropdown: true, mx_Dropdown_disabled: this.props.disabled, }; diff --git a/src/components/views/elements/EditableItemList.tsx b/src/components/views/elements/EditableItemList.tsx index 9cc5371ff5..2d29976604 100644 --- a/src/components/views/elements/EditableItemList.tsx +++ b/src/components/views/elements/EditableItemList.tsx @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ChangeEvent } from "react"; import { _t } from "../../../languageHandler"; import Field from "./Field"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; interface IItemProps { index?: number; @@ -35,21 +35,21 @@ export class EditableItem extends React.Component { verifyRemove: false, }; - private onRemove = (e): void => { + private onRemove = (e: ButtonEvent): void => { e.stopPropagation(); e.preventDefault(); this.setState({ verifyRemove: true }); }; - private onDontRemove = (e): void => { + private onDontRemove = (e: ButtonEvent): void => { e.stopPropagation(); e.preventDefault(); this.setState({ verifyRemove: false }); }; - private onActuallyRemove = (e): void => { + private onActuallyRemove = (e: ButtonEvent): void => { e.stopPropagation(); e.preventDefault(); @@ -105,19 +105,19 @@ interface IProps { } export default class EditableItemList

    extends React.PureComponent { - protected onItemAdded = (e): void => { + protected onItemAdded = (e: ButtonEvent): void => { e.stopPropagation(); e.preventDefault(); - if (this.props.onItemAdded) this.props.onItemAdded(this.props.newItem); + this.props.onItemAdded?.(this.props.newItem); }; protected onItemRemoved = (index: number): void => { - if (this.props.onItemRemoved) this.props.onItemRemoved(index); + this.props.onItemRemoved?.(index); }; - protected onNewItemChanged = (e): void => { - if (this.props.onNewItemChanged) this.props.onNewItemChanged(e.target.value); + protected onNewItemChanged = (e: ChangeEvent): void => { + this.props.onNewItemChanged?.(e.target.value); }; protected renderNewItemField(): JSX.Element { diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 423ae62d19..cf11350c2b 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -32,13 +32,13 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { const lazyLoadEffectModule = async (name: string): Promise => { if (!name) return null; - let effect: ICanvasEffect | null = effectsRef.current[name] || null; + let effect: ICanvasEffect | null = effectsRef.current.get(name) || null; if (effect === null) { const options = CHAT_EFFECTS.find((e) => e.command === name)?.options; try { const { default: Effect } = await import(`../../../effects/${name}`); effect = new Effect(options); - effectsRef.current[name] = effect; + effectsRef.current.set(name, effect); } catch (err) { logger.warn(`Unable to load effect module at '../../../effects/${name}.`, err); } @@ -70,7 +70,7 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { - const effectModule: ICanvasEffect = currentEffects[effect]; + const effectModule: ICanvasEffect = currentEffects.get(effect); if (effectModule && effectModule.isRunning) { effectModule.stop(); } diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 87b81f3280..7603041478 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -34,7 +34,7 @@ interface IState { * catch exceptions during rendering in the component tree below them. */ export default class ErrorBoundary extends React.PureComponent<{}, IState> { - public constructor(props) { + public constructor(props: {}) { super(props); this.state = { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 60288fb2f5..2a55eec8e1 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps } from "react"; +import React, { ComponentProps, ReactNode } from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { EventType } from "matrix-js-sdk/src/@types/event"; @@ -161,7 +161,15 @@ export default class EventListSummary extends React.Component { * @returns {string[]} an array of transitions. */ private static getCanonicalTransitions(transitions: TransitionType[]): TransitionType[] { - const modMap = { + const modMap: Partial< + Record< + TransitionType, + { + after: TransitionType; + newTransition: TransitionType; + } + > + > = { [TransitionType.Joined]: { after: TransitionType.Left, newTransition: TransitionType.JoinedAndLeft, @@ -170,10 +178,6 @@ export default class EventListSummary extends React.Component { after: TransitionType.Joined, newTransition: TransitionType.LeftAndJoined, }, - // $currentTransition : { - // 'after' : $nextTransition, - // 'newTransition' : 'new_transition_type', - // }, }; const res: TransitionType[] = []; @@ -237,15 +241,11 @@ export default class EventListSummary extends React.Component { * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - private static getDescriptionForTransition( - t: TransitionType, - userCount: number, - count: number, - ): string | JSX.Element { + private static getDescriptionForTransition(t: TransitionType, userCount: number, count: number): ReactNode | null { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. - let res = null; + let res: ReactNode | undefined; switch (t) { case TransitionType.Joined: res = @@ -377,7 +377,7 @@ export default class EventListSummary extends React.Component { break; } - return res; + return res ?? null; } private static getTransitionSequence(events: IUserEvents[]): TransitionType[] { diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index b22c1d2b27..bdb5425195 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -145,7 +145,7 @@ export default class Field extends React.PureComponent { }); }, VALIDATION_THROTTLE_MS); - public constructor(props) { + public constructor(props: PropShapes) { super(props); this.state = { valid: undefined, @@ -165,7 +165,7 @@ export default class Field extends React.PureComponent { }); } - private onFocus = (ev): void => { + private onFocus = (ev: React.FocusEvent): void => { this.setState({ focused: true, }); @@ -175,22 +175,18 @@ export default class Field extends React.PureComponent { }); } // Parent component may have supplied its own `onFocus` as well - if (this.props.onFocus) { - this.props.onFocus(ev); - } + this.props.onFocus?.(ev); }; - private onChange = (ev): void => { + private onChange = (ev: React.ChangeEvent): void => { if (this.props.validateOnChange) { this.validateOnChange(); } // Parent component may have supplied its own `onChange` as well - if (this.props.onChange) { - this.props.onChange(ev); - } + this.props.onChange?.(ev); }; - private onBlur = (ev): void => { + private onBlur = (ev: React.FocusEvent): void => { this.setState({ focused: false, }); @@ -200,9 +196,7 @@ export default class Field extends React.PureComponent { }); } // Parent component may have supplied its own `onBlur` as well - if (this.props.onBlur) { - this.props.onBlur(ev); - } + this.props.onBlur?.(ev); }; public async validate({ focused, allowEmpty = true }: IValidateOpts): Promise { diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index b6dff90b9c..ac5a78566e 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -91,7 +91,7 @@ interface IState { } export default class ImageView extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); const { thumbnailInfo } = this.props; diff --git a/src/components/views/elements/InteractiveTooltip.tsx b/src/components/views/elements/InteractiveTooltip.tsx index 33819f8ed6..1986de0243 100644 --- a/src/components/views/elements/InteractiveTooltip.tsx +++ b/src/components/views/elements/InteractiveTooltip.tsx @@ -308,8 +308,8 @@ export default class InteractiveTooltip extends React.Component side: Direction.Top, }; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { contentRect: null, diff --git a/src/components/views/elements/InviteReason.tsx b/src/components/views/elements/InviteReason.tsx index 08e0ceca31..2bdbb98b21 100644 --- a/src/components/views/elements/InviteReason.tsx +++ b/src/components/views/elements/InviteReason.tsx @@ -30,7 +30,7 @@ interface IState { } export default class InviteReason extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { // We hide the reason for invitation by default, since it can be a diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx index ce4db91117..83a1e66f8a 100644 --- a/src/components/views/elements/LabelledToggleSwitch.tsx +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -22,22 +22,24 @@ import { Caption } from "../typography/Caption"; interface IProps { // The value for the toggle switch - value: boolean; + "value": boolean; // The translated label for the switch - label: string; + "label": string; // The translated caption for the switch - caption?: string; + "caption"?: string; // Tooltip to display - tooltip?: string; + "tooltip"?: string; // Whether or not to disable the toggle switch - disabled?: boolean; + "disabled"?: boolean; // True to put the toggle in front of the label // Default false. - toggleInFront?: boolean; + "toggleInFront"?: boolean; // Additional class names to append to the switch. Optional. - className?: string; + "className"?: string; // The function to call when the value changes onChange(checked: boolean): void; + + "data-testid"?: string; } export default class LabelledToggleSwitch extends React.PureComponent { diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx index 9445973b1d..2f7862c922 100644 --- a/src/components/views/elements/Measured.tsx +++ b/src/components/views/elements/Measured.tsx @@ -32,7 +32,7 @@ export default class Measured extends React.PureComponent { breakpoint: 500, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.instanceId = Measured.instanceCount++; diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index f692f74aa8..67ad09018d 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ContextType, MutableRefObject } from "react"; +import React, { ContextType, CSSProperties, MutableRefObject } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import WidgetUtils from "../../../utils/WidgetUtils"; @@ -26,7 +26,7 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; interface IProps { persistentWidgetId: string; persistentRoomId: string; - pointerEvents?: string; + pointerEvents?: CSSProperties["pointerEvents"]; movePersistedElement: MutableRefObject<(() => void) | undefined>; } diff --git a/src/components/views/elements/Pill.tsx b/src/components/views/elements/Pill.tsx index 8f905f5b59..e5df681cc7 100644 --- a/src/components/views/elements/Pill.tsx +++ b/src/components/views/elements/Pill.tsx @@ -20,6 +20,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import dis from "../../../dispatcher/dispatcher"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -30,6 +31,7 @@ import Tooltip, { Alignment } from "./Tooltip"; import RoomAvatar from "../avatars/RoomAvatar"; import MemberAvatar from "../avatars/MemberAvatar"; import { objectHasDiff } from "../../../utils/objects"; +import { ButtonEvent } from "./AccessibleButton"; export enum PillType { UserMention = "TYPE_USER_MENTION", @@ -180,7 +182,7 @@ export default class Pill extends React.Component { }); }; - private doProfileLookup(userId: string, member): void { + private doProfileLookup(userId: string, member: RoomMember): void { MatrixClientPeg.get() .getProfileInfo(userId) .then((resp) => { @@ -196,7 +198,7 @@ export default class Pill extends React.Component { getDirectionalContent: function () { return this.getContent(); }, - }; + } as MatrixEvent; this.setState({ member }); }) .catch((err) => { @@ -204,7 +206,7 @@ export default class Pill extends React.Component { }); } - private onUserPillClicked = (e): void => { + private onUserPillClicked = (e: ButtonEvent): void => { e.preventDefault(); dis.dispatch({ action: Action.ViewUser, diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index 7ba6be8588..f03b40173a 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -44,7 +44,7 @@ export default class RoomAliasField extends React.PureComponent private fieldRef = createRef(); - public constructor(props, context) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); this.state = { diff --git a/src/components/views/elements/RoomFacePile.tsx b/src/components/views/elements/RoomFacePile.tsx index 7149c7e0ce..fc4792c2e0 100644 --- a/src/components/views/elements/RoomFacePile.tsx +++ b/src/components/views/elements/RoomFacePile.tsx @@ -42,7 +42,7 @@ const RoomFacePile: FC = ({ room, onlyKnownUsers = true, numShown = DEFA const count = members.length; // sort users with an explicit avatar first - const iteratees = [(member) => (member.getMxcAvatarUrl() ? 0 : 1)]; + const iteratees = [(member: RoomMember) => (member.getMxcAvatarUrl() ? 0 : 1)]; if (onlyKnownUsers) { members = members.filter(isKnownMember); } else { diff --git a/src/components/views/elements/SearchWarning.tsx b/src/components/views/elements/SearchWarning.tsx index 0737ff3c1b..fec5eee37f 100644 --- a/src/components/views/elements/SearchWarning.tsx +++ b/src/components/views/elements/SearchWarning.tsx @@ -23,7 +23,7 @@ import SdkConfig from "../../../SdkConfig"; import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "../dialogs/UserTab"; -import AccessibleButton from "./AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; export enum WarningKind { Files, @@ -49,7 +49,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El a: (sub) => ( { + onClick={(evt: ButtonEvent) => { evt.preventDefault(); dis.dispatch({ action: Action.ViewUserSettings, diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 78644dc962..a930ec409b 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -33,7 +33,7 @@ function languageMatchesSearchQuery(query: string, language: Languages[0]): bool interface SpellCheckLanguagesDropdownIProps { className: string; value: string; - onOptionChange(language: string); + onOptionChange(language: string): void; } interface SpellCheckLanguagesDropdownIState { @@ -45,7 +45,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component< SpellCheckLanguagesDropdownIProps, SpellCheckLanguagesDropdownIState > { - public constructor(props) { + public constructor(props: SpellCheckLanguagesDropdownIProps) { super(props); this.onSearchChange = this.onSearchChange.bind(this); diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index 8ad41c4705..3a09b3feff 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode } from "react"; +import React, { ChangeEvent, ReactNode } from "react"; import classNames from "classnames"; import StyledRadioButton from "./StyledRadioButton"; @@ -47,8 +47,8 @@ function StyledRadioGroup({ disabled, onChange, }: IProps): JSX.Element { - const _onChange = (e): void => { - onChange(e.target.value); + const _onChange = (e: ChangeEvent): void => { + onChange(e.target.value as T); }; return ( diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 9b927d8189..5e65824454 100644 --- a/src/components/views/elements/Tooltip.tsx +++ b/src/components/views/elements/Tooltip.tsx @@ -68,7 +68,7 @@ export default class Tooltip extends React.PureComponent { alignment: Alignment.Natural, }; - public constructor(props) { + public constructor(props: ITooltipProps) { super(props); this.state = {}; @@ -92,7 +92,7 @@ export default class Tooltip extends React.PureComponent { this.updatePosition(); } - public componentDidUpdate(prevProps): void { + public componentDidUpdate(prevProps: ITooltipProps): void { if (objectHasDiff(prevProps, this.props)) { this.updatePosition(); } diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 415e25cf9e..ceb547a9ce 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -24,7 +24,7 @@ interface IProps { } export default class TooltipButton extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); } diff --git a/src/components/views/elements/TruncatedList.tsx b/src/components/views/elements/TruncatedList.tsx index 45a621eaf3..4f5284d54f 100644 --- a/src/components/views/elements/TruncatedList.tsx +++ b/src/components/views/elements/TruncatedList.tsx @@ -40,7 +40,7 @@ interface IProps { export default class TruncatedList extends React.Component { public static defaultProps = { truncateAt: 2, - createOverflowElement(overflowCount, totalCount) { + createOverflowElement(overflowCount: number, totalCount: number) { return

    {_t("And %(count)s more...", { count: overflowCount })}
    ; }, }; diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index be6d6a5696..bb0a9a12d0 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -40,7 +40,7 @@ interface IState { } class QuickReactions extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { hover: null, diff --git a/src/components/views/emojipicker/ReactionPicker.tsx b/src/components/views/emojipicker/ReactionPicker.tsx index 023328cedb..e113ab8950 100644 --- a/src/components/views/emojipicker/ReactionPicker.tsx +++ b/src/components/views/emojipicker/ReactionPicker.tsx @@ -50,7 +50,7 @@ class ReactionPicker extends React.Component { this.addListeners(); } - public componentDidUpdate(prevProps): void { + public componentDidUpdate(prevProps: IProps): void { if (prevProps.reactions !== this.props.reactions) { this.addListeners(); this.onReactionsChange(); @@ -78,7 +78,7 @@ class ReactionPicker extends React.Component { return {}; } const userId = MatrixClientPeg.get().getUserId(); - const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || []; + const myAnnotations = this.props.reactions.getAnnotationsBySender()[userId] || new Set(); return Object.fromEntries( [...myAnnotations] .filter((event) => !event.isRedacted()) diff --git a/src/components/views/location/LocationShareMenu.tsx b/src/components/views/location/LocationShareMenu.tsx index 31664a872f..5968d50c47 100644 --- a/src/components/views/location/LocationShareMenu.tsx +++ b/src/components/views/location/LocationShareMenu.tsx @@ -38,7 +38,7 @@ type Props = Omit & { relation?: IEventRelation; }; -const getEnabledShareTypes = (relation): LocationShareType[] => { +const getEnabledShareTypes = (relation?: IEventRelation): LocationShareType[] => { const enabledShareTypes = [LocationShareType.Own]; // live locations cannot have a relation diff --git a/src/components/views/messages/DateSeparator.tsx b/src/components/views/messages/DateSeparator.tsx index 183bfffd15..1f8f166bf5 100644 --- a/src/components/views/messages/DateSeparator.tsx +++ b/src/components/views/messages/DateSeparator.tsx @@ -53,10 +53,10 @@ interface IState { } export default class DateSeparator extends React.Component { - private settingWatcherRef = null; + private settingWatcherRef?: string; - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { jumpToDateEnabled: SettingsStore.getValue("feature_jump_to_date"), }; @@ -116,7 +116,7 @@ export default class DateSeparator extends React.Component { } } - private pickDate = async (inputTimestamp): Promise => { + private pickDate = async (inputTimestamp: number | string | Date): Promise => { const unixTimestamp = new Date(inputTimestamp).getTime(); const cli = MatrixClientPeg.get(); @@ -175,7 +175,7 @@ export default class DateSeparator extends React.Component { this.closeMenu(); }; - private onDatePicked = (dateString): void => { + private onDatePicked = (dateString: string): void => { this.pickDate(dateString); this.closeMenu(); }; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index 90b1811e19..db60d706f2 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { forwardRef, ReactNode, ReactChildren } from "react"; +import React, { forwardRef, ReactNode, ReactChild } from "react"; import classNames from "classnames"; interface IProps { @@ -22,7 +22,7 @@ interface IProps { title: string; timestamp?: JSX.Element; subtitle?: ReactNode; - children?: ReactChildren; + children?: ReactChild; } const EventTileBubble = forwardRef( diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 2a78e79e7f..188f4526a9 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -16,6 +16,7 @@ limitations under the License. import React from "react"; import { logger } from "matrix-js-sdk/src/logger"; +import { IContent } from "matrix-js-sdk/src/matrix"; import { Playback } from "../../../audio/Playback"; import InlineSpinner from "../elements/InlineSpinner"; @@ -66,8 +67,8 @@ export default class MAudioBody extends React.PureComponent // We should have a buffer to work with now: let's set it up // Note: we don't actually need a waveform to render an audio event, but voice messages do. - const content = this.props.mxEvent.getContent(); - const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map((p) => p / 1024); + const content = this.props.mxEvent.getContent(); + const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map((p: number) => p / 1024); // We should have a buffer to work with now: let's set it up const playback = PlaybackManager.instance.createPlaybackInstance(buffer, waveform); diff --git a/src/components/views/messages/MFileBody.tsx b/src/components/views/messages/MFileBody.tsx index e68fe24341..f0a80a345b 100644 --- a/src/components/views/messages/MFileBody.tsx +++ b/src/components/views/messages/MFileBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React, { AllHTMLAttributes, createRef } from "react"; import { filesize } from "filesize"; import { logger } from "matrix-js-sdk/src/logger"; @@ -30,7 +30,7 @@ import { FileDownloader } from "../../../utils/FileDownloader"; import TextWithTooltip from "../elements/TextWithTooltip"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; -export let DOWNLOAD_ICON_URL; // cached copy of the download.svg asset for the sandboxed iframe later on +export let DOWNLOAD_ICON_URL: string; // cached copy of the download.svg asset for the sandboxed iframe later on async function cacheDownloadIcon(): Promise { if (DOWNLOAD_ICON_URL) return; // cached already @@ -155,7 +155,7 @@ export default class MFileBody extends React.Component { }); } - public componentDidUpdate(prevProps, prevState): void { + public componentDidUpdate(prevProps: IProps, prevState: IState): void { if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) { this.props.onHeightChanged(); } @@ -295,7 +295,7 @@ export default class MFileBody extends React.Component { ); } else if (contentUrl) { - const downloadProps = { + const downloadProps: AllHTMLAttributes = { target: "_blank", rel: "noreferrer noopener", diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 0796b6830e..bb3a21c4b4 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -29,7 +29,7 @@ interface IProps { } export default class MJitsiWidgetEvent extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); } diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 2a11557214..d12f252750 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -122,9 +122,9 @@ export default class MKeyVerificationRequest extends React.Component { return null; } - let title; - let subtitle; - let stateNode; + let title: string; + let subtitle: string; + let stateNode: JSX.Element; if (!request.canAccept) { let stateLabel; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index e94705f97b..abad590cbf 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -47,7 +47,7 @@ export default class MVideoBody extends React.PureComponent private videoRef = React.createRef(); private sizeWatcher: string; - public constructor(props) { + public constructor(props: IBodyProps) { super(props); this.state = { diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 92612383fc..3bb6260d70 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { SyntheticEvent } from "react"; import classNames from "classnames"; import { MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { Relations, RelationsEvent } from "matrix-js-sdk/src/models/relations"; @@ -52,7 +52,7 @@ const ReactButton: React.FC = ({ mxEvent, reactions }) => { })} title={_t("Add reaction")} onClick={openMenu} - onContextMenu={(e) => { + onContextMenu={(e: SyntheticEvent): void => { e.preventDefault(); openMenu(); }} diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.tsx b/src/components/views/messages/ReactionsRowButtonTooltip.tsx index b875b7e27a..600502e7dd 100644 --- a/src/components/views/messages/ReactionsRowButtonTooltip.tsx +++ b/src/components/views/messages/ReactionsRowButtonTooltip.tsx @@ -35,6 +35,7 @@ interface IProps { export default class ReactionsRowButtonTooltip extends React.PureComponent { public static contextType = MatrixClientContext; + public context!: React.ContextType; public render(): JSX.Element { const { content, reactionEvents, mxEvent, visible } = this.props; @@ -42,7 +43,7 @@ export default class ReactionsRowButtonTooltip extends React.PureComponent { public static contextType = RoomContext; public context!: React.ContextType; - public constructor(props) { + public constructor(props: IBodyProps) { super(props); this.state = { diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx index 12cda9f948..0c221a7933 100644 --- a/src/components/views/messages/TileErrorBoundary.tsx +++ b/src/components/views/messages/TileErrorBoundary.tsx @@ -37,7 +37,7 @@ interface IState { } export default class TileErrorBoundary extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/messages/ViewSourceEvent.tsx b/src/components/views/messages/ViewSourceEvent.tsx index 5f265a2c0d..344ddd344e 100644 --- a/src/components/views/messages/ViewSourceEvent.tsx +++ b/src/components/views/messages/ViewSourceEvent.tsx @@ -31,7 +31,7 @@ interface IState { } export default class ViewSourceEvent extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/right_panel/HeaderButtons.tsx b/src/components/views/right_panel/HeaderButtons.tsx index 7a6eea6f23..75542e1533 100644 --- a/src/components/views/right_panel/HeaderButtons.tsx +++ b/src/components/views/right_panel/HeaderButtons.tsx @@ -26,6 +26,7 @@ import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePha import { IRightPanelCardState } from "../../../stores/right-panel/RightPanelStoreIPanelState"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { NotificationColor } from "../../../stores/notifications/NotificationColor"; +import { ActionPayload } from "../../../dispatcher/payloads"; export enum HeaderKind { Room = "room", @@ -67,7 +68,7 @@ export default abstract class HeaderButtons

    extends React.Component): void { const rps = RightPanelStore.instance; diff --git a/src/components/views/room_settings/AliasSettings.tsx b/src/components/views/room_settings/AliasSettings.tsx index 9c6b1cf2dc..d540ac61ad 100644 --- a/src/components/views/room_settings/AliasSettings.tsx +++ b/src/components/views/room_settings/AliasSettings.tsx @@ -15,8 +15,9 @@ limitations under the License. */ import React, { ChangeEvent, ContextType, createRef, SyntheticEvent } from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { logger } from "matrix-js-sdk/src/logger"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import EditableItemList from "../elements/EditableItemList"; import { _t } from "../../../languageHandler"; @@ -52,7 +53,7 @@ class EditableAliasesList extends EditableItemList { }; protected renderNewItemField(): JSX.Element { - const onChange = (alias: string): void => this.onNewItemChanged({ target: { value: alias } }); + const onChange = (alias: string): void => this.props.onNewItemChanged?.(alias); return (
    { canSetCanonicalAlias: false, }; - public constructor(props, context: ContextType) { + public constructor(props: IProps, context: ContextType) { super(props, context); - const state = { + const state: IState = { altAliases: [], // [ #alias:domain.tld, ... ] localAliases: [], // [ #alias:my-hs.tld, ... ] canonicalAlias: null, // #canonical:domain.tld @@ -140,7 +141,7 @@ export default class AliasSettings extends React.Component { try { const mxClient = this.context; - let localAliases = []; + let localAliases: string[] = []; const response = await mxClient.getLocalAliases(this.props.roomId); if (Array.isArray(response?.aliases)) { localAliases = response.aliases; @@ -164,14 +165,14 @@ export default class AliasSettings extends React.Component { updatingCanonicalAlias: true, }); - const eventContent = { + const eventContent: IContent = { alt_aliases: this.state.altAliases, }; if (alias) eventContent["alias"] = alias; this.context - .sendStateEvent(this.props.roomId, "m.room.canonical_alias", eventContent, "") + .sendStateEvent(this.props.roomId, EventType.RoomCanonicalAlias, eventContent, "") .catch((err) => { logger.error(err); Modal.createDialog(ErrorDialog, { @@ -195,7 +196,7 @@ export default class AliasSettings extends React.Component { updatingCanonicalAlias: true, }); - const eventContent = {}; + const eventContent: IContent = {}; if (this.state.canonicalAlias) { eventContent["alias"] = this.state.canonicalAlias; @@ -205,7 +206,7 @@ export default class AliasSettings extends React.Component { } this.context - .sendStateEvent(this.props.roomId, "m.room.canonical_alias", eventContent, "") + .sendStateEvent(this.props.roomId, EventType.RoomCanonicalAlias, eventContent, "") .then(() => { this.setState({ altAliases, diff --git a/src/components/views/room_settings/RoomPublishSetting.tsx b/src/components/views/room_settings/RoomPublishSetting.tsx index 673afccb77..d20ea9f92e 100644 --- a/src/components/views/room_settings/RoomPublishSetting.tsx +++ b/src/components/views/room_settings/RoomPublishSetting.tsx @@ -33,15 +33,15 @@ interface IState { } export default class RoomPublishSetting extends React.PureComponent { - public constructor(props, context) { - super(props, context); + public constructor(props: IProps) { + super(props); this.state = { isRoomPublished: false, }; } - private onRoomPublishChange = (e): void => { + private onRoomPublishChange = (): void => { const valueBefore = this.state.isRoomPublished; const newValue = !valueBefore; this.setState({ isRoomPublished: newValue }); diff --git a/src/components/views/rooms/AppsDrawer.tsx b/src/components/views/rooms/AppsDrawer.tsx index 25c8684c9c..38f119718e 100644 --- a/src/components/views/rooms/AppsDrawer.tsx +++ b/src/components/views/rooms/AppsDrawer.tsx @@ -45,8 +45,7 @@ interface IProps { } interface IState { - // @ts-ignore - TS wants a string key, but we know better - apps: { [id: Container]: IApp[] }; + apps: Partial<{ [id in Container]: IApp[] }>; resizingVertical: boolean; // true when changing the height of the apps drawer resizingHorizontal: boolean; // true when changing the distribution of the width between widgets resizing: boolean; @@ -203,10 +202,9 @@ export default class AppsDrawer extends React.Component { break; } }; - // @ts-ignore - TS wants a string key, but we know better - private getApps = (): { [id: Container]: IApp[] } => { - // @ts-ignore - const appsDict: { [id: Container]: IApp[] } = {}; + + private getApps = (): Partial<{ [id in Container]: IApp[] }> => { + const appsDict: Partial<{ [id in Container]: IApp[] }> = {}; appsDict[Container.Top] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top); appsDict[Container.Center] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center); return appsDict; diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index b88f6dbdb7..5f7f88d01c 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -57,7 +57,7 @@ export default class Autocomplete extends React.PureComponent { public static contextType = RoomContext; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 8492f57519..ccb36091af 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -55,7 +55,7 @@ export default class AuxPanel extends React.Component { showApps: true, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -76,7 +76,7 @@ export default class AuxPanel extends React.Component { } } - public shouldComponentUpdate(nextProps, nextState): boolean { + public shouldComponentUpdate(nextProps: IProps, nextState: IState): boolean { return objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState); } @@ -146,9 +146,9 @@ export default class AuxPanel extends React.Component { ); } - let stateViews = null; + let stateViews: JSX.Element | null = null; if (this.state.counters && SettingsStore.getValue("feature_state_counters")) { - const counters = []; + const counters: JSX.Element[] = []; this.state.counters.forEach((counter, idx) => { const title = counter.title; diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index eaf97a39f4..00daed475c 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -32,7 +32,7 @@ import { } from "../../../editor/operations"; import { getCaretOffsetAndText, getRangeForSelection } from "../../../editor/dom"; import Autocomplete, { generateCompletionDomId } from "../rooms/Autocomplete"; -import { getAutoCompleteCreator, Part, Type } from "../../../editor/parts"; +import { getAutoCompleteCreator, Part, SerializedPart, Type } from "../../../editor/parts"; import { parseEvent, parsePlainTextMessage } from "../../../editor/deserialize"; import { renderModel } from "../../../editor/render"; import SettingsStore from "../../../settings/SettingsStore"; @@ -107,7 +107,7 @@ interface IProps { initialCaret?: DocumentOffset; disabled?: boolean; - onChange?(); + onChange?(): void; onPaste?(event: ClipboardEvent, model: EditorModel): boolean; } @@ -140,7 +140,7 @@ export default class BasicMessageEditor extends React.Component private readonly surroundWithHandle: string; private readonly historyManager = new HistoryManager(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { showPillAvatar: SettingsStore.getValue("Pill.shouldShowPillAvatar"), @@ -367,7 +367,7 @@ export default class BasicMessageEditor extends React.Component let parts: Part[]; if (partsText) { const serializedTextParts = JSON.parse(partsText); - parts = serializedTextParts.map((p) => partCreator.deserializePart(p)); + parts = serializedTextParts.map((p: SerializedPart) => partCreator.deserializePart(p)); } else { parts = parsePlainTextMessage(plainText, partCreator, { shouldEscape: false }); } diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index a5e1156437..af734d25a7 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -29,7 +29,7 @@ import { getCaretOffsetAndText } from "../../../editor/dom"; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from "../../../editor/serialize"; import { findEditableEvent } from "../../../utils/EventUtils"; import { parseEvent } from "../../../editor/deserialize"; -import { CommandPartCreator, Part, PartCreator } from "../../../editor/parts"; +import { CommandPartCreator, Part, PartCreator, SerializedPart } from "../../../editor/parts"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer"; import { CommandCategories } from "../../../SlashCommands"; @@ -59,7 +59,7 @@ function getHtmlReplyFallback(mxEvent: MatrixEvent): string { } function getTextReplyFallback(mxEvent: MatrixEvent): string { - const body = mxEvent.getContent().body; + const body: string = mxEvent.getContent().body; const lines = body.split("\n").map((l) => l.trim()); if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) { return `${lines[0]}\n\n`; @@ -253,7 +253,7 @@ class EditMessageComposer extends React.Component partCreator.deserializePart(p)); + const parts: Part[] = serializedParts.map((p: SerializedPart) => partCreator.deserializePart(p)); return parts; } catch (e) { logger.error("Error parsing editing state: ", e); diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx index bfd5cece66..7bcc60541d 100644 --- a/src/components/views/rooms/EntityTile.tsx +++ b/src/components/views/rooms/EntityTile.tsx @@ -35,13 +35,15 @@ const PowerLabel: Record = { [PowerStatus.Moderator]: _td("Mod"), }; -const PRESENCE_CLASS = { +export type PresenceState = "offline" | "online" | "unavailable"; + +const PRESENCE_CLASS: Record = { offline: "mx_EntityTile_offline", online: "mx_EntityTile_online", unavailable: "mx_EntityTile_unavailable", }; -function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean): string { +function presenceClassForMember(presenceState: PresenceState, lastActiveAgo: number, showPresence: boolean): string { if (showPresence === false) { return "mx_EntityTile_online_beenactive"; } @@ -67,7 +69,7 @@ interface IProps { title?: string; avatarJsx?: JSX.Element; // className?: string; - presenceState?: string; + presenceState?: PresenceState; presenceLastActiveAgo?: number; presenceLastTs?: number; presenceCurrentlyActive?: boolean; @@ -104,7 +106,7 @@ export default class EntityTile extends React.PureComponent { } public render(): JSX.Element { - const mainClassNames = { + const mainClassNames: Record = { mx_EntityTile: true, mx_EntityTile_noHover: this.props.suppressOnHover, }; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d6c2052d08..e0780251c5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -617,8 +617,8 @@ export class UnwrappedEventTile extends React.Component } private propsEqual(objA: EventTileProps, objB: EventTileProps): boolean { - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); + const keysA = Object.keys(objA) as Array; + const keysB = Object.keys(objB) as Array; if (keysA.length !== keysB.length) { return false; diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx index 23bd19ff4d..3de8d68a89 100644 --- a/src/components/views/rooms/LinkPreviewWidget.tsx +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -37,7 +37,7 @@ interface IProps { export default class LinkPreviewWidget extends React.Component { private image = createRef(); - private onImageClick = (ev): void => { + private onImageClick = (ev: React.MouseEvent): void => { const p = this.props.preview; if (ev.button != 0 || ev.metaKey) return; ev.preventDefault(); diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 396e093986..0d48d0b267 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -68,7 +68,8 @@ interface IState { } export default class MemberList extends React.Component { - private showPresence = true; + // XXX: exported for tests + public showPresence = true; private mounted = false; public static contextType = SDKContext; @@ -195,7 +196,8 @@ export default class MemberList extends React.Component { { leading: true, trailing: true }, ); - private async updateListNow(showLoadingSpinner: boolean): Promise { + // XXX: exported for tests + public async updateListNow(showLoadingSpinner?: boolean): Promise { if (!this.mounted) { return; } diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx index 94b4771007..de544cd929 100644 --- a/src/components/views/rooms/MemberTile.tsx +++ b/src/components/views/rooms/MemberTile.tsx @@ -28,10 +28,11 @@ import dis from "../../../dispatcher/dispatcher"; import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { Action } from "../../../dispatcher/actions"; -import EntityTile, { PowerStatus } from "./EntityTile"; +import EntityTile, { PowerStatus, PresenceState } from "./EntityTile"; import MemberAvatar from "./../avatars/MemberAvatar"; import DisambiguatedProfile from "../messages/DisambiguatedProfile"; import UserIdentifierCustomisations from "../../../customisations/UserIdentifier"; +import { E2EState } from "./E2EIcon"; interface IProps { member: RoomMember; @@ -40,7 +41,7 @@ interface IProps { interface IState { isRoomEncrypted: boolean; - e2eStatus: string; + e2eStatus: E2EState; } export default class MemberTile extends React.Component { @@ -51,7 +52,7 @@ export default class MemberTile extends React.Component { showPresence: true, }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { @@ -121,7 +122,7 @@ export default class MemberTile extends React.Component { const userTrust = cli.checkUserTrust(userId); if (!userTrust.isCrossSigningVerified()) { this.setState({ - e2eStatus: userTrust.wasCrossSigningVerified() ? "warning" : "normal", + e2eStatus: userTrust.wasCrossSigningVerified() ? E2EState.Warning : E2EState.Normal, }); return; } @@ -138,7 +139,7 @@ export default class MemberTile extends React.Component { return isMe ? !deviceTrust.isCrossSigningVerified() : !deviceTrust.isVerified(); }); this.setState({ - e2eStatus: anyDeviceUnverified ? "warning" : "verified", + e2eStatus: anyDeviceUnverified ? E2EState.Warning : E2EState.Verified, }); } @@ -186,7 +187,7 @@ export default class MemberTile extends React.Component { public render(): JSX.Element { const member = this.props.member; const name = this.getDisplayName(); - const presenceState = member.user ? member.user.presence : null; + const presenceState = member.user?.presence ?? null; const av =