From a47b3eb0ee85d2b65723c0d536aa97af412da0d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 29 Mar 2023 08:23:54 +0100 Subject: [PATCH] Apply `strictNullChecks` to `src/components/views/elements/*` (#10462 * Apply `strictNullChecks` to `src/components/views/elements/*` * Iterate * Iterate * Iterate * Apply `strictNullChecks` to `src/components/views/elements/*` * Iterate * Iterate * Iterate * Update snapshot --- src/HtmlUtils.tsx | 12 +++--- src/components/views/auth/CountryDropdown.tsx | 5 ++- .../dialogs/AddExistingToSpaceDialog.tsx | 27 ++++++++------ .../views/elements/AppPermission.tsx | 10 ++--- src/components/views/elements/AppTile.tsx | 10 ++--- .../elements/DesktopCapturerSourcePicker.tsx | 2 +- src/components/views/elements/Dropdown.tsx | 37 ++++++++++--------- .../views/elements/EditableText.tsx | 7 ++-- .../views/elements/EffectsOverlay.tsx | 6 +-- .../views/elements/EventListSummary.tsx | 14 ++++--- src/components/views/elements/Field.tsx | 13 ++++--- .../views/elements/FilterDropdown.tsx | 19 ++++++---- src/components/views/elements/ImageView.tsx | 37 ++++++++++++------- .../views/elements/InteractiveTooltip.tsx | 1 + .../views/elements/JoinRuleDropdown.tsx | 13 ++++--- .../views/elements/LanguageDropdown.tsx | 5 ++- .../views/elements/MiniAvatarUploader.tsx | 6 +-- src/components/views/elements/ReplyChain.tsx | 11 +++--- src/components/views/elements/RoomTopic.tsx | 4 +- .../elements/SpellCheckLanguagesDropdown.tsx | 5 ++- .../views/location/LiveDurationDropdown.tsx | 15 +++++--- .../views/spaces/QuickThemeSwitcher.tsx | 11 ++++-- .../views/elements/EventListSummary-test.tsx | 5 ++- .../FilterDropdown-test.tsx.snap | 4 +- 24 files changed, 158 insertions(+), 121 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index e8b38d50c5..f925facaf0 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactElement, ReactNode } from "react"; +import React, { LegacyRef, ReactElement, ReactNode } from "react"; import sanitizeHtml from "sanitize-html"; import cheerio from "cheerio"; import classNames from "classnames"; @@ -93,8 +93,8 @@ const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?) * positives, but useful for fast-path testing strings to see if they * need emojification. */ -function mightContainEmoji(str: string): boolean { - return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); +function mightContainEmoji(str?: string): boolean { + return !!str && (SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str)); } /** @@ -463,7 +463,7 @@ const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => ( * @returns if isHtmlMessage is true, returns an array of strings, otherwise return an array of React Elements for emojis * and plain text for everything else */ -function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | string)[] { +function formatEmojis(message: string | undefined, isHtmlMessage: boolean): (JSX.Element | string)[] { const emojiToSpan = isHtmlMessage ? emojiToHtmlSpan : emojiToJsxSpan; const result: (JSX.Element | string)[] = []; let text = ""; @@ -641,9 +641,9 @@ export function bodyToHtml(content: IContent, highlights: Optional, op * @return The HTML-ified node. */ export function topicToHtml( - topic: string, + topic?: string, htmlTopic?: string, - ref?: React.Ref, + ref?: LegacyRef, allowExtendedHtml = false, ): ReactNode { if (!SettingsStore.getValue("feature_html_topic")) { diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index 4b4396ebb5..fe20730a08 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from "../../../phonenumber"; import SdkConfig from "../../../SdkConfig"; import { _t } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; +import { NonEmptyArray } from "../../../@types/common"; const COUNTRIES_BY_ISO2: Record = {}; for (const c of COUNTRIES) { @@ -131,7 +132,7 @@ export default class CountryDropdown extends React.Component { {_t(country.name)} (+{country.prefix}) ); - }); + }) as NonEmptyArray; // default value here too, otherwise we need to handle null / undefined // values between mounting and the initial value propagating diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 9fe0ca865b..9156e1b2e0 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ReactNode, useContext, useMemo, useRef, useState } from "react"; +import React, { ReactElement, ReactNode, useContext, useMemo, useRef, useState } from "react"; import classNames from "classnames"; import { Room } from "matrix-js-sdk/src/models/room"; import { sleep } from "matrix-js-sdk/src/utils"; @@ -41,6 +41,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher"; import LazyRenderList from "../elements/LazyRenderList"; import { useSettingValue } from "../../../hooks/useSettings"; import { filterBoolean } from "../../../utils/arrays"; +import { NonEmptyArray } from "../../../@types/common"; // These values match CSS const ROW_HEIGHT = 32 + 12; @@ -415,17 +416,19 @@ export const SubspaceSelector: React.FC = ({ title, spac value={value.roomId} label={_t("Space selection")} > - {options.map((space) => { - const classes = classNames({ - mx_SubspaceSelector_dropdownOptionActive: space === value, - }); - return ( -
- - {space.name || getDisplayAliasForRoom(space) || space.roomId} -
- ); - })} + { + options.map((space) => { + const classes = classNames({ + mx_SubspaceSelector_dropdownOptionActive: space === value, + }); + return ( +
+ + {space.name || getDisplayAliasForRoom(space) || space.roomId} +
+ ); + }) as NonEmptyArray + } ); } else { diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx index 7adae9113b..23187850d4 100644 --- a/src/components/views/elements/AppPermission.tsx +++ b/src/components/views/elements/AppPermission.tsx @@ -40,7 +40,7 @@ interface IProps { interface IState { roomMember: RoomMember; isWrapped: boolean; - widgetDomain: string; + widgetDomain: string | null; } export default class AppPermission extends React.Component { @@ -66,14 +66,14 @@ export default class AppPermission extends React.Component { }; } - private parseWidgetUrl(): { isWrapped: boolean; widgetDomain: string } { + private parseWidgetUrl(): { isWrapped: boolean; widgetDomain: string | null } { const widgetUrl = url.parse(this.props.url); - const params = new URLSearchParams(widgetUrl.search); + const params = new URLSearchParams(widgetUrl.search ?? undefined); // HACK: We're relying on the query params when we should be relying on the widget's `data`. // This is a workaround for Scalar. - if (WidgetUtils.isScalarUrl(this.props.url) && params && params.get("url")) { - const unwrappedUrl = url.parse(params.get("url")); + if (WidgetUtils.isScalarUrl(this.props.url) && params?.get("url")) { + const unwrappedUrl = url.parse(params.get("url")!); return { widgetDomain: unwrappedUrl.host || unwrappedUrl.hostname, isWrapped: true, diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index 01629c5d5c..bf1ccb35d1 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -586,7 +586,7 @@ export default class AppTile extends React.Component { ); - } else if (!this.state.hasPermissionToLoad) { + } else if (!this.state.hasPermissionToLoad && this.props.room) { // only possible for room widgets, can assert this.props.room here const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId); appTileBody = ( @@ -689,11 +689,9 @@ export default class AppTile extends React.Component { const layoutButtons: ReactNode[] = []; if (this.props.showLayoutButtons) { - const isMaximised = WidgetLayoutStore.instance.isInContainer( - this.props.room, - this.props.app, - Container.Center, - ); + const isMaximised = + this.props.room && + WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, Container.Center); const maximisedClasses = classNames({ mx_AppTileMenuBar_iconButton: true, mx_AppTileMenuBar_iconButton_collapse: isMaximised, diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index cc1b7d1ffd..b34f951af3 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -123,7 +123,7 @@ export default class DesktopCapturerSourcePicker extends React.Component { - this.props.onFinished(this.state.selectedSource.id); + this.props.onFinished(this.state.selectedSource?.id); }; private onTabChange = (): void => { diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index e16eaf978e..f603a4b957 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -23,6 +23,7 @@ import { _t } from "../../../languageHandler"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { objectHasDiff } from "../../../utils/objects"; +import { NonEmptyArray } from "../../../@types/common"; interface IMenuOptionProps { children: ReactElement; @@ -77,7 +78,7 @@ export interface DropdownProps { label: string; value?: string; className?: string; - children: ReactElement[]; + children: NonEmptyArray; // negative for consistency with HTML disabled?: boolean; // The width that the dropdown should be. If specified, @@ -102,7 +103,7 @@ export interface DropdownProps { interface IState { expanded: boolean; - highlightedOption: string | null; + highlightedOption: string; searchQuery: string; } @@ -122,14 +123,14 @@ export default class Dropdown extends React.Component { this.reindexChildren(this.props.children); - const firstChild = React.Children.toArray(props.children)[0] as ReactElement; + const firstChild = props.children[0]; this.state = { // True if the menu is dropped-down expanded: false, // The key of the highlighted option // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? (firstChild.key as string) : null, + highlightedOption: firstChild.key, // the current search query searchQuery: "", }; @@ -144,7 +145,7 @@ export default class Dropdown extends React.Component { this.reindexChildren(this.props.children); const firstChild = this.props.children[0]; this.setState({ - highlightedOption: String(firstChild?.key) ?? null, + highlightedOption: firstChild.key, }); } } @@ -156,7 +157,7 @@ export default class Dropdown extends React.Component { private reindexChildren(children: ReactElement[]): void { this.childrenByKey = {}; React.Children.forEach(children, (child) => { - this.childrenByKey[child.key] = child; + this.childrenByKey[(child as DropdownProps["children"][number]).key] = child; }); } @@ -291,13 +292,11 @@ export default class Dropdown extends React.Component { return keys[index <= 0 ? keys.length - 1 : (index - 1) % keys.length]; } - private scrollIntoView(node: Element): void { - if (node) { - node.scrollIntoView({ - block: "nearest", - behavior: "auto", - }); - } + private scrollIntoView(node: Element | null): void { + node?.scrollIntoView({ + block: "nearest", + behavior: "auto", + }); } private getMenuOptions(): JSX.Element[] { @@ -317,7 +316,7 @@ export default class Dropdown extends React.Component { ); }); - if (options.length === 0) { + if (!options?.length) { return [
{_t("No results")} @@ -363,9 +362,13 @@ export default class Dropdown extends React.Component { } if (!currentValue) { - const selectedChild = this.props.getShortOption - ? this.props.getShortOption(this.props.value) - : this.childrenByKey[this.props.value]; + let selectedChild: ReactNode | undefined; + if (this.props.value) { + selectedChild = this.props.getShortOption + ? this.props.getShortOption(this.props.value) + : this.childrenByKey[this.props.value]; + } + currentValue = (
{selectedChild || this.props.placeholder} diff --git a/src/components/views/elements/EditableText.tsx b/src/components/views/elements/EditableText.tsx index 5c11e52ecc..4e2f228a84 100644 --- a/src/components/views/elements/EditableText.tsx +++ b/src/components/views/elements/EditableText.tsx @@ -87,6 +87,7 @@ export default class EditableText extends React.Component { } private showPlaceholder = (show: boolean): void => { + if (!this.editableDiv.current) return; if (show) { this.editableDiv.current.textContent = this.props.placeholder; this.editableDiv.current.setAttribute( @@ -134,7 +135,7 @@ export default class EditableText extends React.Component { if (!(ev.target as HTMLDivElement).textContent) { this.showPlaceholder(true); } else if (!this.placeholder) { - this.value = (ev.target as HTMLDivElement).textContent; + this.value = (ev.target as HTMLDivElement).textContent ?? ""; } const action = getKeyBindingsManager().getAccessibilityAction(ev); @@ -163,7 +164,7 @@ export default class EditableText extends React.Component { range.setStart(node, 0); range.setEnd(node, ev.target.childNodes.length); - const sel = window.getSelection(); + const sel = window.getSelection()!; sel.removeAllRanges(); sel.addRange(range); } @@ -190,7 +191,7 @@ export default class EditableText extends React.Component { }; private onBlur = (ev: React.FocusEvent): void => { - const sel = window.getSelection(); + const sel = window.getSelection()!; sel.removeAllRanges(); if (this.props.blurToCancel) { diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index 604b513d30..11e11211b2 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -54,14 +54,14 @@ const EffectsOverlay: FunctionComponent = ({ roomWidth }) => { }; const onAction = (payload: { action: string }): void => { const actionPrefix = "effects."; - if (payload.action.indexOf(actionPrefix) === 0) { + if (canvasRef.current && payload.action.startsWith(actionPrefix)) { const effect = payload.action.slice(actionPrefix.length); - lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current)); + lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current!)); } }; const dispatcherRef = dis.register(onAction); const canvas = canvasRef.current; - canvas.height = UIStore.instance.windowHeight; + if (canvas) canvas.height = UIStore.instance.windowHeight; UIStore.instance.on(UI_EVENTS.Resize, resize); return () => { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 882e68c9f8..1fc44e5f90 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -78,7 +78,9 @@ enum TransitionType { const SEP = ","; -export default class EventListSummary extends React.Component { +export default class EventListSummary extends React.Component< + IProps & Required> +> { public static contextType = RoomContext; public context!: React.ContextType; @@ -508,12 +510,12 @@ export default class EventListSummary extends React.Component { const type = e.getType(); let userKey = e.getSender()!; - if (type === EventType.RoomThirdPartyInvite) { + if (e.isState() && type === EventType.RoomThirdPartyInvite) { userKey = e.getContent().display_name; - } else if (type === EventType.RoomMember) { - userKey = e.getStateKey(); - } else if (e.isRedacted()) { - userKey = e.getUnsigned()?.redacted_because?.sender; + } else if (e.isState() && type === EventType.RoomMember) { + userKey = e.getStateKey()!; + } else if (e.isRedacted() && e.getUnsigned()?.redacted_because) { + userKey = e.getUnsigned().redacted_because!.sender; } // Initialise a user's events diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index c5e531360f..96151a2e96 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -38,8 +38,6 @@ export interface IValidateOpts { interface IProps { // The field's ID, which binds the input and label together. Immutable. id?: string; - // id of a element for suggestions - list?: string; // The field's label string. label?: string; // The field's placeholder string. Defaults to the label. @@ -119,7 +117,7 @@ interface IState { } export default class Field extends React.PureComponent { - private id: string; + private readonly id: string; private inputRef: RefObject; public static readonly defaultProps = { @@ -243,7 +241,6 @@ export default class Field extends React.PureComponent { tooltipContent, forceValidity, tooltipClassName, - list, validateOnBlur, validateOnChange, validateOnFocus, @@ -262,7 +259,11 @@ export default class Field extends React.PureComponent { inputProps.onBlur = this.onBlur; // Appease typescript's inference - const inputProps_ = { ...inputProps, ref: this.inputRef, list }; + const inputProps_: React.HTMLAttributes & + React.ClassAttributes = { + ...inputProps, + ref: this.inputRef, + }; const fieldInput = React.createElement(this.props.element, inputProps_, children); @@ -287,7 +288,7 @@ export default class Field extends React.PureComponent { }); // Handle displaying feedback on validity - let fieldTooltip; + let fieldTooltip: JSX.Element | undefined; if (tooltipContent || this.state.feedback) { let role: React.AriaRole; if (tooltipContent) { diff --git a/src/components/views/elements/FilterDropdown.tsx b/src/components/views/elements/FilterDropdown.tsx index 80ca10ebdc..8dd5b0173d 100644 --- a/src/components/views/elements/FilterDropdown.tsx +++ b/src/components/views/elements/FilterDropdown.tsx @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import classNames from "classnames"; import { Icon as CheckmarkIcon } from "../../../../res/img/element-icons/roomlist/checkmark.svg"; import Dropdown, { DropdownProps } from "./Dropdown"; +import { NonEmptyArray } from "../../../@types/common"; export type FilterDropdownOption = { id: FilterKeysType; @@ -63,13 +64,15 @@ export const FilterDropdown = ({ className={classNames("mx_FilterDropdown", className)} getShortOption={getSelectedFilterOptionComponent(options, selectedLabel)} > - {options.map(({ id, label, description }) => ( -
- {id === value && } - {label} - {!!description && {description}} -
- ))} + { + options.map(({ id, label, description }) => ( +
+ {id === value && } + {label} + {!!description && {description}} +
+ )) as NonEmptyArray + } ); }; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 6118e02317..aa8f82bb03 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -96,17 +96,24 @@ export default class ImageView extends React.Component { const { thumbnailInfo } = this.props; + let translationX = 0; + let translationY = 0; + if (thumbnailInfo) { + translationX = thumbnailInfo.positionX + thumbnailInfo.width / 2 - UIStore.instance.windowWidth / 2; + translationY = + thumbnailInfo.positionY + + thumbnailInfo.height / 2 - + UIStore.instance.windowHeight / 2 - + getPanelHeight() / 2; + } + this.state = { zoom: 0, // We default to 0 and override this in imageLoaded once we have naturalSize minZoom: MAX_SCALE, maxZoom: MAX_SCALE, rotation: 0, - translationX: thumbnailInfo?.positionX + thumbnailInfo?.width / 2 - UIStore.instance.windowWidth / 2 ?? 0, - translationY: - thumbnailInfo?.positionY + - thumbnailInfo?.height / 2 - - UIStore.instance.windowHeight / 2 - - getPanelHeight() / 2 ?? 0, + translationX, + translationY, moving: false, contextMenuDisplayed: false, }; @@ -143,6 +150,7 @@ export default class ImageView extends React.Component { } private imageLoaded = (): void => { + if (!this.image.current) return; // First, we calculate the zoom, so that the image has the same size as // the thumbnail const { thumbnailInfo } = this.props; @@ -226,22 +234,23 @@ export default class ImageView extends React.Component { translationX: 0, translationY: 0, }); - } else if (typeof anchorX !== "number" && typeof anchorY !== "number") { + } else if (typeof anchorX !== "number" || typeof anchorY !== "number") { // Zoom relative to the center of the view this.setState({ zoom: newZoom, translationX: (this.state.translationX * newZoom) / oldZoom, translationY: (this.state.translationY * newZoom) / oldZoom, }); - } else { + } else if (this.image.current) { // Zoom relative to the given point on the image. // First we need to figure out the offset of the anchor point // relative to the center of the image, accounting for rotation. - let offsetX: number | undefined; - let offsetY: number | undefined; + let offsetX: number; + let offsetY: number; // The modulo operator can return negative values for some // rotations, so we have to do some extra work to normalize it - switch (((this.state.rotation % 360) + 360) % 360) { + const rotation = (((this.state.rotation % 360) + 360) % 360) as 0 | 90 | 180 | 270; + switch (rotation) { case 0: offsetX = this.image.current.clientWidth / 2 - anchorX; offsetY = this.image.current.clientHeight / 2 - anchorY; @@ -384,7 +393,7 @@ export default class ImageView extends React.Component { private onEndMoving = (): void => { // Zoom out if we haven't moved much if ( - this.state.moving === true && + this.state.moving && Math.abs(this.state.translationX - this.previousX) < ZOOM_DISTANCE && Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { @@ -397,7 +406,7 @@ export default class ImageView extends React.Component { private renderContextMenu(): JSX.Element { let contextMenu: JSX.Element | undefined; - if (this.state.contextMenuDisplayed) { + if (this.state.contextMenuDisplayed && this.props.mxEvent) { contextMenu = ( { const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); let permalink = "#"; if (this.props.permalinkCreator) { - permalink = this.props.permalinkCreator.forEvent(mxEvent.getId()); + permalink = this.props.permalinkCreator.forEvent(mxEvent.getId()!); } const senderName = mxEvent.sender?.name ?? mxEvent.getSender(); diff --git a/src/components/views/elements/InteractiveTooltip.tsx b/src/components/views/elements/InteractiveTooltip.tsx index 3aecd8a743..c862f06d95 100644 --- a/src/components/views/elements/InteractiveTooltip.tsx +++ b/src/components/views/elements/InteractiveTooltip.tsx @@ -378,6 +378,7 @@ export default class InteractiveTooltip extends React.Component private onMouseMove = (ev: MouseEvent): void => { const { clientX: x, clientY: y } = ev; const { contentRect } = this.state; + if (!contentRect) return; const targetRect = this.target.getBoundingClientRect(); let direction: Direction; diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx index 99d2780a49..36e68bf459 100644 --- a/src/components/views/elements/JoinRuleDropdown.tsx +++ b/src/components/views/elements/JoinRuleDropdown.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, { ReactElement } from "react"; import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import Dropdown from "./Dropdown"; +import { NonEmptyArray } from "../../../@types/common"; interface IProps { value: JoinRule; @@ -45,13 +46,15 @@ const JoinRuleDropdown: React.FC = ({
{labelPublic}
, - ]; + ] as NonEmptyArray; if (labelRestricted) { options.unshift( -
- {labelRestricted} -
, + ( +
+ {labelRestricted} +
+ ) as ReactElement & { key: string }, ); } diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index 178d85c4eb..5de1ffe785 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -15,13 +15,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 * as languageHandler from "../../../languageHandler"; import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; import Spinner from "./Spinner"; import Dropdown from "./Dropdown"; +import { NonEmptyArray } from "../../../@types/common"; type Languages = Awaited>; @@ -99,7 +100,7 @@ export default class LanguageDropdown extends React.Component { const options = displayedLanguages.map((language) => { return
{language.label}
; - }); + }) as NonEmptyArray; // default value here too, otherwise we need to handle null / undefined // values between mounting and the initial value propagating diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 18652a4c62..464c89ba23 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -16,7 +16,7 @@ limitations under the License. import classNames from "classnames"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "react"; +import React, { useContext, useRef, useState, MouseEvent, ReactNode, RefObject } from "react"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RoomContext from "../../../contexts/RoomContext"; @@ -59,7 +59,7 @@ const MiniAvatarUploader: React.FC = ({ setShow(false); }, 13000); // hide after being shown for 10 seconds - const uploadRef = useRef(); + const uploadRef = useRef() as RefObject; const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; @@ -97,7 +97,7 @@ const MiniAvatarUploader: React.FC = ({ })} disabled={busy} onClick={() => { - uploadRef.current.click(); + uploadRef.current?.click(); }} onMouseOver={() => setHover(true)} onMouseLeave={() => setHover(false)} diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index e4a720ce4c..6409464701 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -31,7 +31,7 @@ import { Action } from "../../../dispatcher/actions"; import Spinner from "./Spinner"; import ReplyTile from "../rooms/ReplyTile"; import { Pill, PillType } from "./Pill"; -import AccessibleButton, { ButtonEvent } from "./AccessibleButton"; +import AccessibleButton from "./AccessibleButton"; import { getParentEventId, shouldDisplayReply } from "../../../utils/Reply"; import RoomContext from "../../../contexts/RoomContext"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; @@ -45,7 +45,7 @@ const SHOW_EXPAND_QUOTE_PIXELS = 60; interface IProps { // the latest event in this chain of replies - parentEv?: MatrixEvent; + parentEv: MatrixEvent; // called when the ReplyChain contents has changed, including EventTiles thereof onHeightChanged: () => void; permalinkCreator?: RoomPermalinkCreator; @@ -91,7 +91,7 @@ export default class ReplyChain extends React.Component { err: false, }; - this.room = this.matrixClient.getRoom(this.props.parentEv.getRoomId()); + this.room = this.matrixClient.getRoom(this.props.parentEv.getRoomId())!; } private get matrixClient(): MatrixClient { @@ -155,7 +155,7 @@ export default class ReplyChain extends React.Component { } } - private async getEvent(eventId: string): Promise { + private async getEvent(eventId?: string): Promise { if (!eventId) return null; const event = this.room.findEventById(eventId); if (event) return event; @@ -180,7 +180,8 @@ export default class ReplyChain extends React.Component { this.initialize(); }; - private onQuoteClick = async (event: ButtonEvent): Promise => { + private onQuoteClick = async (): Promise => { + if (!this.state.loadedEv) return; const events = [this.state.loadedEv, ...this.state.events]; let loadedEv: MatrixEvent | null = null; diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx index 7aa6bce949..ad59012c78 100644 --- a/src/components/views/elements/RoomTopic.tsx +++ b/src/components/views/elements/RoomTopic.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useCallback, useContext, useRef } from "react"; +import React, { RefObject, useCallback, useContext, useRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { EventType } from "matrix-js-sdk/src/@types/event"; @@ -38,7 +38,7 @@ interface IProps extends React.HTMLProps { export default function RoomTopic({ room, ...props }: IProps): JSX.Element { const client = useContext(MatrixClientContext); - const ref = useRef(); + const ref = useRef() as RefObject; const topic = useTopic(room); const body = topicToHtml(topic?.text, topic?.html, ref); diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index 7a725cf2d4..3689e8b2ec 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.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, { ReactElement } from "react"; import Dropdown from "../../views/elements/Dropdown"; import PlatformPeg from "../../../PlatformPeg"; @@ -22,6 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { _t } from "../../../languageHandler"; import Spinner from "./Spinner"; import * as languageHandler from "../../../languageHandler"; +import { NonEmptyArray } from "../../../@types/common"; type Languages = Awaited>; function languageMatchesSearchQuery(query: string, language: Languages[0]): boolean { @@ -106,7 +107,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component< const options = displayedLanguages.map((language) => { return
{language.label}
; - }); + }) as NonEmptyArray; // default value here too, otherwise we need to handle null / undefined; // values between mounting and the initial value propagating diff --git a/src/components/views/location/LiveDurationDropdown.tsx b/src/components/views/location/LiveDurationDropdown.tsx index 6c7a7a14ec..fbb88ca6bc 100644 --- a/src/components/views/location/LiveDurationDropdown.tsx +++ b/src/components/views/location/LiveDurationDropdown.tsx @@ -14,11 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { formatDuration } from "../../../DateUtils"; import { _t } from "../../../languageHandler"; import Dropdown from "../elements/Dropdown"; +import { NonEmptyArray } from "../../../@types/common"; const DURATION_MS = { fifteenMins: 900000, @@ -68,11 +69,13 @@ const LiveDurationDropdown: React.FC = ({ timeout, onChange }) => { onOptionChange={onOptionChange} className="mx_LiveDurationDropdown" > - {options.map(({ key, label }) => ( -
- {label} -
- ))} + { + options.map(({ key, label }) => ( +
+ {label} +
+ )) as NonEmptyArray + } ); }; diff --git a/src/components/views/spaces/QuickThemeSwitcher.tsx b/src/components/views/spaces/QuickThemeSwitcher.tsx index b68783a795..500588e9ed 100644 --- a/src/components/views/spaces/QuickThemeSwitcher.tsx +++ b/src/components/views/spaces/QuickThemeSwitcher.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useMemo } from "react"; +import React, { ReactElement, useMemo } from "react"; import { _t } from "../../../languageHandler"; import { Action } from "../../../dispatcher/actions"; @@ -26,6 +26,7 @@ import { SettingLevel } from "../../../settings/SettingLevel"; import dis from "../../../dispatcher/dispatcher"; import { RecheckThemePayload } from "../../../dispatcher/payloads/RecheckThemePayload"; import PosthogTrackers from "../../../PosthogTrackers"; +import { NonEmptyArray } from "../../../@types/common"; type Props = { requestClose: () => void; @@ -86,9 +87,11 @@ const QuickThemeSwitcher: React.FC = ({ requestClose }) => { value={selectedTheme} label={_t("Space selection")} > - {themeOptions.map((theme) => ( -
{theme.name}
- ))} + { + themeOptions.map((theme) =>
{theme.name}
) as NonEmptyArray< + ReactElement & { key: string } + > + }
); diff --git a/test/components/views/elements/EventListSummary-test.tsx b/test/components/views/elements/EventListSummary-test.tsx index 3fcec7d6d8..03c01c453e 100644 --- a/test/components/views/elements/EventListSummary-test.tsx +++ b/test/components/views/elements/EventListSummary-test.tsx @@ -118,7 +118,10 @@ describe("EventListSummary", function () { ...mockClientMethodsUser(), }); - const defaultProps: ComponentProps = { + const defaultProps: Omit< + ComponentProps, + "summaryLength" | "threshold" | "avatarsMaxLength" + > = { layout: Layout.Bubble, events: [], children: [], diff --git a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap index 627c92f3eb..d82ee64cef 100644 --- a/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap +++ b/test/components/views/elements/__snapshots__/FilterDropdown-test.tsx.snap @@ -7,8 +7,8 @@ exports[` renders dropdown options in menu 1`] = ` role="listbox" >