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
This commit is contained in:
Michael Telatynski 2023-03-29 08:23:54 +01:00 committed by GitHub
parent cefd94859c
commit a47b3eb0ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 158 additions and 121 deletions

View file

@ -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<string[]>, op
* @return The HTML-ified node.
*/
export function topicToHtml(
topic: string,
topic?: string,
htmlTopic?: string,
ref?: React.Ref<HTMLSpanElement>,
ref?: LegacyRef<HTMLSpanElement>,
allowExtendedHtml = false,
): ReactNode {
if (!SettingsStore.getValue("feature_html_topic")) {

View file

@ -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<string, PhoneNumberCountryDefinition> = {};
for (const c of COUNTRIES) {
@ -131,7 +132,7 @@ export default class CountryDropdown extends React.Component<IProps, IState> {
{_t(country.name)} (+{country.prefix})
</div>
);
});
}) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating

View file

@ -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<ISubspaceSelectorProps> = ({ title, spac
value={value.roomId}
label={_t("Space selection")}
>
{options.map((space) => {
const classes = classNames({
mx_SubspaceSelector_dropdownOptionActive: space === value,
});
return (
<div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{space.name || getDisplayAliasForRoom(space) || space.roomId}
</div>
);
})}
{
options.map((space) => {
const classes = classNames({
mx_SubspaceSelector_dropdownOptionActive: space === value,
});
return (
<div key={space.roomId} className={classes}>
<RoomAvatar room={space} width={24} height={24} />
{space.name || getDisplayAliasForRoom(space) || space.roomId}
</div>
);
}) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown>
);
} else {

View file

@ -40,7 +40,7 @@ interface IProps {
interface IState {
roomMember: RoomMember;
isWrapped: boolean;
widgetDomain: string;
widgetDomain: string | null;
}
export default class AppPermission extends React.Component<IProps, IState> {
@ -66,14 +66,14 @@ export default class AppPermission extends React.Component<IProps, IState> {
};
}
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,

View file

@ -586,7 +586,7 @@ export default class AppTile extends React.Component<IProps, IState> {
<AppWarning errorMsg={_t("Error loading Widget")} />
</div>
);
} 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<IProps, IState> {
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,

View file

@ -123,7 +123,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<PickerI
};
private onShare = (): void => {
this.props.onFinished(this.state.selectedSource.id);
this.props.onFinished(this.state.selectedSource?.id);
};
private onTabChange = (): void => {

View file

@ -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<ReactElement & { key: string }>;
// 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<DropdownProps, IState> {
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<DropdownProps, IState> {
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<DropdownProps, IState> {
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<DropdownProps, IState> {
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<DropdownProps, IState> {
</MenuOption>
);
});
if (options.length === 0) {
if (!options?.length) {
return [
<div key="0" className="mx_Dropdown_option" role="option" aria-selected={false}>
{_t("No results")}
@ -363,9 +362,13 @@ export default class Dropdown extends React.Component<DropdownProps, IState> {
}
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 = (
<div className="mx_Dropdown_option" id={`${this.props.id}_value`}>
{selectedChild || this.props.placeholder}

View file

@ -87,6 +87,7 @@ export default class EditableText extends React.Component<IProps, IState> {
}
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
};
private onBlur = (ev: React.FocusEvent<HTMLDivElement>): void => {
const sel = window.getSelection();
const sel = window.getSelection()!;
sel.removeAllRanges();
if (this.props.blurToCancel) {

View file

@ -54,14 +54,14 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ 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 () => {

View file

@ -78,7 +78,9 @@ enum TransitionType {
const SEP = ",";
export default class EventListSummary extends React.Component<IProps> {
export default class EventListSummary extends React.Component<
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
> {
public static contextType = RoomContext;
public context!: React.ContextType<typeof RoomContext>;
@ -508,12 +510,12 @@ export default class EventListSummary extends React.Component<IProps> {
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

View file

@ -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 <datalist> 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<PropShapes, IState> {
private id: string;
private readonly id: string;
private inputRef: RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
public static readonly defaultProps = {
@ -243,7 +241,6 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
tooltipContent,
forceValidity,
tooltipClassName,
list,
validateOnBlur,
validateOnChange,
validateOnFocus,
@ -262,7 +259,11 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
inputProps.onBlur = this.onBlur;
// Appease typescript's inference
const inputProps_ = { ...inputProps, ref: this.inputRef, list };
const inputProps_: React.HTMLAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> &
React.ClassAttributes<HTMLSelectElement | HTMLInputElement | HTMLTextAreaElement> = {
...inputProps,
ref: this.inputRef,
};
const fieldInput = React.createElement(this.props.element, inputProps_, children);
@ -287,7 +288,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
});
// Handle displaying feedback on validity
let fieldTooltip;
let fieldTooltip: JSX.Element | undefined;
if (tooltipContent || this.state.feedback) {
let role: React.AriaRole;
if (tooltipContent) {

View file

@ -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<FilterKeysType extends string> = {
id: FilterKeysType;
@ -63,13 +64,15 @@ export const FilterDropdown = <FilterKeysType extends string = string>({
className={classNames("mx_FilterDropdown", className)}
getShortOption={getSelectedFilterOptionComponent<FilterKeysType>(options, selectedLabel)}
>
{options.map(({ id, label, description }) => (
<div className="mx_FilterDropdown_option" data-testid={`filter-option-${id}`} key={id}>
{id === value && <CheckmarkIcon className="mx_FilterDropdown_optionSelectedIcon" />}
<span className="mx_FilterDropdown_optionLabel">{label}</span>
{!!description && <span className="mx_FilterDropdown_optionDescription">{description}</span>}
</div>
))}
{
options.map(({ id, label, description }) => (
<div className="mx_FilterDropdown_option" data-testid={`filter-option-${id}`} key={id}>
{id === value && <CheckmarkIcon className="mx_FilterDropdown_optionSelectedIcon" />}
<span className="mx_FilterDropdown_optionLabel">{label}</span>
{!!description && <span className="mx_FilterDropdown_optionDescription">{description}</span>}
</div>
)) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown>
);
};

View file

@ -96,17 +96,24 @@ export default class ImageView extends React.Component<IProps, IState> {
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<IProps, IState> {
}
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
private renderContextMenu(): JSX.Element {
let contextMenu: JSX.Element | undefined;
if (this.state.contextMenuDisplayed) {
if (this.state.contextMenuDisplayed && this.props.mxEvent) {
contextMenu = (
<MessageContextMenu
{...aboveLeftOf(this.contextMenuButton.current.getBoundingClientRect())}
@ -445,7 +454,7 @@ export default class ImageView extends React.Component<IProps, IState> {
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();

View file

@ -378,6 +378,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState>
private onMouseMove = (ev: MouseEvent): void => {
const { clientX: x, clientY: y } = ev;
const { contentRect } = this.state;
if (!contentRect) return;
const targetRect = this.target.getBoundingClientRect();
let direction: Direction;

View file

@ -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<IProps> = ({
<div key={JoinRule.Public} className="mx_JoinRuleDropdown_public">
{labelPublic}
</div>,
];
] as NonEmptyArray<ReactElement & { key: string }>;
if (labelRestricted) {
options.unshift(
<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{labelRestricted}
</div>,
(
<div key={JoinRule.Restricted} className="mx_JoinRuleDropdown_restricted">
{labelRestricted}
</div>
) as ReactElement & { key: string },
);
}

View file

@ -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<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
@ -99,7 +100,7 @@ export default class LanguageDropdown extends React.Component<IProps, IState> {
const options = displayedLanguages.map((language) => {
return <div key={language.value}>{language.label}</div>;
});
}) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propagating

View file

@ -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<IProps> = ({
setShow(false);
}, 13000); // hide after being shown for 10 seconds
const uploadRef = useRef<HTMLInputElement>();
const uploadRef = useRef() as RefObject<HTMLInputElement>;
const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
@ -97,7 +97,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
})}
disabled={busy}
onClick={() => {
uploadRef.current.click();
uploadRef.current?.click();
}}
onMouseOver={() => setHover(true)}
onMouseLeave={() => setHover(false)}

View file

@ -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<IProps, IState> {
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<IProps, IState> {
}
}
private async getEvent(eventId: string): Promise<MatrixEvent | null> {
private async getEvent(eventId?: string): Promise<MatrixEvent | null> {
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<IProps, IState> {
this.initialize();
};
private onQuoteClick = async (event: ButtonEvent): Promise<void> => {
private onQuoteClick = async (): Promise<void> => {
if (!this.state.loadedEv) return;
const events = [this.state.loadedEv, ...this.state.events];
let loadedEv: MatrixEvent | null = null;

View file

@ -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<HTMLDivElement> {
export default function RoomTopic({ room, ...props }: IProps): JSX.Element {
const client = useContext(MatrixClientContext);
const ref = useRef<HTMLDivElement>();
const ref = useRef() as RefObject<HTMLDivElement>;
const topic = useTopic(room);
const body = topicToHtml(topic?.text, topic?.html, ref);

View file

@ -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<ReturnType<typeof languageHandler.getAllLanguagesFromJson>>;
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 <div key={language.value}>{language.label}</div>;
});
}) as NonEmptyArray<ReactElement & { key: string }>;
// default value here too, otherwise we need to handle null / undefined;
// values between mounting and the initial value propagating

View file

@ -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<Props> = ({ timeout, onChange }) => {
onOptionChange={onOptionChange}
className="mx_LiveDurationDropdown"
>
{options.map(({ key, label }) => (
<div data-test-id={`live-duration-option-${key}`} key={key}>
{label}
</div>
))}
{
options.map(({ key, label }) => (
<div data-test-id={`live-duration-option-${key}`} key={key}>
{label}
</div>
)) as NonEmptyArray<ReactElement & { key: string }>
}
</Dropdown>
);
};

View file

@ -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<Props> = ({ requestClose }) => {
value={selectedTheme}
label={_t("Space selection")}
>
{themeOptions.map((theme) => (
<div key={theme.id}>{theme.name}</div>
))}
{
themeOptions.map((theme) => <div key={theme.id}>{theme.name}</div>) as NonEmptyArray<
ReactElement & { key: string }
>
}
</Dropdown>
</div>
);

View file

@ -118,7 +118,10 @@ describe("EventListSummary", function () {
...mockClientMethodsUser(),
});
const defaultProps: ComponentProps<typeof EventListSummary> = {
const defaultProps: Omit<
ComponentProps<typeof EventListSummary>,
"summaryLength" | "threshold" | "avatarsMaxLength"
> = {
layout: Layout.Bubble,
events: [],
children: [],

View file

@ -7,8 +7,8 @@ exports[`<FilterDropdown /> renders dropdown options in menu 1`] = `
role="listbox"
>
<div
aria-selected="false"
class="mx_Dropdown_option"
aria-selected="true"
class="mx_Dropdown_option mx_Dropdown_option_highlight"
id="test__one"
role="option"
>