diff --git a/cypress/e2e/polls/polls.spec.ts b/cypress/e2e/polls/polls.spec.ts index 51d169d61b..f7e06e18f2 100644 --- a/cypress/e2e/polls/polls.spec.ts +++ b/cypress/e2e/polls/polls.spec.ts @@ -54,12 +54,12 @@ describe("Polls", () => { }; const getPollOption = (pollId: string, optionText: string): Chainable => { - return getPollTile(pollId).contains(".mx_MPollBody_option .mx_StyledRadioButton", optionText); + return getPollTile(pollId).contains(".mx_PollOption .mx_StyledRadioButton", optionText); }; const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => { getPollOption(pollId, optionText).within(() => { - cy.get(".mx_MPollBody_optionVoteCount").should("contain", `${votes} vote`); + cy.get(".mx_PollOption_optionVoteCount").should("contain", `${votes} vote`); }); }; 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/res/css/_components.pcss b/res/css/_components.pcss index 541178b926..cda1278df9 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -19,6 +19,7 @@ @import "./components/views/context_menus/_KebabContextMenu.pcss"; @import "./components/views/dialogs/polls/_PollListItem.pcss"; @import "./components/views/elements/_FilterDropdown.pcss"; +@import "./components/views/elements/_FilterTabGroup.pcss"; @import "./components/views/elements/_LearnMore.pcss"; @import "./components/views/location/_EnableLiveShare.pcss"; @import "./components/views/location/_LiveDurationDropdown.pcss"; @@ -32,6 +33,7 @@ @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/pips/_WidgetPip.pcss"; +@import "./components/views/polls/_PollOption.pcss"; @import "./components/views/settings/devices/_CurrentDeviceSection.pcss"; @import "./components/views/settings/devices/_DeviceDetailHeading.pcss"; @import "./components/views/settings/devices/_DeviceDetails.pcss"; diff --git a/res/css/components/views/elements/_FilterTabGroup.pcss b/res/css/components/views/elements/_FilterTabGroup.pcss new file mode 100644 index 0000000000..bbf1a279ad --- /dev/null +++ b/res/css/components/views/elements/_FilterTabGroup.pcss @@ -0,0 +1,46 @@ +/* +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. +*/ + +.mx_FilterTabGroup { + color: $primary-content; + label { + margin-right: $spacing-12; + cursor: pointer; + span { + display: inline-block; + line-height: $font-24px; + } + } + input[type="radio"] { + appearance: none; + margin: 0; + padding: 0; + + &:focus, + &:hover { + & + span { + color: $secondary-content; + } + } + + &:checked + span { + color: $accent; + font-weight: $font-semi-bold; + // underline + box-shadow: 0 1.5px 0 0 currentColor; + } + } +} diff --git a/res/css/components/views/polls/_PollOption.pcss b/res/css/components/views/polls/_PollOption.pcss new file mode 100644 index 0000000000..e5a97b7f2b --- /dev/null +++ b/res/css/components/views/polls/_PollOption.pcss @@ -0,0 +1,109 @@ +/* +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. +*/ + +.mx_PollOption { + border: 1px solid $quinary-content; + border-radius: 8px; + padding: 6px 12px; + max-width: 550px; + background-color: $background; + + .mx_StyledRadioButton_content, + .mx_PollOption_endedOption { + padding-top: 2px; + margin-right: 0px; + } + + .mx_StyledRadioButton_spacer { + display: none; + } +} + +.mx_PollOption, +/* label has cursor: default in user-agent stylesheet */ +/* override */ +.mx_PollOption_live-option { + cursor: pointer; +} + +.mx_PollOption_content { + display: flex; + justify-content: space-between; +} + +.mx_PollOption_optionVoteCount { + color: $secondary-content; + font-size: $font-12px; + white-space: nowrap; +} + +.mx_PollOption_winnerIcon { + height: 12px; + width: 12px; + color: $accent; + margin-right: $spacing-4; + vertical-align: middle; +} + +.mx_PollOption_checked { + border-color: $accent; + + .mx_PollOption_popularityBackground { + .mx_PollOption_popularityAmount { + background-color: $accent; + } + } + + // override checked radio button styling + // to show checkmark instead + .mx_StyledRadioButton_checked { + input[type="radio"] + div { + border-width: 2px; + border-color: $accent; + background-color: $accent; + background-image: url("$(res)/img/element-icons/check-white.svg"); + background-size: 12px; + background-repeat: no-repeat; + background-position: center; + + div { + visibility: hidden; + } + } + } +} + +/* options not actionable in these states */ +.mx_PollOption_checked, +.mx_PollOption_ended { + pointer-events: none; +} + +.mx_PollOption_popularityBackground { + width: 100%; + height: 8px; + margin-right: 12px; + border-radius: 8px; + background-color: $system; + margin-top: $spacing-8; + + .mx_PollOption_popularityAmount { + width: 0%; + height: 8px; + border-radius: 8px; + background-color: $quaternary-content; + } +} diff --git a/res/css/structures/_RoomStatusBar.pcss b/res/css/structures/_RoomStatusBar.pcss index e8393ac0ce..d3e08adfd6 100644 --- a/res/css/structures/_RoomStatusBar.pcss +++ b/res/css/structures/_RoomStatusBar.pcss @@ -115,6 +115,7 @@ limitations under the License. padding-left: 30px; /* 18px for the icon, 2px margin to text, 10px regular padding */ display: inline-block; position: relative; + user-select: none; &:nth-child(2) { border-left: 1px solid $resend-button-divider-color; diff --git a/res/css/views/messages/_MPollBody.pcss b/res/css/views/messages/_MPollBody.pcss index e9ea2bc3dc..193bd9382a 100644 --- a/res/css/views/messages/_MPollBody.pcss +++ b/res/css/views/messages/_MPollBody.pcss @@ -47,108 +47,6 @@ limitations under the License. mask-image: url("$(res)/img/element-icons/room/composer/poll.svg"); } - .mx_MPollBody_option { - border: 1px solid $quinary-content; - border-radius: 8px; - margin-bottom: 16px; - padding: 6px 12px; - max-width: 550px; - background-color: $background; - - .mx_StyledRadioButton, - .mx_MPollBody_endedOption { - margin-bottom: 8px; - } - - .mx_StyledRadioButton_content, - .mx_MPollBody_endedOption { - padding-top: 2px; - margin-right: 0px; - } - - .mx_StyledRadioButton_spacer { - display: none; - } - - .mx_MPollBody_optionDescription { - display: flex; - justify-content: space-between; - - .mx_MPollBody_optionVoteCount { - color: $secondary-content; - font-size: $font-12px; - white-space: nowrap; - } - } - - .mx_MPollBody_popularityBackground { - width: 100%; - height: 8px; - margin-right: 12px; - border-radius: 8px; - background-color: $system; - - .mx_MPollBody_popularityAmount { - width: 0%; - height: 8px; - border-radius: 8px; - background-color: $quaternary-content; - } - } - } - - .mx_MPollBody_option:last-child { - margin-bottom: 8px; - } - - .mx_MPollBody_option_checked { - border-color: $accent; - - .mx_MPollBody_popularityBackground { - .mx_MPollBody_popularityAmount { - background-color: $accent; - } - } - } - - /* options not actionable in these states */ - .mx_MPollBody_option_checked, - .mx_MPollBody_option_ended { - pointer-events: none; - } - - .mx_StyledRadioButton_checked, - .mx_MPollBody_endedOptionWinner { - input[type="radio"] + div { - border-width: 2px; - border-color: $accent; - background-color: $accent; - background-image: url("$(res)/img/element-icons/check-white.svg"); - background-size: 12px; - background-repeat: no-repeat; - background-position: center; - - div { - visibility: hidden; - } - } - } - - .mx_MPollBody_endedOptionWinner .mx_MPollBody_optionDescription .mx_MPollBody_optionVoteCount::before { - content: ""; - position: relative; - display: inline-block; - margin-right: 4px; - top: 2px; - height: 12px; - width: 12px; - background-color: $accent; - mask-repeat: no-repeat; - mask-size: contain; - mask-position: center; - mask-image: url("$(res)/img/element-icons/trophy.svg"); - } - .mx_MPollBody_totalVotes { display: flex; flex-direction: inline; @@ -168,9 +66,8 @@ limitations under the License. pointer-events: none; } -.mx_MPollBody_option, -/* label has cursor: default in user-agent stylesheet */ -/* override */ -.mx_MPollBody_live-option { - cursor: pointer; +.mx_MPollBody_allOptions { + display: grid; + grid-gap: $spacing-16; + margin-bottom: $spacing-8; } diff --git a/res/img/element-icons/trophy.svg b/res/img/element-icons/trophy.svg index 7caf61fd35..99f4831b57 100644 --- a/res/img/element-icons/trophy.svg +++ b/res/img/element-icons/trophy.svg @@ -1,3 +1,3 @@ - + 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..e4e12bedfe 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 @@ -77,7 +74,7 @@ export default class AsyncWrapper extends React.Component { this.props.onFinished(false); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.component) { const Component = this.state.component; return ; diff --git a/src/Avatar.ts b/src/Avatar.ts index 986ae6306a..391495030c 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -138,7 +138,7 @@ export function getInitialLetter(name: string): string | undefined { } export function avatarUrlForRoom( - room: Room, + room: Room | null, width: number, height: number, resizeMethod?: ResizeMethod, 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..e8b38d50c5 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -204,7 +204,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { attribs.style += "height: 100%;"; } - attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height); + attribs.src = mediaFromMxc(src).getThumbnailOfSourceHttp(width, height)!; return { tagName, attribs }; }, "code": function (tagName: string, attribs: sanitizeHtml.Attributes) { @@ -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 @@ -352,7 +352,7 @@ const topicSanitizeHtmlParams: IExtendedSanitizeOptions = { }; abstract class BaseHighlighter { - public constructor(public highlightClass: string, public highlightLink: string) {} + public constructor(public highlightClass: string, public highlightLink?: string) {} /** * apply the highlights to a section of text @@ -504,7 +504,7 @@ function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | s export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnString): string; export function bodyToHtml(content: IContent, highlights: Optional, opts: IOptsReturnNode): ReactNode; export function bodyToHtml(content: IContent, highlights: Optional, opts: IOpts = {}): ReactNode | string { - const isFormattedBody = content.format === "org.matrix.custom.html" && !!content.formatted_body; + const isFormattedBody = content.format === "org.matrix.custom.html" && typeof content.formatted_body === "string"; let bodyHasEmoji = false; let isHtmlMessage = false; @@ -514,7 +514,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op } let strippedBody: string; - let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext + let safeBody: string | undefined; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext try { // sanitizeHtml can hang if an unclosed HTML tag is thrown at it @@ -529,7 +529,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody); strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody; - bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody : plainBody); + bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody! : plainBody); const highlighter = safeHighlights?.length ? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink) @@ -543,11 +543,11 @@ export function bodyToHtml(content: IContent, highlights: Optional, op // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure. sanitizeParams.textFilter = function (safeText) { - return highlighter.applyHighlights(safeText, safeHighlights).join(""); + return highlighter.applyHighlights(safeText, safeHighlights!).join(""); }; } - safeBody = sanitizeHtml(formattedBody, sanitizeParams); + safeBody = sanitizeHtml(formattedBody!, sanitizeParams); const phtml = cheerio.load(safeBody, { // @ts-ignore: The `_useHtmlParser2` internal option is the // simplest way to both parse and render using `htmlparser2`. @@ -574,7 +574,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op safeBody = formatEmojis(safeBody, true).join(""); } } else if (highlighter) { - safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join(""); + safeBody = highlighter.applyHighlights(plainBody, safeHighlights!).join(""); } } finally { delete sanitizeParams.textFilter; @@ -597,9 +597,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed); emojiBody = - match && - match[0] && - match[0].length === contentBodyTrimmed.length && + match?.[0]?.length === contentBodyTrimmed.length && // Prevent user pills expanding for users with only emoji in // their username. Permalinks (links in pills) can be any URL // now, so we just check for an HTTP-looking thing. @@ -614,7 +612,7 @@ export function bodyToHtml(content: IContent, highlights: Optional, op "markdown-body": isHtmlMessage && !emojiBody, }); - let emojiBodyElements: JSX.Element[]; + let emojiBodyElements: JSX.Element[] | undefined; if (!safeBody && bodyHasEmoji) { emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[]; } @@ -649,7 +647,7 @@ export function topicToHtml( allowExtendedHtml = false, ): ReactNode { if (!SettingsStore.getValue("feature_html_topic")) { - htmlTopic = null; + htmlTopic = undefined; } let isFormattedTopic = !!htmlTopic; @@ -657,10 +655,10 @@ export function topicToHtml( let safeTopic = ""; try { - topicHasEmoji = mightContainEmoji(isFormattedTopic ? htmlTopic : topic); + topicHasEmoji = mightContainEmoji(isFormattedTopic ? htmlTopic! : topic); if (isFormattedTopic) { - safeTopic = sanitizeHtml(htmlTopic, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams); + safeTopic = sanitizeHtml(htmlTopic!, allowExtendedHtml ? sanitizeHtmlParams : topicSanitizeHtmlParams); if (topicHasEmoji) { safeTopic = formatEmojis(safeTopic, true).join(""); } @@ -669,7 +667,7 @@ export function topicToHtml( isFormattedTopic = false; // Fall back to plain-text topic } - let emojiBodyElements: ReturnType; + let emojiBodyElements: ReturnType | undefined; if (!isFormattedTopic && topicHasEmoji) { emojiBodyElements = formatEmojis(topic, false); } 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/Markdown.ts b/src/Markdown.ts index a32126117d..89cfcf65fe 100644 --- a/src/Markdown.ts +++ b/src/Markdown.ts @@ -139,7 +139,7 @@ export default class Markdown { */ private repairLinks(parsed: commonmark.Node): commonmark.Node { const walker = parsed.walker(); - let event: commonmark.NodeWalkingStep = null; + let event: commonmark.NodeWalkingStep | null = null; let text = ""; let isInPara = false; let previousNode: commonmark.Node | null = null; @@ -287,7 +287,7 @@ export default class Markdown { // However, if it's a blockquote, adds a p tag anyway // in order to avoid deviation to commonmark and unexpected // results when parsing the formatted HTML. - if (node.parent.type === "block_quote" || isMultiLine(node)) { + if (node.parent?.type === "block_quote" || isMultiLine(node)) { realParagraph.call(this, node, entering); } }; 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..b8c3f855ac 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; }); } @@ -120,7 +120,7 @@ export default class NodeAnimator extends React.Component { this.nodes[k] = node; } - public render(): JSX.Element { + public render(): React.ReactNode { return <>{Object.values(this.children)}; } } 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/PosthogTrackers.ts b/src/PosthogTrackers.ts index 8a8b02965c..a82b78c1dd 100644 --- a/src/PosthogTrackers.ts +++ b/src/PosthogTrackers.ts @@ -120,7 +120,7 @@ export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName PosthogTrackers.instance.clearOverride(this.props.screenName); } - public render(): JSX.Element { + public render(): React.ReactNode { return null; // no need to render anything, we just need to hook into the React lifecycle } } diff --git a/src/Presence.ts b/src/Presence.ts index c13cc32b60..02d2ef0e7e 100644 --- a/src/Presence.ts +++ b/src/Presence.ts @@ -33,9 +33,9 @@ enum State { } class Presence { - private unavailableTimer: Timer = null; - private dispatcherRef: string = null; - private state: State = null; + private unavailableTimer: Timer | null = null; + private dispatcherRef: string | null = null; + private state: State | null = null; /** * Start listening the user activity to evaluate his presence state. @@ -73,14 +73,14 @@ class Presence { * Get the current presence state. * @returns {string} the presence state (see PRESENCE enum) */ - public getState(): State { + public getState(): State | null { return this.state; } private onAction = (payload: ActionPayload): void => { if (payload.action === "user_activity") { this.setState(State.Online); - this.unavailableTimer.restart(); + this.unavailableTimer?.restart(); } }; diff --git a/src/Resend.ts b/src/Resend.ts index bc62c62efa..17e39a7e29 100644 --- a/src/Resend.ts +++ b/src/Resend.ts @@ -46,7 +46,7 @@ export default class Resend { } public static resend(event: MatrixEvent): Promise { - const room = MatrixClientPeg.get().getRoom(event.getRoomId()); + const room = MatrixClientPeg.get().getRoom(event.getRoomId())!; return MatrixClientPeg.get() .resendEvent(event, room) .then( diff --git a/src/RoomAliasCache.ts b/src/RoomAliasCache.ts index c318db2d3f..f565d8d2d3 100644 --- a/src/RoomAliasCache.ts +++ b/src/RoomAliasCache.ts @@ -30,6 +30,6 @@ export function storeRoomAliasInCache(alias: string, id: string): void { aliasToIDMap.set(alias, id); } -export function getCachedRoomIDForAlias(alias: string): string { +export function getCachedRoomIDForAlias(alias: string): string | undefined { return aliasToIDMap.get(alias); } diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index 582eb360f8..246e9e5a37 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -112,7 +112,7 @@ export function inviteUsersToRoom( ): Promise { return inviteMultipleToRoom(roomId, userIds, sendSharedHistoryKeys, progressCallback) .then((result) => { - const room = MatrixClientPeg.get().getRoom(roomId); + const room = MatrixClientPeg.get().getRoom(roomId)!; showAnyInviteErrors(result.states, room, result.inviter); }) .catch((err) => { @@ -175,14 +175,14 @@ export function showAnyInviteErrors(
{name} - {user.userId} + {user?.userId}
{inviter.getErrorText(addr)} diff --git a/src/RoomNotifs.ts b/src/RoomNotifs.ts index c29105b780..aa0d89df79 100644 --- a/src/RoomNotifs.ts +++ b/src/RoomNotifs.ts @@ -46,7 +46,7 @@ export function getRoomNotifsState(client: MatrixClient, roomId: string): RoomNo } // for everything else, look at the room rule. - let roomRule = null; + let roomRule: IPushRule | undefined; try { roomRule = client.getRoomPushRule("global", roomId); } catch (err) { @@ -106,7 +106,7 @@ export function getUnreadNotificationCount(room: Room, type: NotificationCountTy function setRoomNotifsStateMuted(roomId: string): Promise { const cli = MatrixClientPeg.get(); - const promises = []; + const promises: Promise[] = []; // delete the room rule const roomRule = cli.getRoomPushRule("global", roomId); @@ -137,7 +137,7 @@ function setRoomNotifsStateMuted(roomId: string): Promise { function setRoomNotifsStateUnmuted(roomId: string, newState: RoomNotifState): Promise { const cli = MatrixClientPeg.get(); - const promises = []; + const promises: Promise[] = []; const overrideMuteRule = findOverrideMuteRule(roomId); if (overrideMuteRule) { diff --git a/src/Rooms.ts b/src/Rooms.ts index 8d10f121af..250f385206 100644 --- a/src/Rooms.ts +++ b/src/Rooms.ts @@ -29,13 +29,13 @@ import AliasCustomisations from "./customisations/Alias"; * @param {Object} room The room object * @returns {string} A display alias for the given room */ -export function getDisplayAliasForRoom(room: Room): string | undefined { +export function getDisplayAliasForRoom(room: Room): string | null { return getDisplayAliasForAliasSet(room.getCanonicalAlias(), room.getAltAliases()); } // The various display alias getters should all feed through this one path so // there's a single place to change the logic. -export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: string[]): string { +export function getDisplayAliasForAliasSet(canonicalAlias: string | null, altAliases: string[]): string | null { if (AliasCustomisations.getDisplayAliasForAliasSet) { return AliasCustomisations.getDisplayAliasForAliasSet(canonicalAlias, altAliases); } @@ -45,7 +45,7 @@ export function getDisplayAliasForAliasSet(canonicalAlias: string, altAliases: s export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise { let newTarget; if (isDirect) { - const guessedUserId = guessDMRoomTargetId(room, MatrixClientPeg.get().getUserId()); + const guessedUserId = guessDMRoomTargetId(room, MatrixClientPeg.get().getUserId()!); newTarget = guessedUserId; } else { newTarget = null; @@ -118,7 +118,7 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string { if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; - oldestTs = user.events.member.getTs(); + oldestTs = user.events.member?.getTs(); } } if (oldestUser) return oldestUser.userId; @@ -129,7 +129,7 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string { if (oldestTs === undefined || (user.events.member && user.events.member.getTs() < oldestTs)) { oldestUser = user; - oldestTs = user.events.member.getTs(); + oldestTs = user.events.member?.getTs(); } } 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 3434090d8e..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; @@ -1042,7 +1039,7 @@ export const Commands = [ throw newTranslatableError("Session already verified!"); } else { throw newTranslatableError( - "WARNING: Session already verified, but keys do NOT MATCH!", + "WARNING: session already verified, but keys do NOT MATCH!", ); } } 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 9217aca3c0..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,11 +182,12 @@ export default class UserActivity { this.activeRecentlyTimeout.abort(); }; - private onUserActivity = (event: MouseEvent): void => { + // XXX: exported for tests + public onUserActivity = (event: Event): void => { // ignore anything if the window isn't focused if (!this.document.hasFocus()) return; - if (event.screenX && event.type === "mousemove") { + if (event.type === "mousemove" && this.isMouseEvent(event)) { if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { // mouse hasn't actually moved return; @@ -223,4 +224,8 @@ export default class UserActivity { } attachedTimers.forEach((t) => t.abort()); } + + private isMouseEvent(event: Event): event is MouseEvent { + return event.type.startsWith("mouse"); + } } diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 7a163350db..0214ab9cbe 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -38,7 +38,7 @@ export default class VoipUserMapper { return window.mxVoipUserMapper; } - private async userToVirtualUser(userId: string): Promise { + private async userToVirtualUser(userId: string): Promise { const results = await LegacyCallHandler.instance.sipVirtualLookup(userId); if (results.length === 0 || !results[0].fields.lookup_success) return null; return results[0].userid; @@ -59,11 +59,11 @@ export default class VoipUserMapper { if (!virtualUser) return null; const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId); - MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, { + MatrixClientPeg.get().setRoomAccountData(virtualRoomId!, VIRTUAL_ROOM_EVENT_TYPE, { native_room: roomId, }); - this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId); + this.virtualToNativeRoomIdCache.set(virtualRoomId!, roomId); return virtualRoomId; } @@ -72,9 +72,9 @@ export default class VoipUserMapper { * Gets the ID of the virtual room for a room, or null if the room has no * virtual room */ - public async getVirtualRoomForRoom(roomId: string): Promise { + public async getVirtualRoomForRoom(roomId: string): Promise { const virtualUser = await this.getVirtualUserForRoom(roomId); - if (!virtualUser) return null; + if (!virtualUser) return undefined; return findDMForUser(MatrixClientPeg.get(), virtualUser); } @@ -121,8 +121,12 @@ export default class VoipUserMapper { if (!LegacyCallHandler.instance.getSupportsVirtualRooms()) return; const inviterId = invitedRoom.getDMInviter(); + if (!inviterId) { + logger.error("Could not find DM inviter for room id: " + invitedRoom.roomId); + } + logger.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); - const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId); + const result = await LegacyCallHandler.instance.sipNativeLookup(inviterId!); if (result.length === 0) { return; } @@ -141,11 +145,11 @@ export default class VoipUserMapper { // (possibly we should only join if we've also joined the native room, then we'd also have // to make sure we joined virtual rooms on joining a native one) MatrixClientPeg.get().joinRoom(invitedRoom.roomId); - } - // also put this room in the virtual room ID cache so isVirtualRoom return the right answer - // in however long it takes for the echo of setAccountData to come down the sync - this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); + // also put this room in the virtual room ID cache so isVirtualRoom return the right answer + // in however long it takes for the echo of setAccountData to come down the sync + this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId); + } } } } diff --git a/src/WhoIsTyping.ts b/src/WhoIsTyping.ts index 01e7b2e4f7..d4a43636ce 100644 --- a/src/WhoIsTyping.ts +++ b/src/WhoIsTyping.ts @@ -21,11 +21,11 @@ import { MatrixClientPeg } from "./MatrixClientPeg"; import { _t } from "./languageHandler"; export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] { - return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers())); + return usersTyping(room, [MatrixClientPeg.get().getUserId()!].concat(MatrixClientPeg.get().getIgnoredUsers())); } export function usersTypingApartFromMe(room: Room): RoomMember[] { - return usersTyping(room, [MatrixClientPeg.get().getUserId()]); + return usersTyping(room, [MatrixClientPeg.get().getUserId()!]); } /** @@ -36,7 +36,7 @@ export function usersTypingApartFromMe(room: Room): RoomMember[] { * @returns {RoomMember[]} list of user objects who are typing. */ export function usersTyping(room: Room, exclude: string[] = []): RoomMember[] { - const whoIsTyping = []; + const whoIsTyping: RoomMember[] = []; const memberKeys = Object.keys(room.currentState.members); for (const userId of memberKeys) { 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..5393ae3fc6 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,12 +138,12 @@ 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); }; - public render(): JSX.Element { + public render(): React.ReactNode { const brand = SdkConfig.get().brand; let crawlerState; diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx index a75b41f602..654ed4bb2f 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.tsx @@ -239,7 +239,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent

{_t( - "Warning: You should only set up key backup from a trusted computer.", + "Warning: you should only set up key backup from a trusted computer.", {}, { b: (sub) => {sub} }, )} @@ -459,7 +459,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { return f.stages.length === 1 && f.stages[0] === "m.login.password"; }); this.setState({ @@ -842,7 +842,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent } as Pick); }; - public render(): JSX.Element { + public render(): React.ReactNode { const disableForm = this.state.phase === Phase.Exporting; return ( @@ -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..5d3864e811 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, }); }; @@ -127,7 +127,7 @@ export default class ImportE2eKeysDialog extends React.Component return false; }; - public render(): JSX.Element { + public render(): React.ReactNode { const disableForm = this.state.phase !== Phase.Edit; return ( diff --git a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx index fbe94cb8e6..fbb823dd8e 100644 --- a/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx +++ b/src/async-components/views/dialogs/security/NewRecoveryMethodDialog.tsx @@ -48,13 +48,13 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onFinished: this.props.onFinished, }, - null, + undefined, /* priority = */ false, /* static = */ true, ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const title = {_t("New Recovery Method")}; const newMethodDetected =

{_t("A new Security Phrase and key for Secure Messages have been detected.")}

; diff --git a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx index 1a7e79b9d2..f795a3e534 100644 --- a/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx +++ b/src/async-components/views/dialogs/security/RecoveryMethodRemovedDialog.tsx @@ -37,14 +37,14 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent>, - null, + undefined, null, /* priority = */ false, /* static = */ true, ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const title = {_t("Recovery Method Removed")}; return ( diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index 0828a3df1d..939d76d0a5 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -91,7 +91,7 @@ export class PlaybackQueue { public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback): void { // We don't ever detach our listeners: we expect the Playback to clean up for us - this.playbacks.set(mxEvent.getId(), playback); + this.playbacks.set(mxEvent.getId()!, playback); playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state)); playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock)); } @@ -99,12 +99,12 @@ export class PlaybackQueue { private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState): void { // Remember where the user got to in playback const wasLastPlaying = this.currentPlaybackId === mxEvent.getId(); - if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) { + if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()!) && !wasLastPlaying) { // noinspection JSIgnoredPromiseFromCall - playback.skipTo(this.clockStates.get(mxEvent.getId())!); + playback.skipTo(this.clockStates.get(mxEvent.getId()!)!); } else if (newState === PlaybackState.Stopped) { // Remove the now-useless clock for some space savings - this.clockStates.delete(mxEvent.getId()); + this.clockStates.delete(mxEvent.getId()!); if (wasLastPlaying) { this.recentFullPlays.add(this.currentPlaybackId); @@ -133,7 +133,7 @@ export class PlaybackQueue { // timeline is already most recent last, so we can iterate down that. const timeline = arrayFastClone(this.room.getLiveTimeline().getEvents()); let scanForVoiceMessage = false; - let nextEv: MatrixEvent; + let nextEv: MatrixEvent | undefined; for (const event of timeline) { if (event.getId() === mxEvent.getId()) { scanForVoiceMessage = true; @@ -149,8 +149,8 @@ export class PlaybackQueue { break; // Stop automatic playback: next useful event is not a voice message } - const havePlayback = this.playbacks.has(event.getId()); - const isRecentlyCompleted = this.recentFullPlays.has(event.getId()); + const havePlayback = this.playbacks.has(event.getId()!); + const isRecentlyCompleted = this.recentFullPlays.has(event.getId()!); if (havePlayback && !isRecentlyCompleted) { nextEv = event; break; @@ -164,7 +164,7 @@ export class PlaybackQueue { } else { this.playbackIdOrder = orderClone; - const instance = this.playbacks.get(nextEv.getId()); + const instance = this.playbacks.get(nextEv.getId()!); PlaybackManager.instance.pauseAllExcept(instance); // This should cause a Play event, which will re-populate our playback order @@ -196,7 +196,7 @@ export class PlaybackQueue { } } - this.currentPlaybackId = mxEvent.getId(); + this.currentPlaybackId = mxEvent.getId()!; if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { order.push(this.currentPlaybackId); } @@ -214,7 +214,7 @@ export class PlaybackQueue { if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values if (playback.currentState !== PlaybackState.Stopped) { - this.clockStates.set(mxEvent.getId(), clocks[0]); // [0] is the current seek position + this.clockStates.set(mxEvent.getId()!, clocks[0]); // [0] is the current seek position } } } 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..f4d7905c33 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"; @@ -78,7 +77,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { private targetMaxLength: number | null = TARGET_MAX_LENGTH; public amplitudes: number[] = []; // at each second mark, generated private liveWaveform = new FixedRollingArray(RECORDING_PLAYBACK_SAMPLES, 0); - public onDataAvailable: (data: ArrayBuffer) => void; + public onDataAvailable?: (data: ArrayBuffer) => void; public get contentType(): string { return "audio/ogg"; @@ -182,7 +181,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { }); // not using EventEmitter here because it leads to detached bufferes - this.recorder.ondataavailable = (data: ArrayBuffer) => this?.onDataAvailable(data); + this.recorder.ondataavailable = (data: ArrayBuffer) => this.onDataAvailable?.(data); } catch (e) { logger.error("Error starting recording: ", e); if (e instanceof DOMException) { 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/AutocompleteProvider.tsx b/src/autocomplete/AutocompleteProvider.tsx index 32c2d80c6f..0b1fe2dd9e 100644 --- a/src/autocomplete/AutocompleteProvider.tsx +++ b/src/autocomplete/AutocompleteProvider.tsx @@ -70,7 +70,7 @@ export default abstract class AutocompleteProvider { * @param {boolean} force True if the user is forcing completion * @return {object} { command, range } where both objects fields are null if no match */ - public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand | null { + public getCurrentCommand(query: string, selection: ISelectionRange, force = false): Partial { let commandRegex = this.commandRegex; if (force && this.shouldForceComplete()) { @@ -78,7 +78,7 @@ export default abstract class AutocompleteProvider { } if (!commandRegex) { - return null; + return {}; } commandRegex.lastIndex = 0; diff --git a/src/autocomplete/CommandProvider.tsx b/src/autocomplete/CommandProvider.tsx index 113c928790..995760f4b3 100644 --- a/src/autocomplete/CommandProvider.tsx +++ b/src/autocomplete/CommandProvider.tsx @@ -95,7 +95,7 @@ export default class CommandProvider extends AutocompleteProvider { description={_t(result.description)} /> ), - range, + range: range!, }; }); } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index cc25068db8..9c0acff4cd 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; @@ -90,7 +94,7 @@ export default class EmojiProvider extends AutocompleteProvider { shouldMatchWordsOnly: true, }); - this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))); + this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean))) as IEmoji[]; } public async getCompletions( @@ -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 || "")); @@ -148,7 +152,7 @@ export default class EmojiProvider extends AutocompleteProvider { {c.emoji.unicode} ), - range, + range: range!, })); } return []; diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 5efe0e86f6..d4a4793ab5 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -40,12 +40,13 @@ export default class NotifProvider extends AutocompleteProvider { ): Promise { const client = MatrixClientPeg.get(); - if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId)) return []; + if (!this.room.currentState.mayTriggerNotifOfType("room", client.credentials.userId!)) return []; const { command, range } = this.getCurrentCommand(query, selection, force); if ( - command?.[0].length > 1 && - ["@room", "@channel", "@everyone", "@here"].some((c) => c.startsWith(command[0])) + command?.[0] && + command[0].length > 1 && + ["@room", "@channel", "@everyone", "@here"].some((c) => c.startsWith(command![0])) ) { return [ { @@ -58,7 +59,7 @@ export default class NotifProvider extends AutocompleteProvider { ), - range, + range: range!, }, ]; } diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts index 1f7b5a5a7f..031f0b0122 100644 --- a/src/autocomplete/QueryMatcher.ts +++ b/src/autocomplete/QueryMatcher.ts @@ -88,7 +88,7 @@ export default class QueryMatcher { if (!this._items.has(key)) { this._items.set(key, []); } - this._items.get(key).push({ + this._items.get(key)!.push({ keyWeight: Number(index), object, }); @@ -104,7 +104,11 @@ export default class QueryMatcher { if (query.length === 0) { return []; } - const matches = []; + const matches: { + index: number; + object: T; + keyWeight: number; + }[] = []; // Iterate through the map & check each key. // ES6 Map iteration order is defined to be insertion order, so results // here will come out in the order they were put in. diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 9cde501365..3b86f16f1c 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -39,12 +39,12 @@ function canonicalScore(displayedAlias: string, room: Room): number { function matcherObject( room: Room, - displayedAlias: string | null, + displayedAlias: string, matchName = "", ): { room: Room; matchName: string; - displayedAlias: string | null; + displayedAlias: string; } { return { room, @@ -81,7 +81,7 @@ export default class RoomProvider extends AutocompleteProvider { // the only reason we need to do this is because Fuse only matches on properties let matcherObjects = this.getRooms().reduce[]>((aliases, room) => { if (room.getCanonicalAlias()) { - aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name)); + aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias()!, room.name)); } if (room.getAltAliases().length) { const altAliases = room.getAltAliases().map((alias) => matcherObject(room, alias)); @@ -122,7 +122,7 @@ export default class RoomProvider extends AutocompleteProvider { ), - range, + range: range!, }), ) .filter((completion) => !!completion.completion && completion.completion.length > 0); diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx index 65de4b1bb4..0ba5f656d8 100644 --- a/src/autocomplete/UserProvider.tsx +++ b/src/autocomplete/UserProvider.tsx @@ -44,7 +44,7 @@ const FORCED_USER_REGEX = /[^/,:; \t\n]\S*/g; export default class UserProvider extends AutocompleteProvider { public matcher: QueryMatcher; - public users: RoomMember[]; + public users: RoomMember[] | null; public room: Room; public constructor(room: Room, renderingType?: TimelineRenderingType) { @@ -54,7 +54,7 @@ export default class UserProvider extends AutocompleteProvider { renderingType, }); this.room = room; - this.matcher = new QueryMatcher([], { + this.matcher = new QueryMatcher([], { keys: ["name"], funcs: [(obj) => obj.userId.slice(1)], // index by user id minus the leading '@' shouldMatchWordsOnly: false, @@ -73,7 +73,7 @@ export default class UserProvider extends AutocompleteProvider { private onRoomTimeline = ( ev: MatrixEvent, - room: Room | null, + room: Room | undefined, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData, @@ -110,18 +110,15 @@ 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) => { - const description = UserIdentifierCustomisations.getDisplayUserIdentifier(user.userId, { + return this.matcher.match(query, limit).map((user) => { + const description = UserIdentifierCustomisations.getDisplayUserIdentifier?.(user.userId, { roomId: this.room.roomId, withDisplayName: true, }); @@ -132,18 +129,18 @@ export default class UserProvider extends AutocompleteProvider { completion: user.rawDisplayName, completionId: user.userId, type: "user", - suffix: selection.beginning && range.start === 0 ? ": " : " ", + suffix: selection.beginning && range!.start === 0 ? ": " : " ", href: makeUserPermalink(user.userId), component: ( ), - range, + range: range!, }; }); } - return completions; + return []; } public getName(): string { @@ -152,10 +149,10 @@ 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(); + lastSpoken[event.getSender()!] = event.getTs(); } const currentUserId = MatrixClientPeg.get().credentials.userId; @@ -167,7 +164,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher.setObjects(this.users); } - public onUserSpoke(user: RoomMember): void { + public onUserSpoke(user: RoomMember | null): void { if (!this.users) return; if (!user) return; if (user.userId === MatrixClientPeg.get().credentials.userId) return; diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 90fda3fe21..efbbbccb55 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -55,7 +55,7 @@ export default class AutoHideScrollbar ex } } - public render(): JSX.Element { + public render(): React.ReactNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { element, className, onScroll, tabIndex, wrappedRef, children, ...otherProps } = this.props; 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/EmbeddedPage.tsx b/src/components/structures/EmbeddedPage.tsx index e3cacf0114..cbd7d885c7 100644 --- a/src/components/structures/EmbeddedPage.tsx +++ b/src/components/structures/EmbeddedPage.tsx @@ -118,7 +118,7 @@ export default class EmbeddedPage extends React.PureComponent { } }; - public render(): JSX.Element { + public render(): React.ReactNode { // HACK: Workaround for the context's MatrixClient not updating. const client = this.context || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index 4390dcf36e..1b53b7d293 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, }; @@ -223,7 +223,7 @@ class FilePanel extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { if (MatrixClientPeg.get().isGuest()) { return ( 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/GenericErrorPage.tsx b/src/components/structures/GenericErrorPage.tsx index 4261d9b2f4..4f348daf01 100644 --- a/src/components/structures/GenericErrorPage.tsx +++ b/src/components/structures/GenericErrorPage.tsx @@ -22,7 +22,7 @@ interface IProps { } export default class GenericErrorPage extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return (
diff --git a/src/components/structures/IndicatorScrollbar.tsx b/src/components/structures/IndicatorScrollbar.tsx index 1476198239..c7cde582d3 100644 --- a/src/components/structures/IndicatorScrollbar.tsx +++ b/src/components/structures/IndicatorScrollbar.tsx @@ -177,7 +177,7 @@ export default class IndicatorScrollbar e } }; - public render(): JSX.Element { + public render(): React.ReactNode { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props; diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 99be8705a4..fd83f0aea2 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..7b01f1e405 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,13 +615,11 @@ 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 { + public render(): React.ReactNode { let pageElement; switch (this.props.page_type) { diff --git a/src/components/structures/MainSplit.tsx b/src/components/structures/MainSplit.tsx index 1254fbc6f3..e945496311 100644 --- a/src/components/structures/MainSplit.tsx +++ b/src/components/structures/MainSplit.tsx @@ -47,7 +47,7 @@ export default class MainSplit extends React.Component { }; private loadSidePanelSize(): { height: string | number; width: number } { - let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size"), 10); + let rhsSize = parseInt(window.localStorage.getItem("mx_rhs_size")!, 10); if (isNaN(rhsSize)) { rhsSize = 350; @@ -59,7 +59,7 @@ export default class MainSplit extends React.Component { }; } - public render(): JSX.Element { + public render(): React.ReactNode { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7fdda93eb6..66f681476b 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 => { @@ -2022,7 +2021,7 @@ export default class MatrixChat extends React.PureComponent { return fragmentAfterLogin; } - public render(): JSX.Element { + public render(): React.ReactNode { const fragmentAfterLogin = this.getFragmentAfterLogin(); let view = null; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 2dd432cb92..94265b11fa 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) { @@ -982,7 +982,7 @@ export default class MessagePanel extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { let topSpinner; let bottomSpinner; if (this.props.backPaginating) { diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx index 813522ffcb..f5f5b29d63 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, @@ -45,7 +45,7 @@ export default class NonUrgentToastContainer extends React.PureComponent { return (
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index ac351399d4..67e3523f38 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 = { @@ -55,7 +55,7 @@ export default class NotificationPanel extends React.PureComponent

{_t("You're all caught up")}

diff --git a/src/components/structures/PictureInPictureDragger.tsx b/src/components/structures/PictureInPictureDragger.tsx index 40c1caee6f..2456e63303 100644 --- a/src/components/structures/PictureInPictureDragger.tsx +++ b/src/components/structures/PictureInPictureDragger.tsx @@ -245,7 +245,7 @@ export default class PictureInPictureDragger extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { const style = { transform: `translateX(${this.translationX}px) translateY(${this.translationY}px)`, }; diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 0697f4a6da..d2cde6a76e 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { MutableRefObject, useContext, useRef } from "react"; +import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { logger } from "matrix-js-sdk/src/logger"; import { Optional } from "matrix-events-sdk"; @@ -288,7 +288,7 @@ class PipContainerInner extends React.Component { ); } - public render(): JSX.Element { + public render(): ReactNode { const pipMode = true; let pipContent: Array = []; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 3748ee0ec7..4f27392e25 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 = { @@ -149,7 +149,7 @@ export default class RightPanel extends React.Component { this.setState({ searchQuery }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let card =
; const roomId = this.props.room?.roomId; const phase = this.props.overwriteCard?.phase ?? this.state.phase; diff --git a/src/components/structures/RoomSearchView.tsx b/src/components/structures/RoomSearchView.tsx index 132e2a191b..93ed8e4c51 100644 --- a/src/components/structures/RoomSearchView.tsx +++ b/src/components/structures/RoomSearchView.tsx @@ -111,11 +111,11 @@ export const RoomSearchView = forwardRef( ); if (!bundledRelationship || event.getThread()) continue; const room = client.getRoom(event.getRoomId()); - const thread = room.findThreadForEvent(event); + const thread = room?.findThreadForEvent(event); if (thread) { event.setThread(thread); } else { - room.createThread(event.getId(), event, [], true); + room?.createThread(event.getId()!, event, [], true); } } } @@ -214,7 +214,7 @@ export const RoomSearchView = forwardRef( scrollPanel?.checkScroll(); }; - let lastRoomId: string; + let lastRoomId: string | undefined; let mergedTimeline: MatrixEvent[] = []; let ourEventsIndexes: number[] = []; diff --git a/src/components/structures/RoomStatusBar.tsx b/src/components/structures/RoomStatusBar.tsx index f370091a8a..ff9d4472d4 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; @@ -212,7 +213,7 @@ export default class RoomStatusBar extends React.PureComponent { {}, { consentLink: (sub) => ( - + {sub} ), @@ -271,7 +272,7 @@ export default class RoomStatusBar extends React.PureComponent { ); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.shouldShowConnectionError()) { return (
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 8d85b54df7..d71ae223b1 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"; @@ -223,6 +224,7 @@ export interface IRoomState { narrow: boolean; // List of undecryptable events currently visible on-screen visibleDecryptionFailures?: MatrixEvent[]; + msc3946ProcessDynamicPredecessor: boolean; } interface LocalRoomViewProps { @@ -416,6 +418,7 @@ export class RoomView extends React.Component { liveTimeline: undefined, narrow: false, visibleDecryptionFailures: [], + msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"), }; this.dispatcherRef = dis.register(this.onAction); @@ -467,6 +470,9 @@ export class RoomView extends React.Component { ), SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange), SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange), + SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) => + this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }), + ), ]; } @@ -851,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; @@ -953,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") { @@ -961,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); @@ -1125,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 @@ -1145,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. @@ -1697,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(); }; @@ -1770,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; @@ -1793,15 +1805,13 @@ 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; }; private getOldRoom(): Room | null { - const createEvent = this.state.room.currentState.getStateEvents(EventType.RoomCreate, ""); - if (!createEvent || !createEvent.getContent()["predecessor"]) return null; - - return this.context.client.getRoom(createEvent.getContent()["predecessor"]["room_id"]); + const { roomId } = this.state.room?.findPredecessor(this.state.msc3946ProcessDynamicPredecessor) || {}; + return this.context.client?.getRoom(roomId) || null; } public getHiddenHighlightCount(): number { @@ -1869,7 +1879,7 @@ export class RoomView extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.room instanceof LocalRoom) { if (this.state.room.state === LocalRoomState.CREATING) { return this.renderLocalRoomCreateLoader(); 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/SearchBox.tsx b/src/components/structures/SearchBox.tsx index a0777f2d52..16c2df3173 100644 --- a/src/components/structures/SearchBox.tsx +++ b/src/components/structures/SearchBox.tsx @@ -24,7 +24,7 @@ import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; interface IProps extends HTMLProps { - onSearch?: (query: string) => void; + onSearch: (query: string) => void; onCleared?: (source?: string) => void; onKeyDown?: (ev: React.KeyboardEvent) => void; onFocus?: (ev: React.FocusEvent) => void; @@ -62,7 +62,7 @@ export default class SearchBox extends React.Component { private onSearch = throttle( (): void => { - this.props.onSearch(this.search.current.value); + this.props.onSearch(this.search.current?.value); }, 200, { trailing: true, leading: true }, @@ -101,7 +101,7 @@ export default class SearchBox extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { onSearch, 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..0abbf996cb 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" @@ -814,7 +814,7 @@ export default class SpaceRoomView extends React.PureComponent { } } - public render(): JSX.Element { + public render(): React.ReactNode { const rightPanel = this.state.showRightPanel && this.state.phase === Phase.Landing ? ( diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 8d553b5549..8eaf3f2be9 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, @@ -343,7 +343,7 @@ export default class ThreadView extends React.Component { ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const highlightedEventId = this.props.isInitialEventHighlighted ? this.props.initialEvent?.getId() : null; const threadRelation = this.threadRelation; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index d0f0f9e876..7e859e15cc 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 @@ -1886,7 +1886,7 @@ class TimelinePanel extends React.Component { this.callEventGroupers = buildLegacyCallEventGroupers(this.callEventGroupers, events); } - public render(): JSX.Element { + public render(): React.ReactNode { // just show a spinner while the timeline loads. // // put it in a div of the right class (mx_RoomView_messagePanel) so @@ -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..c95f5a1099 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(), @@ -50,7 +50,7 @@ export default class ToastContainer extends React.Component<{}, IState> { }); }; - public render(): JSX.Element { + public render(): React.ReactNode { const totalCount = this.state.toasts.length; const isStacked = totalCount > 1; let toast; diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx index 9d256b6aa5..c87ad4754f 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 @@ -103,7 +103,7 @@ export default class UploadBar extends React.PureComponent { ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload!); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (!this.state.currentFile) { return null; } diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 302f01f311..aea40bbe57 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -429,7 +429,7 @@ export default class UserMenu extends React.Component { ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const avatarSize = 32; // should match border-radius of the avatar const userId = MatrixClientPeg.get().getUserId(); diff --git a/src/components/structures/UserView.tsx b/src/components/structures/UserView.tsx index 226de3e233..a5cdb0b584 100644 --- a/src/components/structures/UserView.tsx +++ b/src/components/structures/UserView.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import Modal from "../../Modal"; @@ -31,7 +32,7 @@ import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases import { UserOnboardingPage } from "../views/user-onboarding/UserOnboardingPage"; interface IProps { - userId?: string; + userId: string; resizeNotifier: ResizeNotifier; } @@ -66,7 +67,7 @@ export default class UserView extends React.Component { private async loadProfileInfo(): Promise { const cli = MatrixClientPeg.get(); this.setState({ loading: true }); - let profileInfo; + let profileInfo: Awaited>; try { profileInfo = await cli.getProfileInfo(this.props.userId); } catch (err) { @@ -83,7 +84,7 @@ export default class UserView extends React.Component { this.setState({ member, loading: false }); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.loading) { return ; } else if (this.state.member) { diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index 245c1587c5..faf445cef5 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -142,7 +142,7 @@ export default class ViewSource extends React.Component { return room.currentState.mayClientSendStateEvent(mxEvent.getType(), cli); } - public render(): JSX.Element { + public render(): React.ReactNode { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isEditing = this.state.isEditing; diff --git a/src/components/structures/auth/CompleteSecurity.tsx b/src/components/structures/auth/CompleteSecurity.tsx index 7b898d694c..3171a0ec88 100644 --- a/src/components/structures/auth/CompleteSecurity.tsx +++ b/src/components/structures/auth/CompleteSecurity.tsx @@ -57,7 +57,7 @@ export default class CompleteSecurity extends React.Component { store.stop(); } - public render(): JSX.Element { + public render(): React.ReactNode { const { phase, lostKeys } = this.state; let icon; let title; diff --git a/src/components/structures/auth/E2eSetup.tsx b/src/components/structures/auth/E2eSetup.tsx index 6c112a99aa..80c18401dc 100644 --- a/src/components/structures/auth/E2eSetup.tsx +++ b/src/components/structures/auth/E2eSetup.tsx @@ -27,7 +27,7 @@ interface IProps { } export default class E2eSetup extends React.Component { - public render(): JSX.Element { + public render(): React.ReactNode { return ( diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index 94399bf88c..c3a6201252 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -487,7 +487,7 @@ export default class ForgotPassword extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { let resetPasswordJsx: JSX.Element; switch (this.state.phase) { diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 4cbe0f5bc6..8bd72ad43c 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 ( @@ -555,7 +563,7 @@ export default class LoginComponent extends React.PureComponent ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const loader = this.isBusy() && !this.state.busyLoggingIn ? (
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index aac39334a0..196b390417 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, @@ -571,7 +573,7 @@ export default class Registration extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { let errorText; const err = this.state.errorText; if (err) { diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index 8102cbcfab..c3270c52c2 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); @@ -141,7 +141,7 @@ export default class SetupEncryptionBody extends React.Component this.props.onFinished(); }; - public render(): JSX.Element { + public render(): React.ReactNode { const { phase, lostKeys } = this.state; if (this.state.verificationRequest) { diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index d6ad4bfb16..46e8568697 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(); @@ -326,7 +326,7 @@ export default class SoftLogout extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { return ( @@ -339,7 +339,7 @@ export default class SoftLogout extends React.Component {

{_t("Clear personal data")}

{_t( - "Warning: Your personal data (including encryption keys) is still stored " + + "Warning: your personal data (including encryption keys) is still stored " + "in this session. Clear it if you're finished using this session, or want to sign " + "in to another account.", )} diff --git a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx index 1f63d51bd9..d1de4ba4b3 100644 --- a/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx +++ b/src/components/structures/auth/forgot-password/VerifyEmailModal.tsx @@ -52,8 +52,8 @@ export const VerifyEmailModal: React.FC = ({

{_t("Verify your email to continue")}

{_t( - `We need to know it’s you before resetting your password. - Click the link in the email we just sent to %(email)s`, + "We need to know it’s you before resetting your password. " + + "Click the link in the email we just sent to %(email)s", { email, }, diff --git a/src/components/views/audio_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx index 790de5bb89..ae0239cd42 100644 --- a/src/components/views/audio_messages/Clock.tsx +++ b/src/components/views/audio_messages/Clock.tsx @@ -43,7 +43,7 @@ export default class Clock extends React.Component { return currentFloor !== nextFloor; } - public render(): JSX.Element { + public render(): React.ReactNode { return ( {this.props.formatFn(this.props.seconds)} diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx index 975fb79a36..364fcb6c77 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 = { @@ -48,7 +48,7 @@ export default class DurationClock extends React.PureComponent { this.setState({ durationSeconds: time[1] }); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ; } } diff --git a/src/components/views/audio_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx index 1a3f30b108..0c8c6603d2 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, @@ -59,7 +59,7 @@ export default class LiveRecordingClock extends React.PureComponent; } } diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx index 36ca2b4e13..47a4d5884f 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), @@ -63,7 +63,7 @@ export default class LiveRecordingWaveform extends React.PureComponent; } } 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..5dd7dc14cf 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 = { @@ -65,7 +65,7 @@ export default class PlaybackClock extends React.PureComponent { this.setState({ seconds: time[0], durationSeconds: time[1] }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let seconds = this.state.seconds; if (this.state.playbackPhase === PlaybackState.Stopped) { if (Number.isFinite(this.props.defaultDisplaySeconds)) { diff --git a/src/components/views/audio_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx index fe86ef3326..19fd5de797 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 = { @@ -61,7 +61,7 @@ export default class PlaybackWaveform extends React.PureComponent; } } 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/audio_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx index 6c66eba234..ea3572022a 100644 --- a/src/components/views/audio_messages/Waveform.tsx +++ b/src/components/views/audio_messages/Waveform.tsx @@ -41,7 +41,7 @@ export default class Waveform extends React.PureComponent { progress: 1, }; - public render(): JSX.Element { + public render(): React.ReactNode { return (

{this.props.relHeights.map((h, i) => { diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index 6c16f972d6..9119743378 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -122,7 +122,7 @@ export default class CaptchaForm extends React.Component{this.state.errorText}
; 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/EmailField.tsx b/src/components/views/auth/EmailField.tsx index cb664d8352..2731503e38 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -74,7 +74,7 @@ class EmailField extends PureComponent { return result; }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { public static LOGIN_TYPE = AuthType.Password; - public constructor(props) { + public constructor(props: IAuthEntryProps) { super(props); this.state = { @@ -136,7 +136,7 @@ export class PasswordAuthEntry extends React.Component; } @@ -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; @@ -349,7 +353,7 @@ export class TermsAuthEntry extends React.Component; } @@ -438,7 +442,7 @@ export class EmailIdentityAuthEntry extends React.Component< this.props.onPhaseChange(DEFAULT_PHASE); } - public render(): JSX.Element { + public render(): React.ReactNode { let errorSection; // ignore the error when errcode is M_UNAUTHORIZED as we expect that error until the link is clicked. if (this.props.errorText && this.props.errorCode !== "M_UNAUTHORIZED") { @@ -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; } else { @@ -729,7 +733,7 @@ export class RegistrationTokenAuthEntry 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 @@ -948,7 +952,7 @@ export class FallbackAuthEntry extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { let errorSection; if (this.props.errorText) { errorSection = ( diff --git a/src/components/views/auth/LoginWithQR.tsx b/src/components/views/auth/LoginWithQR.tsx index 74e69c96b7..80a57cab36 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 = { @@ -229,7 +229,7 @@ export default class LoginWithQR extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { - public constructor(props) { + public constructor(props: IProps) { super(props); } @@ -69,7 +69,7 @@ export default class LoginWithQRFlow extends React.Component { ); }; - public render(): JSX.Element { + public render(): React.ReactNode { let title = ""; let titleIcon: JSX.Element | undefined; let main: JSX.Element | undefined; @@ -184,7 +184,11 @@ export default class LoginWithQRFlow extends React.Component {

{_t("Scan the QR code below with your device that's signed out.")}

  1. {_t("Start at the sign in screen")}
  2. -
  3. {_t("Select 'Scan QR code'")}
  4. +
  5. + {_t("Select '%(scanQRCode)s'", { + scanQRCode: _t("Scan QR code"), + })} +
  6. {_t("Review and approve the sign in")}
{code} diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index 36b6bdb7fb..a1fb67c528 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 { @@ -65,7 +65,7 @@ class PassphraseConfirmField extends PureComponent { return result; }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { labelStrongPassword?: string; labelAllowedButUnsafe?: string; - onChange(ev: React.FormEvent); - onValidate?(result: IValidationResult); + onChange(ev: React.FormEvent): void; + onValidate?(result: IValidationResult): void; } class PassphraseField extends PureComponent { @@ -102,7 +102,7 @@ class PassphraseField extends PureComponent { return result; }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( >; 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; @@ -369,7 +367,7 @@ export default class PasswordLogin extends React.PureComponent { } } - public render(): JSX.Element { + public render(): React.ReactNode { let forgotPasswordJsx; if (this.props.onForgotPasswordClick) { diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 3f41cb6127..25a319e817 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; @@ -524,7 +534,7 @@ export default class RegistrationForm extends React.PureComponent ); 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/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 4813871455..6760906450 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -109,7 +109,7 @@ export default function MemberAvatar({ } export class LegacyMemberAvatar extends React.Component { - public render(): JSX.Element { + public render(): React.ReactNode { return {this.props.children}; } } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 50389c7749..3a13dfdac4 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -135,7 +135,7 @@ export default class RoomAvatar extends React.Component { return this.props.room?.roomId || this.props.oobData?.roomId; } - public render(): JSX.Element { + public render(): React.ReactNode { const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; const roomName = room?.name ?? oobData.name; diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index c6b57f39c5..7fbf68b290 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -91,7 +91,7 @@ const BetaCard: React.FC = ({ title: titleOverride, featureId }) => { ); } - let refreshWarning: string; + let refreshWarning: string | undefined; if (requiresRefresh) { const brand = SdkConfig.get().brand; refreshWarning = value diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index b0af3f4cc7..06cb566eb4 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,18 +58,18 @@ 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 }); }; - public render(): JSX.Element { + public render(): React.ReactNode { return (
diff --git a/src/components/views/context_menus/GenericElementContextMenu.tsx b/src/components/views/context_menus/GenericElementContextMenu.tsx index e4b5203acc..5e3d55fbd5 100644 --- a/src/components/views/context_menus/GenericElementContextMenu.tsx +++ b/src/components/views/context_menus/GenericElementContextMenu.tsx @@ -47,7 +47,7 @@ export default class GenericElementContextMenu extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { return
{this.props.element}
; } } diff --git a/src/components/views/context_menus/GenericTextContextMenu.tsx b/src/components/views/context_menus/GenericTextContextMenu.tsx index 3e6b8a1114..c40b69216d 100644 --- a/src/components/views/context_menus/GenericTextContextMenu.tsx +++ b/src/components/views/context_menus/GenericTextContextMenu.tsx @@ -21,7 +21,7 @@ interface IProps { } export default class GenericTextContextMenu extends React.Component { - public render(): JSX.Element { + public render(): React.ReactNode { return (
{this.props.message} diff --git a/src/components/views/context_menus/LegacyCallContextMenu.tsx b/src/components/views/context_menus/LegacyCallContextMenu.tsx index 8e4efa28e4..e0b52a6de1 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); } @@ -46,7 +46,7 @@ export default class LegacyCallContextMenu extends React.Component { this.props.onFinished(); }; - public render(): JSX.Element { + public render(): React.ReactNode { const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold"); const handler = this.props.call.isRemoteOnHold() ? this.onUnholdClick : this.onHoldClick; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 677565f7d8..b2b6c57b38 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -376,7 +376,7 @@ export default class MessageContextMenu extends React.Component this.closeMenu(); }; - public render(): JSX.Element { + public render(): React.ReactNode { const cli = MatrixClientPeg.get(); const me = cli.getUserId(); const { mxEvent, rightClick, link, eventTileOps, reactions, collapseReplyChain, ...other } = this.props; diff --git a/src/components/views/dialogs/AskInviteAnywayDialog.tsx b/src/components/views/dialogs/AskInviteAnywayDialog.tsx index a5638b475d..9b9982618a 100644 --- a/src/components/views/dialogs/AskInviteAnywayDialog.tsx +++ b/src/components/views/dialogs/AskInviteAnywayDialog.tsx @@ -48,7 +48,7 @@ export default class AskInviteAnywayDialog extends React.Component { this.props.onFinished(false); }; - public render(): JSX.Element { + public render(): React.ReactNode { const errorList = this.props.unknownProfileUsers.map((address) => (
  • {address.userId}: {address.errorText} diff --git a/src/components/views/dialogs/BaseDialog.tsx b/src/components/views/dialogs/BaseDialog.tsx index 2eb5234caa..484356ff2b 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(); @@ -115,7 +115,7 @@ export default class BaseDialog extends React.Component { this.props.onFinished(false); }; - public render(): JSX.Element { + public render(): React.ReactNode { let cancelButton; if (this.props.hasCancel) { cancelButton = ( @@ -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..4074db6d3e 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, @@ -180,7 +180,7 @@ export default class BugReportDialog extends React.Component { this.setState({ downloadProgress }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let error = null; if (this.state.err) { error =
    {this.state.err}
    ; 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..85af8203b7 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 (
  • @@ -76,7 +86,7 @@ export default class ChangelogDialog extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { const logs = REPOS.map((repo) => { let content; if (this.state[repo] == null) { @@ -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..60e18eb263 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, @@ -72,7 +72,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return ( { this.props.onFinished(false); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { private nameField = createRef(); private aliasField = createRef(); - public constructor(props) { + public constructor(props: IProps) { super(props); this.supportsRestricted = !!this.props.parentSpace; @@ -216,7 +216,7 @@ export default class CreateRoomDialog extends React.Component { ], }); - public render(): JSX.Element { + public render(): React.ReactNode { const isVideoRoom = this.props.type === RoomType.ElementVideo; let aliasField: JSX.Element; 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..403af3c0d3 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{this.state.errStr}
    ; diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 0df6ce4206..2b74667ba5 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -91,7 +91,7 @@ const DevtoolsDialog: React.FC = ({ 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/EndPollDialog.tsx b/src/components/views/dialogs/EndPollDialog.tsx index 463605553e..1951f881bb 100644 --- a/src/components/views/dialogs/EndPollDialog.tsx +++ b/src/components/views/dialogs/EndPollDialog.tsx @@ -67,7 +67,7 @@ export default class EndPollDialog extends React.Component { this.props.onFinished(endPoll); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { public static defaultProps = { focus: true, - title: null, - description: null, - button: null, }; private onClick = (): void => { this.props.onFinished(true); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( = ({ 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/FeedbackDialog.tsx b/src/components/views/dialogs/FeedbackDialog.tsx index ef8c9c528a..b2f8dcfe00 100644 --- a/src/components/views/dialogs/FeedbackDialog.tsx +++ b/src/components/views/dialogs/FeedbackDialog.tsx @@ -97,7 +97,7 @@ const FeedbackDialog: React.FC = (props: IProps) => { ); } - let bugReports = null; + let bugReports: JSX.Element | null = null; if (rageshakeUrl) { bugReports = (

    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/IncomingSasDialog.tsx b/src/components/views/dialogs/IncomingSasDialog.tsx index 097fbd39a8..143b37ba51 100644 --- a/src/components/views/dialogs/IncomingSasDialog.tsx +++ b/src/components/views/dialogs/IncomingSasDialog.tsx @@ -256,7 +256,7 @@ export default class IncomingSasDialog extends React.Component { return ; } - public render(): JSX.Element { + public render(): React.ReactNode { let body; switch (this.state.phase) { case PHASE_START: diff --git a/src/components/views/dialogs/InfoDialog.tsx b/src/components/views/dialogs/InfoDialog.tsx index 1a64485c61..d4676ac496 100644 --- a/src/components/views/dialogs/InfoDialog.tsx +++ b/src/components/views/dialogs/InfoDialog.tsx @@ -44,7 +44,7 @@ export default class InfoDialog extends React.Component { this.props.onFinished(); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( dis.fire(Action.ViewUserSettings); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( title={_t("Integrations are disabled")} >

    -

    {_t("Enable 'Manage Integrations' in Settings to do this.")}

    +

    + {_t("Enable '%(manageIntegrations)s' in Settings to do this.", { + manageIntegrations: _t("Manage integrations"), + })} +

    ; 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 }); }; @@ -140,7 +140,7 @@ export default class InteractiveAuthDialog extends React.Component our props > defaults. diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index ff28f6636a..fc20150a63 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(); @@ -100,7 +100,7 @@ class DMUserTile extends React.PureComponent { this.props.onRemove(this.props.member); }; - public render(): JSX.Element { + public render(): React.ReactNode { const avatarSize = 20; const avatar = ; @@ -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(); @@ -187,7 +187,7 @@ class DMRoomTile extends React.PureComponent { return result; } - public render(): JSX.Element { + public render(): React.ReactNode { let timestamp = null; if (this.props.lastActiveTs) { const humanTs = humanizeTime(this.props.lastActiveTs); @@ -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 { @@ -1078,7 +1082,7 @@ export default class InviteDialog extends React.PureComponent; diff --git a/src/components/views/dialogs/LogoutDialog.tsx b/src/components/views/dialogs/LogoutDialog.tsx index 6e4fb1b2e9..af3fb65f6f 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(); @@ -127,7 +127,7 @@ export default class LogoutDialog extends React.Component { this.props.onFinished(true); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.shouldLoadBackupStatus) { const description = (
    diff --git a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx index 742919c9ba..0819d72d7e 100644 --- a/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx +++ b/src/components/views/dialogs/ManualDeviceKeyVerificationDialog.tsx @@ -40,7 +40,7 @@ export default class ManualDeviceKeyVerificationDialog extends React.Component b.disabled).map((b) => b.id), }; - public constructor(props) { + public constructor(props: IProps) { super(props); this.widget = new ElementWidget({ @@ -124,7 +124,7 @@ export default class ModalWidgetDialog extends React.PureComponent = ({ 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..ea5c150eb3 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 }); }; @@ -273,7 +273,7 @@ export default class ReportEventDialog extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { let error = null; if (this.state.err) { error =
    {this.state.err}
    ; diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx index 68616cf117..d451d4a658 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) { @@ -180,7 +181,7 @@ export default class RoomSettingsDialog extends React.Component return tabs; } - public render(): JSX.Element { + public render(): React.ReactNode { const roomName = this.state.roomName; return ( { }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let buttons; if (this.state.busy) { buttons = ; diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx index a62c1467dc..65652cec43 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(); Modal.createDialog(BugReportDialog, {}); }; - public render(): JSX.Element { + public render(): React.ReactNode { const brand = SdkConfig.get().brand; let inviteToggle = null; diff --git a/src/components/views/dialogs/ScrollableBaseModal.tsx b/src/components/views/dialogs/ScrollableBaseModal.tsx index 35512f0038..ca3d8591c3 100644 --- a/src/components/views/dialogs/ScrollableBaseModal.tsx +++ b/src/components/views/dialogs/ScrollableBaseModal.tsx @@ -73,7 +73,7 @@ export default abstract class ScrollableBaseModal< protected abstract submit(): void; protected abstract renderContent(): React.ReactNode; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { }); } - public render(): JSX.Element { + public render(): React.ReactNode { let timeline = this.renderTimeline().filter((c) => !!c); // remove nulls for next check if (timeline.length === 0) { timeline = [
    {_t("You're all caught up.")}
    ]; diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 31c7a00659..75436e03a8 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 }); @@ -163,7 +163,7 @@ export default class ServerPickerDialog extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return ( { window.location.reload(); }; - public render(): JSX.Element { + public render(): React.ReactNode { const brand = SdkConfig.get().brand; const clearStorageButton = ( diff --git a/src/components/views/dialogs/SetEmailDialog.tsx b/src/components/views/dialogs/SetEmailDialog.tsx index 400b863428..6455517961 100644 --- a/src/components/views/dialogs/SetEmailDialog.tsx +++ b/src/components/views/dialogs/SetEmailDialog.tsx @@ -138,7 +138,7 @@ export default class SetEmailDialog extends React.Component { ); } - public render(): JSX.Element { + public render(): React.ReactNode { const emailInput = this.state.emailBusy ? ( ) : ( diff --git a/src/components/views/dialogs/ShareDialog.tsx b/src/components/views/dialogs/ShareDialog.tsx index b3761a49b6..353ad347e0 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}`, }, ]; @@ -71,14 +71,14 @@ interface IProps extends IDialogProps { interface IState { linkSpecificEvent: boolean; - permalinkCreator: RoomPermalinkCreator; + permalinkCreator: RoomPermalinkCreator | null; } export default class ShareDialog extends React.PureComponent { - public constructor(props) { + public constructor(props: IProps) { super(props); - let permalinkCreator: RoomPermalinkCreator = null; + let permalinkCreator: RoomPermalinkCreator | null = null; if (props.target instanceof Room) { permalinkCreator = new RoomPermalinkCreator(props.target); permalinkCreator.load(); @@ -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 => { @@ -108,15 +108,15 @@ export default class ShareDialog extends React.PureComponent { if (this.props.target instanceof Room) { if (this.state.linkSpecificEvent) { const events = this.props.target.getLiveTimeline().getEvents(); - matrixToUrl = this.state.permalinkCreator.forEvent(events[events.length - 1].getId()); + matrixToUrl = this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!); } else { - matrixToUrl = this.state.permalinkCreator.forShareableRoom(); + matrixToUrl = this.state.permalinkCreator!.forShareableRoom(); } } else if (this.props.target instanceof User || this.props.target instanceof RoomMember) { matrixToUrl = makeUserPermalink(this.props.target.userId); } else if (this.props.target instanceof MatrixEvent) { if (this.state.linkSpecificEvent) { - matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId()); + matrixToUrl = this.props.permalinkCreator.forEvent(this.props.target.getId()!); } else { matrixToUrl = this.props.permalinkCreator.forShareableRoom(); } @@ -124,7 +124,7 @@ export default class ShareDialog extends React.PureComponent { return matrixToUrl; } - public render(): JSX.Element { + public render(): React.ReactNode { let title; let checkbox; 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/StorageEvictedDialog.tsx b/src/components/views/dialogs/StorageEvictedDialog.tsx index 18a70e032a..74af879731 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.tsx +++ b/src/components/views/dialogs/StorageEvictedDialog.tsx @@ -37,7 +37,7 @@ export default class StorageEvictedDialog extends React.Component { this.props.onFinished(true); }; - public render(): JSX.Element { + public render(): React.ReactNode { let logRequest; if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( diff --git a/src/components/views/dialogs/TermsDialog.tsx b/src/components/views/dialogs/TermsDialog.tsx index 819e14be7c..57efd54b99 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; @@ -33,7 +34,7 @@ class TermsCheckbox extends React.PureComponent { this.props.onChange(this.props.url, ev.currentTarget.checked); }; - public render(): JSX.Element { + public render(): React.ReactNode { return ; } } @@ -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 @@ -125,7 +126,7 @@ export default class TermsDialog extends React.PureComponent { return result; }; - public render(): JSX.Element { + public render(): React.ReactNode { return ( { totalFiles: 1, }; - public constructor(props) { + public constructor(props: IProps) { super(props); // Create a fresh `Blob` for previewing (even though `File` already is @@ -65,7 +65,7 @@ export default class UploadConfirmDialog extends React.Component { this.props.onFinished(true, true); }; - public render(): JSX.Element { + public render(): React.ReactNode { let title: string; if (this.props.totalFiles > 1 && this.props.currentIndex !== undefined) { title = _t("Upload files (%(current)s of %(total)s)", { diff --git a/src/components/views/dialogs/UploadFailureDialog.tsx b/src/components/views/dialogs/UploadFailureDialog.tsx index 03e142da05..2dd99e88a5 100644 --- a/src/components/views/dialogs/UploadFailureDialog.tsx +++ b/src/components/views/dialogs/UploadFailureDialog.tsx @@ -43,7 +43,7 @@ export default class UploadFailureDialog extends React.Component { this.props.onFinished(true); }; - public render(): JSX.Element { + public render(): React.ReactNode { let message; let preview; let buttons; diff --git a/src/components/views/dialogs/UserSettingsDialog.tsx b/src/components/views/dialogs/UserSettingsDialog.tsx index 7b33ca58da..5beae8ccaa 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 = { @@ -210,7 +210,7 @@ export default class UserSettingsDialog extends React.Component return tabs; } - public render(): JSX.Element { + public render(): React.ReactNode { return ( { - public constructor(props) { + public constructor(props: IProps) { super(props); this.state = { verificationRequest: this.props.verificationRequest, @@ -47,7 +47,7 @@ export default class VerificationRequestDialog extends React.Component; 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 }; @@ -87,7 +86,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< this.props.onFinished({ approved, remember: this.state.rememberSelection }); } - public render(): JSX.Element { + public render(): React.ReactNode { // We specifically order the timeline capabilities down to the bottom. The capability text // generation cares strongly about this. const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => { diff --git a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx index f33e392443..f185c5b7e6 100644 --- a/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx +++ b/src/components/views/dialogs/WidgetOpenIDPermissionsDialog.tsx @@ -73,7 +73,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent = ({ 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/polls/PollHistoryDialog.tsx b/src/components/views/dialogs/polls/PollHistoryDialog.tsx index 4671da9246..e5525fbbaf 100644 --- a/src/components/views/dialogs/polls/PollHistoryDialog.tsx +++ b/src/components/views/dialogs/polls/PollHistoryDialog.tsx @@ -14,26 +14,47 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { useEffect, useState } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; +import { MatrixEvent, Poll } from "matrix-js-sdk/src/matrix"; import { _t } from "../../../../languageHandler"; import BaseDialog from "../BaseDialog"; import { IDialogProps } from "../IDialogProps"; import { PollHistoryList } from "./PollHistoryList"; -import { getPolls } from "./usePollHistory"; +import { PollHistoryFilter } from "./types"; +import { usePolls } from "./usePollHistory"; type PollHistoryDialogProps = Pick & { roomId: string; matrixClient: MatrixClient; }; + +const sortEventsByLatest = (left: MatrixEvent, right: MatrixEvent): number => right.getTs() - left.getTs(); +const filterPolls = + (filter: PollHistoryFilter) => + (poll: Poll): boolean => + (filter === "ACTIVE") !== poll.isEnded; +const filterAndSortPolls = (polls: Map, filter: PollHistoryFilter): MatrixEvent[] => { + return [...polls.values()] + .filter(filterPolls(filter)) + .map((poll) => poll.rootEvent) + .sort(sortEventsByLatest); +}; + export const PollHistoryDialog: React.FC = ({ roomId, matrixClient, onFinished }) => { - const pollStartEvents = getPolls(roomId, matrixClient); + const { polls } = usePolls(roomId, matrixClient); + const [filter, setFilter] = useState("ACTIVE"); + const [pollStartEvents, setPollStartEvents] = useState(filterAndSortPolls(polls, filter)); + + useEffect(() => { + setPollStartEvents(filterAndSortPolls(polls, filter)); + }, [filter, polls]); return (
    - +
    ); diff --git a/src/components/views/dialogs/polls/PollHistoryList.tsx b/src/components/views/dialogs/polls/PollHistoryList.tsx index ff0ea3a7cf..7c8714aeac 100644 --- a/src/components/views/dialogs/polls/PollHistoryList.tsx +++ b/src/components/views/dialogs/polls/PollHistoryList.tsx @@ -19,13 +19,26 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import PollListItem from "./PollListItem"; import { _t } from "../../../../languageHandler"; +import { FilterTabGroup } from "../../elements/FilterTabGroup"; +import { PollHistoryFilter } from "./types"; type PollHistoryListProps = { pollStartEvents: MatrixEvent[]; + filter: PollHistoryFilter; + onFilterChange: (filter: PollHistoryFilter) => void; }; -export const PollHistoryList: React.FC = ({ pollStartEvents }) => { +export const PollHistoryList: React.FC = ({ pollStartEvents, filter, onFilterChange }) => { return (
    + + name="PollHistoryDialog_filter" + value={filter} + onFilterChange={onFilterChange} + tabs={[ + { id: "ACTIVE", label: "Active polls" }, + { id: "ENDED", label: "Past polls" }, + ]} + /> {!!pollStartEvents.length ? (
      {pollStartEvents.map((pollStartEvent) => ( @@ -33,7 +46,11 @@ export const PollHistoryList: React.FC = ({ pollStartEvent ))}
    ) : ( - {_t("There are no polls in this room")} + + {filter === "ACTIVE" + ? _t("There are no active polls in this room") + : _t("There are no past polls in this room")} + )}
    ); diff --git a/src/components/views/dialogs/polls/types.ts b/src/components/views/dialogs/polls/types.ts new file mode 100644 index 0000000000..1664203475 --- /dev/null +++ b/src/components/views/dialogs/polls/types.ts @@ -0,0 +1,22 @@ +/* +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. +*/ + +/** + * Possible values for the "filter" setting in the poll history dialog + * + * Ended polls have a valid M_POLL_END event + */ +export type PollHistoryFilter = "ACTIVE" | "ENDED"; diff --git a/src/components/views/dialogs/polls/usePollHistory.ts b/src/components/views/dialogs/polls/usePollHistory.ts index aa730b84ee..1da2b4ee1d 100644 --- a/src/components/views/dialogs/polls/usePollHistory.ts +++ b/src/components/views/dialogs/polls/usePollHistory.ts @@ -14,27 +14,32 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; -import { MatrixEvent } from "matrix-js-sdk/src/matrix"; +import { Poll, PollEvent } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/client"; +import { useEventEmitterState } from "../../../../hooks/useEventEmitter"; + /** - * Get poll start events in a rooms live timeline + * Get poll instances from a room * @param roomId - id of room to retrieve polls for * @param matrixClient - client - * @returns {MatrixEvent[]} - array fo poll start events + * @returns {Map} - Map of Poll instances */ -export const getPolls = (roomId: string, matrixClient: MatrixClient): MatrixEvent[] => { +export const usePolls = ( + roomId: string, + matrixClient: MatrixClient, +): { + polls: Map; +} => { const room = matrixClient.getRoom(roomId); if (!room) { throw new Error("Cannot find room"); } - // @TODO(kerrya) poll history will be actively fetched in PSG-1043 - // for now, just display polls that are in the current timeline - const timelineEvents = room.getLiveTimeline().getEvents(); - const pollStartEvents = timelineEvents.filter((event) => M_POLL_START.matches(event.getType())); + const polls = useEventEmitterState(room, PollEvent.New, () => room.polls); - return pollStartEvents; + // @TODO(kerrya) watch polls for end events, trigger refiltering + + return { polls }; }; diff --git a/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx b/src/components/views/dialogs/security/AccessSecretStorageDialog.tsx index d7154b3aa2..4873173a3e 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 = { @@ -268,7 +268,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent { + const canUploadKeysWithPasswordOnly = error.data.flows.some((f: UIAFlow) => { return f.stages.length === 1 && f.stages[0] === "m.login.password"; }); this.setState({ @@ -167,7 +168,7 @@ export default class CreateCrossSigningDialog 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( @@ -308,7 +308,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent

    {_t( - "Warning: You should only set up key backup " + "from a trusted computer.", + "Warning: you should only set up key backup " + "from a trusted computer.", {}, { b: (sub) => {sub} }, )} diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 042574c75f..a4cf53b73b 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -57,7 +57,7 @@ export default class SetupEncryptionDialog extends React.Component = ({ 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/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index f1dd1a0cbe..26c3825fda 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -73,7 +73,7 @@ export default class AccessibleTooltipButton extends React.PureComponent { } } - public render(): JSX.Element { + public render(): React.ReactNode { const brand = SdkConfig.get().brand; const displayName = this.state.roomMember ? this.state.roomMember.name : this.props.creatorUserId; diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index b75de2ee44..b34415f93e 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 @@ -544,7 +544,7 @@ export default class AppTile extends React.Component { this.setState({ menuDisplayed: false }); }; - public render(): JSX.Element { + public render(): React.ReactNode { let appTileBody; // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin @@ -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/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 9d18fa89a4..ccb64ead30 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -55,7 +55,7 @@ export class ExistingSource extends React.Component { this.props.onSelect(this.props.source); }; - public render(): JSX.Element { + public render(): React.ReactNode { const thumbnailClasses = classNames({ mx_desktopCapturerSourcePicker_source_thumbnail: true, mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected, @@ -149,7 +149,7 @@ export default class DesktopCapturerSourcePicker extends React.Component{sources}); } - public render(): JSX.Element { + public render(): React.ReactNode { const tabs = [ this.getTab("screen", _t("Share entire screen")), this.getTab("window", _t("Application window")), diff --git a/src/components/views/elements/DialPadBackspaceButton.tsx b/src/components/views/elements/DialPadBackspaceButton.tsx index c6f3179b10..a7a80df882 100644 --- a/src/components/views/elements/DialPadBackspaceButton.tsx +++ b/src/components/views/elements/DialPadBackspaceButton.tsx @@ -25,7 +25,7 @@ interface IProps { } export default class DialPadBackspaceButton extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return (

    { this.props.onCancel(event); }; - public render(): JSX.Element { + public render(): React.ReactNode { let primaryButtonClassName = "mx_Dialog_primary"; if (this.props.primaryButtonClass) { primaryButtonClassName += " " + this.props.primaryButtonClass; diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx index 5ff299ef55..769980b59a 100644 --- a/src/components/views/elements/Draggable.tsx +++ b/src/components/views/elements/Draggable.tsx @@ -73,7 +73,7 @@ export default class Draggable extends React.Component { }); } - public render(): JSX.Element { + public render(): React.ReactNode { return
    ; } } diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index c7abf5246c..aecc8e8141 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -49,7 +49,7 @@ class MenuOption extends React.Component { this.props.onClick(this.props.dropdownKey); }; - public render(): JSX.Element { + public render(): React.ReactNode { const optClasses = classnames({ mx_Dropdown_option: true, mx_Dropdown_option_highlight: this.props.highlighted, @@ -327,7 +327,7 @@ export default class Dropdown extends React.Component { return options; } - public render(): JSX.Element { + public render(): React.ReactNode { let currentValue; const menuStyle: CSSProperties = {}; @@ -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..15bda060ed 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(); @@ -57,7 +57,7 @@ export class EditableItem extends React.Component { this.setState({ verifyRemove: false }); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.verifyRemove) { return (
    @@ -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 { @@ -148,7 +148,7 @@ export default class EditableItemList

    extends React.PureComponent { if (!this.props.canRemove) { return

  • {item}
  • ; diff --git a/src/components/views/elements/EditableText.tsx b/src/components/views/elements/EditableText.tsx index 89d4220bca..b4695d552b 100644 --- a/src/components/views/elements/EditableText.tsx +++ b/src/components/views/elements/EditableText.tsx @@ -202,7 +202,7 @@ export default class EditableText extends React.Component { this.showPlaceholder(!this.value); }; - public render(): JSX.Element { + public render(): React.ReactNode { const { className, editable, initialValue, label, labelClassName } = this.props; let editableEl; diff --git a/src/components/views/elements/EditableTextContainer.tsx b/src/components/views/elements/EditableTextContainer.tsx index 1468b24e54..6e3132e226 100644 --- a/src/components/views/elements/EditableTextContainer.tsx +++ b/src/components/views/elements/EditableTextContainer.tsx @@ -133,7 +133,7 @@ export default class EditableTextContainer extends React.Component; } else if (this.state.errorString) { 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..1ba14bb051 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[] { @@ -495,7 +495,7 @@ export default class EventListSummary extends React.Component { }; } - public render(): JSX.Element { + public render(): React.ReactNode { const eventsToRender = this.props.events; // Map user IDs to latest Avatar Member. ES6 Maps are ordered by when the key was created, diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 2f11351a4b..10d7b458cc 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -111,7 +111,7 @@ export default class EventTilePreview extends React.Component { return event; } - public render(): JSX.Element { + public render(): React.ReactNode { const className = classnames(this.props.className, { mx_IRCLayout: this.props.layout == Layout.IRC, mx_EventTilePreview_loader: !this.props.userId, diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index b22c1d2b27..1afe52810b 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 { @@ -238,7 +232,7 @@ export default class Field extends React.PureComponent { return valid; } - public render(): JSX.Element { + public render(): React.ReactNode { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { element, diff --git a/src/components/views/elements/FilterTabGroup.tsx b/src/components/views/elements/FilterTabGroup.tsx new file mode 100644 index 0000000000..91991fbd0e --- /dev/null +++ b/src/components/views/elements/FilterTabGroup.tsx @@ -0,0 +1,57 @@ +/* +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. +*/ + +import React, { FieldsetHTMLAttributes, ReactNode } from "react"; + +export type FilterTab = { + label: string | ReactNode; + id: T; +}; +type FilterTabGroupProps = FieldsetHTMLAttributes & { + // group name used for radio buttons + name: string; + onFilterChange: (id: T) => void; + // active tab's id + value: T; + // tabs to display + tabs: FilterTab[]; +}; + +/** + * React component which styles a set of content filters as tabs + * + * This is used in displays which show a list of content items, and the user can select between one of several + * filters for those items. For example, in the Poll History dialog, the user can select between "Active" and "Ended" + * polls. + * + * Type `T` is used for the `value` attribute for the buttons in the radio group. + */ +export const FilterTabGroup = ({ + name, + value, + tabs, + onFilterChange, + ...rest +}: FilterTabGroupProps): JSX.Element => ( +
    + {tabs.map(({ label, id }) => ( + + ))} +
    +); diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 5d6c244abf..af1585e997 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -91,7 +91,7 @@ export default class IRCTimelineProfileResizer extends React.Component; } } diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index b6dff90b9c..5bc1f0562b 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; @@ -411,7 +411,7 @@ export default class ImageView extends React.Component { return {contextMenu}; } - public render(): JSX.Element { + public render(): React.ReactNode { const showEventMeta = !!this.props.mxEvent; let transitionClassName; diff --git a/src/components/views/elements/InfoTooltip.tsx b/src/components/views/elements/InfoTooltip.tsx index 7475b0f34f..ed68621f7c 100644 --- a/src/components/views/elements/InfoTooltip.tsx +++ b/src/components/views/elements/InfoTooltip.tsx @@ -39,7 +39,7 @@ export default class InfoTooltip extends React.PureComponent { super(props); } - public render(): JSX.Element { + public render(): React.ReactNode { const { tooltip, children, tooltipClassName, className, kind } = this.props; const title = _t("Information"); const iconClassName = diff --git a/src/components/views/elements/InlineSpinner.tsx b/src/components/views/elements/InlineSpinner.tsx index 4d06eda3d5..504231820e 100644 --- a/src/components/views/elements/InlineSpinner.tsx +++ b/src/components/views/elements/InlineSpinner.tsx @@ -30,7 +30,7 @@ export default class InlineSpinner extends React.PureComponent { h: 16, }; - public render(): JSX.Element { + public render(): React.ReactNode { return (
    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..78b332b353 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 @@ -45,7 +45,7 @@ export default class InviteReason extends React.PureComponent { }); }; - public render(): JSX.Element { + public render(): React.ReactNode { const classes = classNames({ mx_InviteReason: true, mx_InviteReason_hidden: this.state.hidden, diff --git a/src/components/views/elements/LabelledToggleSwitch.tsx b/src/components/views/elements/LabelledToggleSwitch.tsx index ce4db91117..4455b16a9f 100644 --- a/src/components/views/elements/LabelledToggleSwitch.tsx +++ b/src/components/views/elements/LabelledToggleSwitch.tsx @@ -22,26 +22,28 @@ 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 { - public render(): JSX.Element { + public render(): React.ReactNode { // This is a minimal version of a SettingsFlag const { label, caption } = this.props; let firstPart = ( diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 758e1a5e5b..ee03774cbc 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -83,7 +83,7 @@ export default class LanguageDropdown extends React.Component { }); }; - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.langs === null) { return ; } diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx index 492cff95c9..e14941bd73 100644 --- a/src/components/views/elements/LazyRenderList.tsx +++ b/src/components/views/elements/LazyRenderList.tsx @@ -117,7 +117,7 @@ export default class LazyRenderList extends React.Component, return new ItemRange(topCount, renderCount, bottomCount); } - public render(): JSX.Element { + public render(): React.ReactNode { const { itemHeight, items, renderItem } = this.props; const { renderRange } = this.state; const { topCount, renderCount, bottomCount } = renderRange; diff --git a/src/components/views/elements/LinkWithTooltip.tsx b/src/components/views/elements/LinkWithTooltip.tsx index d682f34e46..88d29e4a86 100644 --- a/src/components/views/elements/LinkWithTooltip.tsx +++ b/src/components/views/elements/LinkWithTooltip.tsx @@ -25,7 +25,7 @@ export default class LinkWithTooltip extends React.Component { super(props); } - public render(): JSX.Element { + public render(): React.ReactNode { const { children, tooltip, ...props } = this.props; return ( diff --git a/src/components/views/elements/Measured.tsx b/src/components/views/elements/Measured.tsx index 9445973b1d..355429c454 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++; @@ -64,7 +64,7 @@ export default class Measured extends React.PureComponent { this.props.onMeasurement(entry.contentRect.width <= this.props.breakpoint); }; - public render(): JSX.Element { + public render(): React.ReactNode { return null; } } diff --git a/src/components/views/elements/PersistedElement.tsx b/src/components/views/elements/PersistedElement.tsx index 1bcee67cfd..32d99c857d 100644 --- a/src/components/views/elements/PersistedElement.tsx +++ b/src/components/views/elements/PersistedElement.tsx @@ -191,7 +191,7 @@ export default class PersistedElement extends React.Component { }); } - public render(): JSX.Element { + public render(): React.ReactNode { return
    ; } } 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..5a8de9777d 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, @@ -212,7 +214,7 @@ export default class Pill extends React.Component { }); }; - public render(): JSX.Element { + public render(): React.ReactNode { const resource = this.state.resourceId; let avatar = null; diff --git a/src/components/views/elements/PowerSelector.tsx b/src/components/views/elements/PowerSelector.tsx index d2c0e7584e..f7a4ab4b81 100644 --- a/src/components/views/elements/PowerSelector.tsx +++ b/src/components/views/elements/PowerSelector.tsx @@ -148,7 +148,7 @@ export default class PowerSelector extends React.Component { } }; - public render(): JSX.Element { + public render(): React.ReactNode { let picker; const label = typeof this.props.label === "undefined" ? _t("Power level") : this.props.label; if (this.state.custom) { diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 3c8bb2c1cf..3ca9c7dee7 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -199,7 +199,7 @@ export default class ReplyChain extends React.Component { return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyChain"); } - public render(): JSX.Element { + public render(): React.ReactNode { let header = null; if (this.state.err) { header = ( diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index 7ba6be8588..b2e6965c63 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 = { @@ -77,7 +77,7 @@ export default class RoomAliasField extends React.PureComponent return { prefix, postfix, value, maxlength }; } - public render(): JSX.Element { + public render(): React.ReactNode { const { prefix, postfix, value, maxlength } = this.domainProps; return ( = ({ 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/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 4ae2799a3b..87a6b38df4 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -75,7 +75,7 @@ export default class SettingsFlag extends React.Component { ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); if (!canChange && this.props.hideIfCannotSet) return null; diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 78644dc962..f4fd7fe9dc 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); @@ -91,7 +91,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component< this.setState({ searchQuery }); } - public render(): JSX.Element { + public render(): React.ReactNode { if (this.state.languages === null) { return ; } diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx index 30b75bfc94..cd65b63014 100644 --- a/src/components/views/elements/Spinner.tsx +++ b/src/components/views/elements/Spinner.tsx @@ -30,7 +30,7 @@ export default class Spinner extends React.PureComponent { h: 32, }; - public render(): JSX.Element { + public render(): React.ReactNode { const { w, h, message } = this.props; return (
    diff --git a/src/components/views/elements/Spoiler.tsx b/src/components/views/elements/Spoiler.tsx index 839272451b..5cc28bd7d4 100644 --- a/src/components/views/elements/Spoiler.tsx +++ b/src/components/views/elements/Spoiler.tsx @@ -42,7 +42,7 @@ export default class Spoiler extends React.Component { this.setState({ visible: !this.state.visible }); }; - public render(): JSX.Element { + public render(): React.ReactNode { const reason = this.props.reason ? ( {"(" + this.props.reason + ")"} ) : null; diff --git a/src/components/views/elements/StyledCheckbox.tsx b/src/components/views/elements/StyledCheckbox.tsx index 5f753394f6..d2fb974dbe 100644 --- a/src/components/views/elements/StyledCheckbox.tsx +++ b/src/components/views/elements/StyledCheckbox.tsx @@ -44,7 +44,7 @@ export default class StyledCheckbox extends React.PureComponent this.id = this.props.id || "checkbox_" + randomString(10); } - public render(): JSX.Element { + public render(): React.ReactNode { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { children, className, kind = CheckboxStyle.Solid, inputRef, ...otherProps } = this.props; diff --git a/src/components/views/elements/StyledRadioButton.tsx b/src/components/views/elements/StyledRadioButton.tsx index 0b7ff491ef..6634cea75f 100644 --- a/src/components/views/elements/StyledRadioButton.tsx +++ b/src/components/views/elements/StyledRadioButton.tsx @@ -34,7 +34,7 @@ export default class StyledRadioButton extends React.PureComponent({ 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/SyntaxHighlight.tsx b/src/components/views/elements/SyntaxHighlight.tsx index 3e8828d94e..3262ce5d6c 100644 --- a/src/components/views/elements/SyntaxHighlight.tsx +++ b/src/components/views/elements/SyntaxHighlight.tsx @@ -24,7 +24,7 @@ interface IProps { } export default class SyntaxHighlight extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { const { children: content, language } = this.props; const highlighted = language ? hljs.highlight(content, { language }) : hljs.highlightAuto(content); diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx index 88add4c962..0fdd5e98cd 100644 --- a/src/components/views/elements/TagComposer.tsx +++ b/src/components/views/elements/TagComposer.tsx @@ -65,7 +65,7 @@ export default class TagComposer extends React.PureComponent { this.props.onRemove(tag); } - public render(): JSX.Element { + public render(): React.ReactNode { return (
    diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index f009f8c526..e54ed077c5 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -32,7 +32,7 @@ export default class TextWithTooltip extends React.Component { super(props); } - public render(): JSX.Element { + public render(): React.ReactNode { const { class: className, children, tooltip, tooltipClass, tooltipProps, ...props } = this.props; return ( diff --git a/src/components/views/elements/Tooltip.tsx b/src/components/views/elements/Tooltip.tsx index 9b927d8189..98ee56b159 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(); } @@ -174,7 +174,7 @@ export default class Tooltip extends React.PureComponent { this.setState(style); }; - public render(): JSX.Element { + public render(): React.ReactNode { const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, { mx_Tooltip_visible: this.props.visible, mx_Tooltip_invisible: !this.props.visible, diff --git a/src/components/views/elements/TooltipButton.tsx b/src/components/views/elements/TooltipButton.tsx index 415e25cf9e..f05a83474f 100644 --- a/src/components/views/elements/TooltipButton.tsx +++ b/src/components/views/elements/TooltipButton.tsx @@ -24,11 +24,11 @@ interface IProps { } export default class TooltipButton extends React.Component { - public constructor(props) { + public constructor(props: IProps) { super(props); } - public render(): JSX.Element { + public render(): React.ReactNode { return ( { public static defaultProps = { truncateAt: 2, - createOverflowElement(overflowCount, totalCount) { + createOverflowElement(overflowCount: number, totalCount: number) { return
    {_t("And %(count)s more...", { count: overflowCount })}
    ; }, }; @@ -70,7 +70,7 @@ export default class TruncatedList extends React.Component { } } - public render(): JSX.Element { + public render(): React.ReactNode { let overflowNode = null; const totalChildren = this.getChildCount(); diff --git a/src/components/views/elements/crypto/VerificationQRCode.tsx b/src/components/views/elements/crypto/VerificationQRCode.tsx index 6f8c9c7411..f19b9613a2 100644 --- a/src/components/views/elements/crypto/VerificationQRCode.tsx +++ b/src/components/views/elements/crypto/VerificationQRCode.tsx @@ -24,7 +24,7 @@ interface IProps { } export default class VerificationQRCode extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { return ( { ); }; - public render(): JSX.Element { + public render(): React.ReactNode { const { emojis, name, heightBefore, viewportHeight, scrollTop } = this.props; if (!emojis || emojis.length === 0) { return null; diff --git a/src/components/views/emojipicker/Emoji.tsx b/src/components/views/emojipicker/Emoji.tsx index 4dd6219fa6..022c29a94a 100644 --- a/src/components/views/emojipicker/Emoji.tsx +++ b/src/components/views/emojipicker/Emoji.tsx @@ -30,7 +30,7 @@ interface IProps { } class Emoji extends React.PureComponent { - public render(): JSX.Element { + public render(): React.ReactNode { const { onClick, onMouseEnter, onMouseLeave, emoji, selectedEmojis } = this.props; const isSelected = selectedEmojis && selectedEmojis.has(emoji.unicode); return ( diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 7a99d4fa2c..2e4a6dc0de 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -249,7 +249,7 @@ class EmojiPicker extends React.Component { return CATEGORY_HEADER_HEIGHT + Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT; } - public render(): JSX.Element { + public render(): React.ReactNode { let heightBefore = 0; return (
    diff --git a/src/components/views/emojipicker/Header.tsx b/src/components/views/emojipicker/Header.tsx index 68aa028336..995e2291e8 100644 --- a/src/components/views/emojipicker/Header.tsx +++ b/src/components/views/emojipicker/Header.tsx @@ -82,7 +82,7 @@ class Header extends React.PureComponent { } }; - public render(): JSX.Element { + public render(): React.ReactNode { return (